diff --git a/Robust.Client/ResourceManagement/BaseResource.cs b/Robust.Client/ResourceManagement/BaseResource.cs index 4bc1b30fb8b..8d6314e074d 100644 --- a/Robust.Client/ResourceManagement/BaseResource.cs +++ b/Robust.Client/ResourceManagement/BaseResource.cs @@ -8,13 +8,8 @@ namespace Robust.Client.ResourceManagement; /// /// Base resource for the cache. /// -public abstract class BaseResource : IDisposable +public abstract class BaseResource : IBaseResource, IDisposable { - /// - /// Fallback resource path if this one does not exist. - /// - public virtual ResPath? Fallback => null; - /// /// Disposes this resource. /// diff --git a/Robust.Client/ResourceManagement/IBaseResource.cs b/Robust.Client/ResourceManagement/IBaseResource.cs new file mode 100644 index 00000000000..4e4e9574b2c --- /dev/null +++ b/Robust.Client/ResourceManagement/IBaseResource.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Utility; + +namespace Robust.Client.ResourceManagement; + +/// +/// Defines type-level metadata for resource types. +/// +public interface IBaseResource +{ + /// + /// Whether resources of this type can be deterministically removed from the cache. + /// + static virtual bool CanBeRemoved => false; + + /// + /// The fallback path for this resource type (if any). + /// + static virtual ResPath? FallbackPath => null; +} diff --git a/Robust.Client/ResourceManagement/IResourceCache.cs b/Robust.Client/ResourceManagement/IResourceCache.cs index 16a2532c71d..bde0d63b29a 100644 --- a/Robust.Client/ResourceManagement/IResourceCache.cs +++ b/Robust.Client/ResourceManagement/IResourceCache.cs @@ -15,10 +15,10 @@ namespace Robust.Client.ResourceManagement; public interface IResourceCache : IResourceManager { T GetResource(string path, bool useFallback = true) - where T : BaseResource, new(); + where T : BaseResource, IBaseResource, new(); T GetResource(ResPath path, bool useFallback = true) - where T : BaseResource, new(); + where T : BaseResource, IBaseResource, new(); bool TryGetResource(string path, [NotNullWhen(true)] out T? resource) where T : BaseResource, new(); @@ -28,6 +28,10 @@ bool TryGetResource(ResPath path, [NotNullWhen(true)] out T? resource) bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioResource? resource); + bool TryRemoveResource(string path) where T : BaseResource, IBaseResource, new(); + + bool TryRemoveResource(ResPath path) where T : BaseResource, IBaseResource, new(); + void ReloadResource(string path) where T : BaseResource, new(); @@ -41,7 +45,7 @@ void CacheResource(ResPath path, T resource) where T : BaseResource, new(); T GetFallback() - where T : BaseResource, new(); + where T : BaseResource, IBaseResource, new(); IEnumerable> GetAllResources() where T : BaseResource, new(); diff --git a/Robust.Client/ResourceManagement/ResourceCache.cs b/Robust.Client/ResourceManagement/ResourceCache.cs index cdcea4fccfe..8b9d5b70615 100644 --- a/Robust.Client/ResourceManagement/ResourceCache.cs +++ b/Robust.Client/ResourceManagement/ResourceCache.cs @@ -7,7 +7,6 @@ using Robust.Client.Audio; using Robust.Shared.ContentPack; using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Utility; namespace Robust.Client.ResourceManagement; @@ -20,12 +19,12 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt private readonly Dictionary _cachedResources = new(); private readonly Dictionary _fallbacks = new(); - public T GetResource(string path, bool useFallback = true) where T : BaseResource, new() + public T GetResource(string path, bool useFallback = true) where T : BaseResource, IBaseResource, new() { return GetResource(new ResPath(path), useFallback); } - public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, new() + public T GetResource(ResPath path, bool useFallback = true) where T : BaseResource, IBaseResource, new() { var cache = GetTypeData(); if (cache.Resources.TryGetValue(path, out var cached)) @@ -43,11 +42,11 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt } catch (Exception e) { - if (useFallback && resource.Fallback != null) + if (useFallback && T.FallbackPath != 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); + return GetResource(T.FallbackPath.Value, false); } else { @@ -107,6 +106,29 @@ public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioReso 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 (T.FallbackPath == path) + return false; + + var cache = GetTypeData(); + + if (!cache.Resources.TryGetValue(path, out var resource)) + return false; + + cache.Resources.Remove(path); + cache.NonExistent.Remove(path); + resource.Dispose(); + + return true; + } + public void ReloadResource(string path) where T : BaseResource, new() { ReloadResource(new ResPath(path)); @@ -153,20 +175,19 @@ public bool TryGetResource(AudioStream stream, [NotNullWhen(true)] out AudioReso GetTypeData().Resources[path] = resource; } - public T GetFallback() where T : BaseResource, new() + public T GetFallback() where T : BaseResource, IBaseResource, new() { if (_fallbacks.TryGetValue(typeof(T), out var fallback)) { return (T) fallback; } - var res = new T(); - if (res.Fallback == null) + if (T.FallbackPath == null) { throw new InvalidOperationException($"Resource of type '{typeof(T)}' has no fallback."); } - fallback = GetResource(res.Fallback.Value, useFallback: false); + fallback = GetResource(T.FallbackPath.Value, useFallback: false); _fallbacks.Add(typeof(T), fallback); return (T) fallback; } diff --git a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs index 0880f8e9518..5fd015da841 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/RSIResource.cs @@ -18,9 +18,9 @@ namespace Robust.Client.ResourceManagement /// Handles the loading code for RSI files. /// See for the RSI API itself. /// - public sealed class RSIResource : BaseResource + public sealed class RSIResource : BaseResource, IBaseResource { - public override ResPath? Fallback => new("/Textures/error.rsi"); + static ResPath? IBaseResource.FallbackPath => new("/Textures/error.rsi"); public RSI RSI { get; private set; } = default!; diff --git a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs index 8b5fb6f96df..8babbe6b9da 100644 --- a/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs +++ b/Robust.Client/ResourceManagement/ResourceTypes/TextureResource.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Threading; using Robust.Client.Graphics; using Robust.Shared.ContentPack; @@ -13,12 +14,23 @@ namespace Robust.Client.ResourceManagement { - public sealed class TextureResource : BaseResource + public sealed class TextureResource : BaseResource, IBaseResource { private OwnedTexture _texture = default!; - public override ResPath? Fallback => new("/Textures/noSprite.png"); + private bool _disposed; + private readonly Lock _lock = new(); - public Texture Texture => _texture; + static ResPath? IBaseResource.FallbackPath => new("/Textures/noSprite.png"); + static bool IBaseResource.CanBeRemoved => true; + + public Texture Texture + { + get + { + ObjectDisposedException.ThrowIf(_disposed, this); + return _texture; + } + } public override void Load(IDependencyCollection dependencies, ResPath path) { @@ -106,12 +118,14 @@ internal void LoadFinish(IResourceCache cache, LoadStepData data) public override void Reload(IDependencyCollection dependencies, ResPath path, CancellationToken ct = default) { + ObjectDisposedException.ThrowIf(_disposed, this); + var data = new LoadStepData {Path = path}; LoadTextureParameters(dependencies.Resolve(), data); LoadPreTextureData(dependencies.Resolve(), data); - if (data.Image.Width == Texture.Width && data.Image.Height == Texture.Height) + if (data.Image.Width == _texture.Width && data.Image.Height == _texture.Height) { // Dimensions match, rewrite texture in place. _texture.SetSubImage(Vector2i.Zero, data.Image); @@ -127,6 +141,18 @@ public override void Reload(IDependencyCollection dependencies, ResPath path, Ca data.Image.Dispose(); } + public override void Dispose() + { + lock (_lock) + { + if (_disposed) return; + _disposed = true; + _texture?.Dispose(); + } + + base.Dispose(); + } + internal sealed class LoadStepData { public ResPath Path = default!;