From 49c75de5fc139144b152207ba05f2936a2d25904 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Jun 2017 21:13:01 -0400 Subject: rewrite content interception using latest proposed API (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 64 ++++++++++++++++------ 1 file changed, 47 insertions(+), 17 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index acd3e108..38457862 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -3,11 +3,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; -using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -42,6 +40,9 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// Implementations which change assets after they're loaded. + internal IDictionary> Editors { get; } = new Dictionary>(); + /// The absolute path to the . public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); @@ -49,13 +50,6 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ - /// Construct an instance. - /// The service provider to use to locate services. - /// The root directory to search for content. - /// Encapsulates monitoring and logging. - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, IMonitor monitor) - : this(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, monitor) { } - /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. @@ -66,8 +60,8 @@ namespace StardewModdingAPI.Framework : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { // initialise - this.Monitor = monitor; IReflectionHelper reflection = new ReflectionHelper(); + this.Monitor = monitor; // get underlying fields for interception this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue(); @@ -125,14 +119,20 @@ namespace StardewModdingAPI.Framework if (this.IsNormalisedKeyLoaded(assetName)) return base.Load(assetName); - // load data - T data = base.Load(assetName); - // let mods intercept content - IContentEventHelper helper = new ContentEventHelper(cacheLocale, assetName, data, this.NormaliseAssetName); - ContentEvents.InvokeAfterAssetLoaded(this.Monitor, helper); - this.Cache[assetName] = helper.Data; - return (T)helper.Data; + IAssetInfo info = new AssetInfo(cacheLocale, assetName, typeof(T), this.NormaliseAssetName); + Lazy data = new Lazy(() => new AssetDataForObject(info.Locale, info.AssetName, base.Load(assetName), this.NormaliseAssetName)); + if (this.TryOverrideAssetLoad(info, data, out T result)) + { + if (result == null) + throw new InvalidCastException($"Can't override asset '{assetName}' with a null value."); + + this.Cache[assetName] = result; + return result; + } + + // fallback to default behavior + return base.Load(assetName); } /// Inject an asset into the cache. @@ -171,5 +171,35 @@ namespace StardewModdingAPI.Framework ? locale : null; } + + /// Try to override an asset being loaded. + /// The asset type. + /// The asset metadata. + /// The loaded asset data. + /// The asset to use instead. + /// Returns whether the asset should be overridden by . + private bool TryOverrideAssetLoad(IAssetInfo info, Lazy data, out T result) + { + bool edited = false; + + // apply editors + foreach (var modEditors in this.Editors) + { + IModMetadata mod = modEditors.Key; + foreach (IAssetEditor editor in modEditors.Value) + { + if (!editor.CanEdit(info)) + continue; + + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + editor.Edit(data.Value); + edited = true; + } + } + + // return result + result = edited ? (T)data.Value.Data : default(T); + return edited; + } } } -- cgit From 271843d8614b916aa69273b778971cff0a02ce0d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 9 Jun 2017 22:10:59 -0400 Subject: tweak asset interception code to simplify future work (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 60 ++++++++-------------- 1 file changed, 22 insertions(+), 38 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 38457862..d269cafa 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -111,28 +111,16 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { - // get normalised metadata assetName = this.NormaliseAssetName(assetName); - string cacheLocale = this.GetCacheLocale(assetName); // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) return base.Load(assetName); - // let mods intercept content - IAssetInfo info = new AssetInfo(cacheLocale, assetName, typeof(T), this.NormaliseAssetName); - Lazy data = new Lazy(() => new AssetDataForObject(info.Locale, info.AssetName, base.Load(assetName), this.NormaliseAssetName)); - if (this.TryOverrideAssetLoad(info, data, out T result)) - { - if (result == null) - throw new InvalidCastException($"Can't override asset '{assetName}' with a null value."); - - this.Cache[assetName] = result; - return result; - } - - // fallback to default behavior - return base.Load(assetName); + // load asset + T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load(assetName)); + this.Cache[assetName] = asset; + return asset; } /// Inject an asset into the cache. @@ -162,27 +150,21 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } - /// Get the locale for which the asset name was saved, if any. - /// The normalised asset name. - private string GetCacheLocale(string normalisedAssetName) - { - string locale = this.GetKeyLocale.Invoke(); - return this.Cache.ContainsKey($"{normalisedAssetName}.{locale}") - ? locale - : null; - } - - /// Try to override an asset being loaded. + /// Read an asset with support for asset interceptors. /// The asset type. - /// The asset metadata. - /// The loaded asset data. - /// The asset to use instead. - /// Returns whether the asset should be overridden by . - private bool TryOverrideAssetLoad(IAssetInfo info, Lazy data, out T result) + /// The current content locale. + /// The normalised asset path relative to the loader root directory, not including the .xnb extension. + /// Get the asset from the underlying content manager. + private T GetAssetWithInterceptors(string locale, string normalisedKey, Func getData) { - bool edited = false; + // get metadata + IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); + + // load asset + T asset = getData(); - // apply editors + // edit asset + IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); foreach (var modEditors in this.Editors) { IModMetadata mod = modEditors.Key; @@ -192,14 +174,16 @@ namespace StardewModdingAPI.Framework continue; this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - editor.Edit(data.Value); - edited = true; + editor.Edit(data); + if (data.Data == null) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); + if (!(data.Data is T)) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); } } // return result - result = edited ? (T)data.Value.Data : default(T); - return edited; + return (T)data.Data; } } } -- cgit From 3b6adf3c2676fa8f73997f9c1f8ec5f727f73690 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:39:04 -0400 Subject: reset asset cache when a new interceptor is added (#255) This lets new interceptors edit assets loaded before they were added, particularly assets loaded before mods are initialised. --- src/StardewModdingAPI/Framework/SContentManager.cs | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index d269cafa..24585963 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -5,10 +5,14 @@ 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 { @@ -59,6 +63,10 @@ namespace StardewModdingAPI.Framework public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { + // validate + if (monitor == null) + throw new ArgumentNullException(nameof(monitor)); + // initialise IReflectionHelper reflection = new ReflectionHelper(); this.Monitor = monitor; @@ -130,6 +138,7 @@ namespace StardewModdingAPI.Framework public void Inject(string assetName, T value) { assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; } @@ -139,6 +148,56 @@ namespace StardewModdingAPI.Framework return this.GetKeyLocale.Invoke(); } + /// Reset the asset cache and reload the game's static assets. + /// This implementation is derived from . + public void Reset() + { + 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")); + } + /********* ** Private methods *********/ -- cgit From 306427786b9ae349e3f33ca2e4be6a79b63cf6ce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 19:55:08 -0400 Subject: let mods implement IAssetEditor for simple cases (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 46 +++++++++++++++------- 1 file changed, 32 insertions(+), 14 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 24585963..1ee1eae6 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -224,25 +224,43 @@ namespace StardewModdingAPI.Framework // edit asset IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); - foreach (var modEditors in this.Editors) + foreach (var entry in this.GetAssetEditors()) { - IModMetadata mod = modEditors.Key; - foreach (IAssetEditor editor in modEditors.Value) - { - if (!editor.CanEdit(info)) - continue; - - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - editor.Edit(data); - if (data.Data == null) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); - if (!(data.Data is T)) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); - } + IModMetadata mod = entry.Mod; + IAssetEditor editor = entry.Editor; + + if (!editor.CanEdit(info)) + continue; + + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + editor.Edit(data); + if (data.Data == null) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); + if (!(data.Data is T)) + throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); } // return result return (T)data.Data; } + + /// Get all registered asset editors. + private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() + { + foreach (var entry in this.Editors) + { + IModMetadata metadata = entry.Key; + IList editors = entry.Value; + + // special case if mod implements interface + // ReSharper disable once SuspiciousTypeConversion.Global + if (metadata.Mod is IAssetEditor modAsEditor) + yield return (metadata, modAsEditor); + + // registered editors + foreach (IAssetEditor editor in editors) + yield return (metadata, editor); + } + } } } -- cgit From 600ef562861fe306390b78ee8f08036f0872e92c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 21:31:21 -0400 Subject: improve error handling when mods set invalid asset value (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 30 +++++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 1ee1eae6..53afb729 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -219,31 +219,47 @@ namespace StardewModdingAPI.Framework // get metadata IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); - // load asset - T asset = getData(); // edit asset - IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + IAssetData data = this.GetAssetData(info, getData()); foreach (var entry in this.GetAssetEditors()) { + // check for match IModMetadata mod = entry.Mod; IAssetEditor editor = entry.Editor; - if (!editor.CanEdit(info)) continue; + // try edit this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + object prevAsset = data.Data; editor.Edit(data); + + // validate edit if (data.Data == null) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value."); - if (!(data.Data is T)) - throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'."); + { + data = this.GetAssetData(info, prevAsset); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value; ignoring override.", LogLevel.Warn); + } + else if (!(data.Data is T)) + { + data = this.GetAssetData(info, prevAsset); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + } } // return result return (T)data.Data; } + /// Get an asset edit helper. + /// The asset info. + /// The loaded asset data. + private IAssetData GetAssetData(IAssetInfo info, object asset) + { + return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + } + /// Get all registered asset editors. private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() { -- cgit From f95c7e8d72014f8008886031cebf7b12aeb7ed46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Jul 2017 23:13:43 -0400 Subject: add support for asset loaders (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 159 +++++++++++++++------ 1 file changed, 115 insertions(+), 44 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 53afb729..0a8a0873 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -44,7 +44,10 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Implementations which change assets after they're loaded. + /// Interceptors which provide the initial versions of matching assets. + internal IDictionary> Loaders { get; } = new Dictionary>(); + + /// Interceptors which edit matching assets after they're loaded. internal IDictionary> Editors { get; } = new Dictionary>(); /// The absolute path to the . @@ -126,9 +129,17 @@ namespace StardewModdingAPI.Framework return base.Load(assetName); // load asset - T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load(assetName)); - this.Cache[assetName] = asset; - return asset; + T data; + { + 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; + } + + // update cache & return data + this.Cache[assetName] = data; + return data; } /// Inject an asset into the cache. @@ -198,6 +209,7 @@ namespace StardewModdingAPI.Framework Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); } + /********* ** Private methods *********/ @@ -209,73 +221,132 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } - /// Read an asset with support for asset interceptors. - /// The asset type. - /// The current content locale. - /// The normalised asset path relative to the loader root directory, not including the .xnb extension. - /// Get the asset from the underlying content manager. - private T GetAssetWithInterceptors(string locale, string normalisedKey, Func getData) + /// Load the initial asset from the registered . + /// The basic asset metadata. + /// Returns the loaded asset metadata, or null if no loader matched. + private IAssetData ApplyLoader(IAssetInfo info) { - // get metadata - IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName); + // find matching loaders + var loaders = this.GetInterceptors(this.Loaders) + .Where(entry => + { + try + { + return entry.Interceptor.CanLoad(info); + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return false; + } + }) + .ToArray(); + + // validate loaders + if (!loaders.Any()) + return null; + if (loaders.Length > 1) + { + string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); + this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); + return null; + } + + // fetch asset from loader + IModMetadata mod = loaders[0].Mod; + IAssetLoader loader = loaders[0].Interceptor; + T data; + try + { + data = loader.Load(info); + this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{mod.DisplayName} crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + return null; + } + + // validate asset + if (data == null) + { + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error); + return null; + } + + // return matched asset + return new AssetDataForObject(info, data, this.NormaliseAssetName); + } + /// Apply any to a loaded asset. + /// The asset type. + /// The basic asset metadata. + /// The loaded asset. + private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset) + { + IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.NormaliseAssetName); // edit asset - IAssetData data = this.GetAssetData(info, getData()); - foreach (var entry in this.GetAssetEditors()) + foreach (var entry in this.GetInterceptors(this.Editors)) { // check for match IModMetadata mod = entry.Mod; - IAssetEditor editor = entry.Editor; - if (!editor.CanEdit(info)) + IAssetEditor editor = entry.Interceptor; + try + { + if (!editor.CanEdit(info)) + continue; + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; + } // try edit - this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); - object prevAsset = data.Data; - editor.Edit(data); + object prevAsset = asset.Data; + try + { + editor.Edit(asset); + this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace); + } + catch (Exception ex) + { + this.Monitor.Log($"{entry.Mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + } // validate edit - if (data.Data == null) + if (asset.Data == null) { - data = this.GetAssetData(info, prevAsset); - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value; ignoring override.", LogLevel.Warn); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); } - else if (!(data.Data is T)) + else if (!(asset.Data is T)) { - data = this.GetAssetData(info, prevAsset); - this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + this.Monitor.Log($"{mod.DisplayName} incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn); + asset = GetNewData(prevAsset); } } // return result - return (T)data.Data; - } - - /// Get an asset edit helper. - /// The asset info. - /// The loaded asset data. - private IAssetData GetAssetData(IAssetInfo info, object asset) - { - return new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName); + return asset; } - /// Get all registered asset editors. - private IEnumerable<(IModMetadata Mod, IAssetEditor Editor)> GetAssetEditors() + /// Get all registered interceptors from a list. + private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors(IDictionary> entries) { - foreach (var entry in this.Editors) + foreach (var entry in entries) { IModMetadata metadata = entry.Key; - IList editors = entry.Value; + IList interceptors = entry.Value; - // special case if mod implements interface - // ReSharper disable once SuspiciousTypeConversion.Global - if (metadata.Mod is IAssetEditor modAsEditor) - yield return (metadata, modAsEditor); + // special case if mod is an interceptor + if (metadata.Mod is T modAsInterceptor) + yield return (metadata, modAsInterceptor); // registered editors - foreach (IAssetEditor editor in editors) - yield return (metadata, editor); + foreach (T interceptor in interceptors) + yield return (metadata, interceptor); } } } -- cgit From 771263299cae11d464c25c5291e59507c639e822 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 01:03:13 -0400 Subject: add SMAPI 2.0 compile mode --- src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0a8a0873..5707aab1 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -206,7 +206,7 @@ namespace StardewModdingAPI.Framework // from Farmer constructor if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load($"Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + Game1.player.FarmerRenderer = new FarmerRenderer(this.Load("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); } -- cgit From 136525b40df5d47b8e398a394af081e19efcf86c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 01:29:56 -0400 Subject: remove System.ValueTuple This caused reference errors on Linux/Mac, and there aren't enough use cases to look into it further for now. --- src/StardewModdingAPI/Framework/SContentManager.cs | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 5707aab1..ebf1c8a5 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -232,11 +232,11 @@ namespace StardewModdingAPI.Framework { try { - return entry.Interceptor.CanLoad(info); + return entry.Value.CanLoad(info); } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{entry.Key.DisplayName} crashed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -247,14 +247,14 @@ namespace StardewModdingAPI.Framework return null; if (loaders.Length > 1) { - string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); + string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); return null; } // fetch asset from loader - IModMetadata mod = loaders[0].Mod; - IAssetLoader loader = loaders[0].Interceptor; + IModMetadata mod = loaders[0].Key; + IAssetLoader loader = loaders[0].Value; T data; try { @@ -290,8 +290,8 @@ namespace StardewModdingAPI.Framework foreach (var entry in this.GetInterceptors(this.Editors)) { // check for match - IModMetadata mod = entry.Mod; - IAssetEditor editor = entry.Interceptor; + IModMetadata mod = entry.Key; + IAssetEditor editor = entry.Value; try { if (!editor.CanEdit(info)) @@ -299,7 +299,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{mod.DisplayName} crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"{entry.Mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{mod.DisplayName} crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } // validate edit @@ -333,7 +333,7 @@ namespace StardewModdingAPI.Framework } /// Get all registered interceptors from a list. - private IEnumerable<(IModMetadata Mod, T Interceptor)> GetInterceptors(IDictionary> entries) + private IEnumerable> GetInterceptors(IDictionary> entries) { foreach (var entry in entries) { @@ -342,11 +342,11 @@ namespace StardewModdingAPI.Framework // special case if mod is an interceptor if (metadata.Mod is T modAsInterceptor) - yield return (metadata, modAsInterceptor); + yield return new KeyValuePair(metadata, modAsInterceptor); // registered editors foreach (T interceptor in interceptors) - yield return (metadata, interceptor); + yield return new KeyValuePair(metadata, interceptor); } } } -- cgit From 96da7c1cbc19e079e06fe8c7c857ffe86c0d9848 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Jul 2017 14:49:29 -0400 Subject: fix crash in new content manager when returning to title (#255) --- src/StardewModdingAPI/Framework/SContentManager.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index ebf1c8a5..42c3b0e6 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -349,5 +349,23 @@ namespace StardewModdingAPI.Framework yield return new KeyValuePair(metadata, interceptor); } } + + /// Dispose all game resources. + /// Whether the content manager is disposing (rather than finalising). + protected override void Dispose(bool disposing) + { + if (!disposing) + return; + + // Clear cache & reload all assets. While that may seem perverse during disposal, it's + // necessary due to limitations in the way SMAPI currently intercepts content assets. + // + // The game uses multiple content managers while SMAPI needs one and only one. The game + // only disposes some of its content managers when returning to title, which means SMAPI + // 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(); + } } } -- cgit From c5e106801e9137078decfd6b6e3761240b47f94e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 7 Jul 2017 11:29:17 -0400 Subject: split reflection logic out of mod helper (#318) --- src/StardewModdingAPI/Framework/SContentManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework/SContentManager.cs') diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 42c3b0e6..669b0e7a 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Framework throw new ArgumentNullException(nameof(monitor)); // initialise - IReflectionHelper reflection = new ReflectionHelper(); + var reflection = new Reflector(); this.Monitor = monitor; // get underlying fields for interception -- cgit