using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Framework.Content { /// A low-level wrapper around the content cache which handles reading, writing, and invalidating entries in the cache. This doesn't handle any higher-level logic like localization, loading content, etc. It assumes all keys passed in are already normalized. internal class ContentCache { /********* ** Fields *********/ /// The underlying asset cache. private readonly Dictionary Cache; /********* ** Accessors *********/ /// Get or set the value of a raw cache entry. /// The cache key. public object this[string key] { get => this.Cache[key]; set => this.Cache[key] = value; } /// The current cache keys. public Dictionary.KeyCollection Keys => this.Cache.Keys; /********* ** Public methods *********/ /**** ** Constructor ****/ /// Construct an instance. /// The asset cache for the underlying content manager. public ContentCache(Dictionary loadedAssets) { this.Cache = loadedAssets; } /**** ** Fetch ****/ /// Get whether the cache contains a given key. /// The cache key. public bool ContainsKey(string key) { return this.Cache.ContainsKey(key); } /**** ** Normalize ****/ /// Normalize path separators in an asset name. /// The file path to normalize. [Pure] [return: NotNullIfNotNull("path")] public string? NormalizePathSeparators(string? path) { return PathUtilities.NormalizeAssetName(path); } /// Normalize a cache key so it's consistent with the underlying cache. /// The asset key. /// This is equivalent to with added file extension logic. [Pure] public string NormalizeKey(string key) { key = this.NormalizePathSeparators(key); return key.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase) ? key[..^4] : key; } /**** ** Remove ****/ /// Remove an asset with the given key. /// The cache key. /// Whether to dispose the entry value, if applicable. /// Returns the removed key (if any). public bool Remove(string key, bool dispose) { // remove and get entry if (!this.Cache.Remove(key, out object? value)) return false; // dispose & remove entry if (dispose && value is IDisposable disposable) disposable.Dispose(); return true; } /// Purge assets matching from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be when they're being invalidated as part of a , to avoid crashing the game. /// Returns any removed keys. public IEnumerable Remove(Func predicate, bool dispose) { List removed = new(); foreach ((string key, object value) in this.Cache) { if (predicate(key, value)) removed.Add(key); } foreach (string key in removed) this.Remove(key, dispose); return removed.Count == 0 ? Enumerable.Empty() // let GC collect the list in gen0 instead of potentially living longer : removed; } } }