using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using Robust.Client.Audio; using Robust.Shared.ContentPack; using Robust.Shared.IoC; using Robust.Shared.Utility; namespace Robust.Client.ResourceManagement; /// /// Handles caching of /// internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable { private readonly Dictionary _cachedResources = new(); private readonly Dictionary _fallbacks = new(); public T GetResource(string path, bool useFallback = true) where T : BaseResource, new() { return GetResource(new ResPath(path), useFallback); } public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, new() { var cache = GetTypeData(); if (cache.Resources.TryGetValue(path, out var cached)) { return (T) cached; } var resource = new T(); try { var dependencies = IoCManager.Instance!; resource.Load(dependencies, path); cache.Resources[path] = resource; return resource; } catch (Exception e) { if (useFallback && resource.Fallback != null) { Sawmill.Error( $"Exception while loading resource {typeof(T)} at '{path}', resorting to fallback.\n{Environment.StackTrace}\n{e}"); return GetResource(resource.Fallback.Value, false); } else { Sawmill.Error( $"Exception while loading resource {typeof(T)} at '{path}', no fallback available\n{Environment.StackTrace}\n{e}"); throw; } } } public bool TryGetResource(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new() { return TryGetResource(new ResPath(path), out resource); } public bool TryGetResource(ResPath path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new() { var cache = GetTypeData(); if (cache.Resources.TryGetValue(path, out var cached)) { resource = (T) cached; return true; } if (cache.NonExistent.Contains(path)) { resource = null; return false; } var _resource = new T(); try { var dependencies = IoCManager.Instance!; _resource.Load(dependencies, path); resource = _resource; cache.Resources[path] = resource; return true; } catch (FileNotFoundException) { cache.NonExistent.Add(path); resource = null; return false; } catch (Exception e) { Sawmill.Error($"Exception while loading resource {typeof(T)} at '{path}'\n{e}"); resource = null; return false; } } public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioResource? resource) { resource = new AudioResource(stream); return true; } public bool TryRemoveResource(string path) where T : BaseResource, IBaseResource, new() => TryRemoveResource(new ResPath(path)); public bool TryRemoveResource(ResPath path) where T : BaseResource, IBaseResource, new() { if (!T.CanBeRemoved) throw new NotSupportedException($"Resource type '{typeof(T)}' does not support deterministic removal."); if (new T().Fallback == path) return false; var cache = GetTypeData(); if (!cache.Resources.Remove(path, out var resource)) return false; cache.NonExistent.Remove(path); resource.Dispose(); return true; } public void ReloadResource(string path) where T : BaseResource, new() { ReloadResource(new ResPath(path)); } public void ReloadResource(ResPath path) where T : BaseResource, new() { var cache = GetTypeData(); if (!cache.Resources.TryGetValue(path, out var res)) { return; } try { var dependencies = IoCManager.Instance!; res.Reload(dependencies, path); } catch (Exception e) { Sawmill.Error($"Exception while reloading resource {typeof(T)} at '{path}'\n{e}"); throw; } } public bool HasResource(string path) where T : BaseResource, new() { return HasResource(new ResPath(path)); } public bool HasResource(ResPath path) where T : BaseResource, new() { return TryGetResource(path, out var _); } public void CacheResource(string path, T resource) where T : BaseResource, new() { CacheResource(new ResPath(path), resource); } public void CacheResource(ResPath path, T resource) where T : BaseResource, new() { GetTypeData().Resources[path] = resource; } public T GetFallback() where T : BaseResource, new() { if (_fallbacks.TryGetValue(typeof(T), out var fallback)) { return (T) fallback; } var res = new T(); if (res.Fallback == null) { throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback."); } fallback = GetResource(res.Fallback.Value, useFallback: false); _fallbacks.Add(typeof(T), fallback); return (T) fallback; } public IEnumerable> GetAllResources() where T : BaseResource, new() { return GetTypeData().Resources.Select(p => new KeyValuePair(p.Key, (T) p.Value)); } public event Action? OnRawTextureLoaded; public event Action? OnRsiLoaded; #region IDisposable Members private bool disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (disposed) { return; } if (disposing) { foreach (var res in _cachedResources.Values.SelectMany(dict => dict.Resources.Values)) { res.Dispose(); } } disposed = true; } ~ResourceCache() { Dispose(false); } #endregion IDisposable Members [MethodImpl(MethodImplOptions.AggressiveInlining)] private TypeData GetTypeData() { return _cachedResources.GetOrNew(typeof(T)); } public void TextureLoaded(TextureLoadedEventArgs eventArgs) { OnRawTextureLoaded?.Invoke(eventArgs); } public void RsiLoaded(RsiLoadedEventArgs eventArgs) { OnRsiLoaded?.Invoke(eventArgs); } private sealed class TypeData { public readonly Dictionary Resources = new(); // List of resources which DON'T exist. // Needed to avoid innocuous TryGet calls repeatedly trying to re-load non-existent resources from disk. public readonly HashSet NonExistent = new(); } }