From 4ea6a4102bb69b72391334c4825bd393eff6ac97 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 15:08:14 -0400 Subject: add support for partial cache invalidation (#335) --- src/StardewModdingAPI/Framework/SContentManager.cs | 173 +++++++++++++++------ 1 file changed, 123 insertions(+), 50 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 669b0e7a..e6b0cac2 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -5,14 +5,10 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Objects; -using StardewValley.Projectiles; namespace StardewModdingAPI.Framework { @@ -40,6 +36,12 @@ namespace StardewModdingAPI.Framework /// The private method which generates the locale portion of an asset name. private readonly IPrivateMethod GetKeyLocale; + /// The language codes used in asset keys. + private IDictionary KeyLocales; + + /// The game's static asset setters by normalised asset name. + private readonly IDictionary CoreAssetSetters; + /********* ** Accessors @@ -86,6 +88,48 @@ namespace StardewModdingAPI.Framework } else this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + + // get asset key locales + this.KeyLocales = this.GetKeyLocales(reflection); + this.CoreAssetSetters = this.GetCoreAssetSetters(); + + } + + /// Get methods to reload core game assets by normalised key. + private IDictionary GetCoreAssetSetters() + { + return Constants.GetCoreAssetSetters() + .ToDictionary>, string, Action>( + p => this.NormaliseAssetName(p.Key), + p => () => p.Value(this, p.Key) + ); + } + + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; } /// Normalise path separators in a file path. For asset keys, see instead. @@ -159,54 +203,55 @@ namespace StardewModdingAPI.Framework return this.GetKeyLocale.Invoke(); } + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + IEnumerable GetAllAssetKeys() + { + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } + } + + return GetAllAssetKeys().Distinct(); + } + /// Reset the asset cache and reload the game's static assets. + /// Matches the asset keys to invalidate. /// This implementation is derived from . - public void Reset() + public void InvalidateCache(Func predicate) { - this.Monitor.Log("Resetting asset cache...", LogLevel.Trace); - this.Cache.Clear(); - - // from Game1.LoadContent - Game1.daybg = this.Load("LooseSprites\\daybg"); - Game1.nightbg = this.Load("LooseSprites\\nightbg"); - Game1.menuTexture = this.Load("Maps\\MenuTiles"); - Game1.lantern = this.Load("LooseSprites\\Lighting\\lantern"); - Game1.windowLight = this.Load("LooseSprites\\Lighting\\windowLight"); - Game1.sconceLight = this.Load("LooseSprites\\Lighting\\sconceLight"); - Game1.cauldronLight = this.Load("LooseSprites\\Lighting\\greenLight"); - Game1.indoorWindowLight = this.Load("LooseSprites\\Lighting\\indoorWindowLight"); - Game1.shadowTexture = this.Load("LooseSprites\\shadow"); - Game1.mouseCursors = this.Load("LooseSprites\\Cursors"); - Game1.controllerMaps = this.Load("LooseSprites\\ControllerMaps"); - Game1.animations = this.Load("TileSheets\\animations"); - Game1.achievements = this.Load>("Data\\Achievements"); - Game1.NPCGiftTastes = this.Load>("Data\\NPCGiftTastes"); - Game1.dialogueFont = this.Load("Fonts\\SpriteFont1"); - Game1.smallFont = this.Load("Fonts\\SmallFont"); - Game1.tinyFont = this.Load("Fonts\\tinyFont"); - Game1.tinyFontBorder = this.Load("Fonts\\tinyFontBorder"); - Game1.objectSpriteSheet = this.Load("Maps\\springobjects"); - Game1.cropSpriteSheet = this.Load("TileSheets\\crops"); - Game1.emoteSpriteSheet = this.Load("TileSheets\\emotes"); - Game1.debrisSpriteSheet = this.Load("TileSheets\\debris"); - Game1.bigCraftableSpriteSheet = this.Load("TileSheets\\Craftables"); - Game1.rainTexture = this.Load("TileSheets\\rain"); - Game1.buffsIcons = this.Load("TileSheets\\BuffsIcons"); - Game1.objectInformation = this.Load>("Data\\ObjectInformation"); - Game1.bigCraftablesInformation = this.Load>("Data\\BigCraftablesInformation"); - FarmerRenderer.hairStylesTexture = this.Load("Characters\\Farmer\\hairstyles"); - FarmerRenderer.shirtsTexture = this.Load("Characters\\Farmer\\shirts"); - FarmerRenderer.hatsTexture = this.Load("Characters\\Farmer\\hats"); - FarmerRenderer.accessoriesTexture = this.Load("Characters\\Farmer\\accessories"); - Furniture.furnitureTexture = this.Load("TileSheets\\furniture"); - SpriteText.spriteTexture = this.Load("LooseSprites\\font_bold"); - SpriteText.coloredTexture = this.Load("LooseSprites\\font_colored"); - Tool.weaponsTexture = this.Load("TileSheets\\weapons"); - Projectile.projectileSheet = this.Load("TileSheets\\Projectiles"); - - // from Farmer constructor - if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + // find matching asset keys + HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); + if (predicate(assetKey)) + { + purgeAssetKeys.Add(assetKey); + purgeCacheKeys.Add(cacheKey); + } + } + + // purge from cache + foreach (string key in purgeCacheKeys) + this.Cache.Remove(key); + + // reload core game assets + int reloaded = 0; + foreach (string key in purgeAssetKeys) + { + if (this.CoreAssetSetters.TryGetValue(key, out Action reloadAsset)) + { + reloadAsset(); + reloaded++; + } + } + + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); } @@ -221,6 +266,33 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset key. + /// The asset locale code (or null if not localised). + private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.KeyLocales.ContainsKey(suffix)) + { + assetKey = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetKey = cacheKey; + localeCode = null; + } + /// Load the initial asset from the registered . /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. @@ -365,7 +437,8 @@ namespace StardewModdingAPI.Framework // can't know which assets are meant to be disposed. Here we remove current assets from // the cache, but don't dispose them to avoid crashing any code that still references // them. The garbage collector will eventually clean up any unused assets. - this.Reset(); + this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); + this.InvalidateCache(p => true); } } } -- cgit From 467ad2ffd45f7c034b89b668883bb5271524821d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 17:36:31 -0400 Subject: let mods invalidate cached assets by name or type (#335) --- .../Framework/ModHelpers/ContentHelper.cs | 27 +++++++++++++++++++++- src/StardewModdingAPI/Framework/SContentManager.cs | 17 ++++++++++---- src/StardewModdingAPI/IContentHelper.cs | 14 +++++++++++ src/StardewModdingAPI/Program.cs | 11 +++++---- 4 files changed, 59 insertions(+), 10 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 5f72176e..c052759f 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The friendly mod name for use in errors. private readonly string ModName; + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + /********* ** Accessors @@ -58,13 +61,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The absolute path to the mod folder. /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + /// Encapsulates monitoring and logging. + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; } /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. @@ -176,6 +181,26 @@ namespace StardewModdingAPI.Framework.ModHelpers } } + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder) + { + this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, source); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + public bool InvalidateCache() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index e6b0cac2..0741d04d 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -220,8 +220,9 @@ namespace StardewModdingAPI.Framework /// Reset the asset cache and reload the game's static assets. /// Matches the asset keys to invalidate. + /// Returns whether any cache entries were invalidated. /// This implementation is derived from . - public void InvalidateCache(Func predicate) + public bool InvalidateCache(Func predicate) { // find matching asset keys HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); @@ -229,7 +230,8 @@ namespace StardewModdingAPI.Framework foreach (string cacheKey in this.Cache.Keys) { this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); - if (predicate(assetKey)) + Type type = this.Cache[cacheKey].GetType(); + if (predicate(assetKey, type)) { purgeAssetKeys.Add(assetKey); purgeCacheKeys.Add(cacheKey); @@ -251,7 +253,14 @@ namespace StardewModdingAPI.Framework } } - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + // report result + if (purgeCacheKeys.Any()) + { + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + return true; + } + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + return false; } @@ -438,7 +447,7 @@ namespace StardewModdingAPI.Framework // the cache, but don't dispose them to avoid crashing any code that still references // them. The garbage collector will eventually clean up any unused assets. this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); - this.InvalidateCache(p => true); + this.InvalidateCache((key, type) => true); } } } diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 32a9ff19..9fe29e4d 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -20,5 +20,19 @@ namespace StardewModdingAPI /// Where to search for a matching content asset. /// The is empty or contains invalid characters. string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); + +#if !SMAPI_1_x + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder); + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + bool InvalidateCache(); +#endif } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 56c56431..969695aa 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -707,15 +707,16 @@ namespace StardewModdingAPI // inject data { + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); - mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); + mod.Monitor = monitor; #if SMAPI_1_x mod.PathOnDisk = metadata.DirectoryPath; #endif @@ -818,7 +819,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => @@ -826,13 +827,13 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } }; } } this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } /// Reload translations for all mods. -- cgit From eeee6b185d5438e5d44ed0da7c23cf19813b29ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 19:39:17 -0400 Subject: use more flexible approach to core asset reloading (#335) --- src/StardewModdingAPI/Constants.cs | 62 ------------ src/StardewModdingAPI/Framework/SContentManager.cs | 26 ++--- src/StardewModdingAPI/Metadata/CoreAssets.cs | 107 +++++++++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 115 insertions(+), 81 deletions(-) create mode 100644 src/StardewModdingAPI/Metadata/CoreAssets.cs (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index e85d7e2b..af120850 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -11,9 +11,6 @@ using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Objects; -using StardewValley.Projectiles; namespace StardewModdingAPI { @@ -223,65 +220,6 @@ namespace StardewModdingAPI }; } - /// Get the game's static asset setters by (non-normalised) asset name. - /// Derived from . - internal static IDictionary> GetCoreAssetSetters() - { - return new Dictionary> - { - // from Game1.loadContent - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), - ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), - ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), - ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), - ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), - ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), - ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), - ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), - ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), - ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), - ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), - ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), - ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), - ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), - ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), - ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), - ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), - ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), - ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), - ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), - ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), - ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), - ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), - ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), - ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), - ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), - ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), - ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), - ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), - ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), - ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), - ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), - ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), - ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), - ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), - - // from Farmer constructor - ["Characters\\Farmer\\farmer_base"] = (content, key) => - { - if (Game1.player != null && Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, - ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => - { - if (Game1.player != null && !Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - } - }; - } - /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0741d04d..0854c379 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Metadata; using StardewValley; namespace StardewModdingAPI.Framework @@ -37,10 +38,10 @@ namespace StardewModdingAPI.Framework private readonly IPrivateMethod GetKeyLocale; /// The language codes used in asset keys. - private IDictionary KeyLocales; + private readonly IDictionary KeyLocales; - /// The game's static asset setters by normalised asset name. - private readonly IDictionary CoreAssetSetters; + /// Provides metadata for core game assets. + private readonly CoreAssets CoreAssets; /********* @@ -89,22 +90,12 @@ namespace StardewModdingAPI.Framework else this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - // get asset key locales + // get asset data + this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); - this.CoreAssetSetters = this.GetCoreAssetSetters(); } - /// Get methods to reload core game assets by normalised key. - private IDictionary GetCoreAssetSetters() - { - return Constants.GetCoreAssetSetters() - .ToDictionary>, string, Action>( - p => this.NormaliseAssetName(p.Key), - p => () => p.Value(this, p.Key) - ); - } - /// Get the locale codes (like ja-JP) used in asset keys. /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) @@ -246,11 +237,8 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in purgeAssetKeys) { - if (this.CoreAssetSetters.TryGetValue(key, out Action reloadAsset)) - { - reloadAsset(); + if(this.CoreAssets.ReloadForKey(this, key)) reloaded++; - } } // report result diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/StardewModdingAPI/Metadata/CoreAssets.cs new file mode 100644 index 00000000..256a911a --- /dev/null +++ b/src/StardewModdingAPI/Metadata/CoreAssets.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Objects; +using StardewValley.Projectiles; + +namespace StardewModdingAPI.Metadata +{ + /// Provides metadata about core assets in the game. + internal class CoreAssets + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + /// The static asset setters. + private readonly IDictionary> StaticSetters; + + + /********* + ** Public methods + *********/ + /// Initialise the core asset data. + /// Normalises an asset key to match the cache key. + public CoreAssets(Func getNormalisedPath) + { + this.GetNormalisedPath = getNormalisedPath; + this.StaticSetters = + new Dictionary> + { + // from Game1.loadContent + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), + ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), + ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), + ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), + ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), + ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), + ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), + ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), + ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), + ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), + ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), + ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), + ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), + ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), + ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), + ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), + ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), + ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), + ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), + ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), + ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), + ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), + ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), + ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), + ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), + ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), + ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), + ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), + ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), + ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), + ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), + ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), + ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), + ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), + ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), + + // from Farmer constructor + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + } + } + .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); + } + + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether an asset was reloaded. + public bool ReloadForKey(SContentManager content, string key) + { + // static assets + if (this.StaticSetters.TryGetValue(key, out Action reload)) + { + reload(content, key); + return true; + } + + return false; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 4cef91d9..8bbafca1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 2ec0e0e26a16b94ba4a12d3bb4561d64a7411b34 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 Aug 2017 23:27:07 -0400 Subject: only invalidate cache entries matched by new interceptors --- .../Framework/ModHelpers/ContentHelper.cs | 2 +- src/StardewModdingAPI/Framework/SContentManager.cs | 94 ++++++++++++++-------- src/StardewModdingAPI/Program.cs | 26 +++--- 3 files changed, 79 insertions(+), 43 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index e94d309e..ffa78ff6 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0854c379..9e086870 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; @@ -96,33 +97,6 @@ namespace StardewModdingAPI.Framework } - /// Get the locale codes (like ja-JP) used in asset keys. - /// Simplifies access to private game code. - private IDictionary GetKeyLocales(Reflector reflection) - { - // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; - - // create locale => code map - IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke()] = code; - } - - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; - - return map; - } - /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. public string NormalisePathSeparators(string path) @@ -209,10 +183,39 @@ namespace StardewModdingAPI.Framework return GetAllAssetKeys().Distinct(); } - /// Reset the asset cache and reload the game's static assets. + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns whether any cache entries were invalidated. + public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return false; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + + // invalidate matching keys + return this.InvalidateCache((assetName, assetType) => + { + // get asset metadata + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); + + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); + }); + } + + /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Returns whether any cache entries were invalidated. - /// This implementation is derived from . public bool InvalidateCache(Func predicate) { // find matching asset keys @@ -220,7 +223,7 @@ namespace StardewModdingAPI.Framework HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); foreach (string cacheKey in this.Cache.Keys) { - this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); + this.ParseCacheKey(cacheKey, out string assetKey, out _); Type type = this.Cache[cacheKey].GetType(); if (predicate(assetKey, type)) { @@ -237,7 +240,7 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in purgeAssetKeys) { - if(this.CoreAssets.ReloadForKey(this, key)) + if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } @@ -263,6 +266,33 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + /// Parse a cache key into its component parts. /// The input cache key. /// The original asset key. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0e1930ac..79f8e801 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -806,33 +806,39 @@ namespace StardewModdingAPI } } - // reset cache when needed - // only register listeners after Entry to avoid repeatedly reloading assets during load + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { - // TODO: optimise by only reloading assets the new editors/loaders can intercept helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); } }; } } - this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(editors, loaders); + } } /// Reload translations for all mods. -- cgit From 723ddc255e1c2b399dfb734306fd00912a741e62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 Aug 2017 17:46:45 -0400 Subject: break loops when loading assets through a mod loader --- src/StardewModdingAPI/Framework/Countdown.cs | 44 --------------- src/StardewModdingAPI/Framework/SContentManager.cs | 22 ++++++-- src/StardewModdingAPI/Framework/SGame.cs | 5 +- .../Framework/Utilities/ContextHash.cs | 62 ++++++++++++++++++++++ .../Framework/Utilities/Countdown.cs | 44 +++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 5 +- 6 files changed, 130 insertions(+), 52 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Countdown.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/ContextHash.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/Countdown.cs (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/Countdown.cs b/src/StardewModdingAPI/Framework/Countdown.cs deleted file mode 100644 index 25ca2546..00000000 --- a/src/StardewModdingAPI/Framework/Countdown.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace StardewModdingAPI.Framework -{ - /// Counts down from a baseline value. - internal class Countdown - { - /********* - ** Accessors - *********/ - /// The initial value from which to count down. - public int Initial { get; } - - /// The current value. - public int Current { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The initial value from which to count down. - public Countdown(int initial) - { - this.Initial = initial; - this.Current = initial; - } - - /// Reduce the current value by one. - /// Returns whether the value was decremented (i.e. wasn't already zero). - public bool Decrement() - { - if (this.Current <= 0) - return false; - - this.Current--; - return true; - } - - /// Restart the countdown. - public void Reset() - { - this.Current = this.Initial; - } - } -} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 9e086870..25775291 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -9,6 +9,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; using StardewValley; @@ -44,6 +45,9 @@ namespace StardewModdingAPI.Framework /// Provides metadata for core game assets. private readonly CoreAssets CoreAssets; + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + /********* ** Accessors @@ -139,11 +143,21 @@ namespace StardewModdingAPI.Framework // load asset T data; + if (this.AssetsBeingLoaded.Contains(assetName)) { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - data = (T)asset.Data; + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); } // update cache & return data diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 755f0274..997e0c8c 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -322,7 +323,7 @@ namespace StardewModdingAPI.Framework #if !SMAPI_1_x if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) #endif - this.AfterLoadTimer--; + this.AfterLoadTimer--; if (this.AfterLoadTimer == 0) { diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs new file mode 100644 index 00000000..0d8487bb --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// A wrapper meant for tracking recursive contexts. + /// The key type. + internal class ContextHash : HashSet + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ContextHash() { } + + /// Construct an instance. + /// The implementation to use when comparing values in the set, or null to use the default comparer for the set type. + public ContextHash(IEqualityComparer comparer) + : base(comparer) { } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The key to add. + /// The action to perform. + /// The specified key is already added. + public void Track(T key, Action action) + { + if(this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + action(); + } + finally + { + this.Remove(key); + } + } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The value type returned by the method. + /// The key to add. + /// The action to perform. + public TResult Track(T key, Func action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + return action(); + } + finally + { + this.Remove(key); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs new file mode 100644 index 00000000..921a35ce --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs @@ -0,0 +1,44 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + /// Counts down from a baseline value. + internal class Countdown + { + /********* + ** Accessors + *********/ + /// The initial value from which to count down. + public int Initial { get; } + + /// The current value. + public int Current { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial value from which to count down. + public Countdown(int initial) + { + this.Initial = initial; + this.Current = initial; + } + + /// Reduce the current value by one. + /// Returns whether the value was decremented (i.e. wasn't already zero). + public bool Decrement() + { + if (this.Current <= 0) + return false; + + this.Current--; + return true; + } + + /// Restart the countdown. + public void Reset() + { + this.Current = this.Initial; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d7e10ca5..73112983 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -1,4 +1,4 @@ - + @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -126,7 +127,7 @@ - + -- cgit