diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-01 18:16:09 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-05-01 18:16:09 -0400 |
commit | c8ad50dad1d706a1901798f9396f6becfea36c0e (patch) | |
tree | 28bd818a5db39ec5ece1bd141a28de955950463b /src/SMAPI/Framework/ModHelpers | |
parent | 451b70953ff4c0b1b27ae0de203ad99379b45b2a (diff) | |
parent | f78093bdb58d477b400cde3f19b70ffd6ddf833d (diff) | |
download | SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.gz SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.bz2 SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI/Framework/ModHelpers')
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/BaseHelper.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/CommandHelper.cs | 9 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 108 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/DataHelper.cs | 32 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/GameContentHelper.cs | 145 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/InputHelper.cs | 6 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModContentHelper.cs | 101 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModHelper.cs | 60 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs | 29 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs | 12 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 30 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/TranslationHelper.cs | 10 |
13 files changed, 460 insertions, 103 deletions
diff --git a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs index 5a3d4bed..12390976 100644 --- a/src/SMAPI/Framework/ModHelpers/BaseHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/BaseHelper.cs @@ -4,20 +4,27 @@ namespace StardewModdingAPI.Framework.ModHelpers internal abstract class BaseHelper : IModLinked { /********* + ** Fields + *********/ + /// <summary>The mod using this instance.</summary> + protected readonly IModMetadata Mod; + + + /********* ** Accessors *********/ /// <inheritdoc /> - public string ModID { get; } + public string ModID => this.Mod.Manifest.UniqueID; /********* ** Protected methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> - protected BaseHelper(string modID) + /// <param name="mod">The mod using this instance.</param> + protected BaseHelper(IModMetadata mod) { - this.ModID = modID; + this.Mod = mod; } } } diff --git a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs index 69382009..226a8d69 100644 --- a/src/SMAPI/Framework/ModHelpers/CommandHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/CommandHelper.cs @@ -1,4 +1,5 @@ using System; +using StardewModdingAPI.Framework.Deprecations; namespace StardewModdingAPI.Framework.ModHelpers { @@ -8,9 +9,6 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Fields *********/ - /// <summary>The mod using this instance.</summary> - private readonly IModMetadata Mod; - /// <summary>Manages console commands.</summary> private readonly CommandManager CommandManager; @@ -22,9 +20,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="mod">The mod using this instance.</param> /// <param name="commandManager">Manages console commands.</param> public CommandHelper(IModMetadata mod, CommandManager commandManager) - : base(mod?.Manifest?.UniqueID ?? "SMAPI") + : base(mod) { - this.Mod = mod; this.CommandManager = commandManager; } @@ -40,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool Trigger(string name, string[] arguments) { SCore.DeprecationManager.Warn( - source: SCore.DeprecationManager.GetSourceName(this.ModID), + source: SCore.DeprecationManager.GetMod(this.ModID), nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.ConsoleCommands)}.{nameof(ICommandHelper.Trigger)}", version: "3.8.1", severity: DeprecationLevel.Notice diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index bfca2264..3c2441e8 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -7,12 +7,15 @@ using System.IO; using System.Linq; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Deprecations; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework.ModHelpers { /// <summary>Provides an API for loading content assets.</summary> + [Obsolete] internal class ContentHelper : BaseHelper, IContentHelper { /********* @@ -27,12 +30,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>A content manager for this mod which manages files from the mod's folder.</summary> private readonly ModContentManager ModContentManager; - /// <summary>The friendly mod name for use in errors.</summary> - private readonly string ModName; - /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + /********* ** Accessors @@ -44,16 +47,42 @@ namespace StardewModdingAPI.Framework.ModHelpers public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; /// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary> - internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>(); + internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new(); /// <summary>The observable implementation of <see cref="AssetLoaders"/>.</summary> - internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new ObservableCollection<IAssetLoader>(); + internal ObservableCollection<IAssetLoader> ObservableAssetLoaders { get; } = new(); /// <inheritdoc /> - public IList<IAssetLoader> AssetLoaders => this.ObservableAssetLoaders; + public IList<IAssetLoader> AssetLoaders + { + get + { + SCore.DeprecationManager.Warn( + source: this.Mod, + nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ObservableAssetLoaders; + } + } /// <inheritdoc /> - public IList<IAssetEditor> AssetEditors => this.ObservableAssetEditors; + public IList<IAssetEditor> AssetEditors + { + get + { + SCore.DeprecationManager.Warn( + source: this.Mod, + nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ObservableAssetEditors; + } + } /********* @@ -62,48 +91,62 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Construct an instance.</summary> /// <param name="contentCore">SMAPI's core content logic.</param> /// <param name="modFolderPath">The absolute path to the mod folder.</param> - /// <param name="modID">The unique ID of the relevant mod.</param> - /// <param name="modName">The friendly mod name for use in errors.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) - : base(modID) + /// <param name="reflection">Simplifies access to private code.</param> + public ContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, IMonitor monitor, Reflector reflection) + : base(mod) { - string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID); + string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID); this.ContentCore = contentCore; this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content"); - this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager); - this.ModName = modName; + this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, this.Mod.DisplayName, modFolderPath, this.GameContentManager); this.Monitor = monitor; + this.Reflection = reflection; } /// <inheritdoc /> public T Load<T>(string key, ContentSource source = ContentSource.ModFolder) + where T : notnull { + IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: source == ContentSource.GameContent); + try { this.AssertAndNormalizeAssetName(key); switch (source) { case ContentSource.GameContent: - return this.GameContentManager.Load<T>(key, this.CurrentLocaleConstant, useCache: false); + if (assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)) + { + assetName = this.ContentCore.ParseAssetName(assetName.Name[..^4], allowLocales: true); + SCore.DeprecationManager.Warn( + this.Mod, + "loading assets from the Content folder with a .xnb file extension", + "3.14.0", + DeprecationLevel.Notice + ); + } + + return this.GameContentManager.LoadLocalized<T>(assetName, this.CurrentLocaleConstant, useCache: false); case ContentSource.ModFolder: - return this.ModContentManager.Load<T>(key, Constants.DefaultLanguage, useCache: false); + return this.ModContentManager.LoadExact<T>(assetName, useCache: false); default: - throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); + throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'."); } } - catch (Exception ex) when (!(ex is SContentLoadException)) + catch (Exception ex) when (ex is not SContentLoadException) { - throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); + throw new SContentLoadException($"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex); } } /// <inheritdoc /> [Pure] - public string NormalizeAssetName(string assetName) + public string NormalizeAssetName(string? assetName) { return this.ModContentManager.AssertAndNormalizeAssetName(assetName); } @@ -117,7 +160,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return this.GameContentManager.AssertAndNormalizeAssetName(key); case ContentSource.ModFolder: - return this.ModContentManager.GetInternalAssetKey(key); + return this.ModContentManager.GetInternalAssetKey(key).Name; default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -128,32 +171,41 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache(string key) { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); - this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)).Any(); + this.Monitor.Log($"Requested cache invalidation for '{actualKey}'."); + return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(actualKey)).Any(); } /// <inheritdoc /> public bool InvalidateCache<T>() + where T : notnull { - 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.ContentCore.InvalidateCache((contentManager, key, type) => typeof(T).IsAssignableFrom(type)).Any(); + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible."); + return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any(); } /// <inheritdoc /> public bool InvalidateCache(Func<IAssetInfo, bool> predicate) { - this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); + this.Monitor.Log("Requested cache invalidation for all assets matching a predicate."); return this.ContentCore.InvalidateCache(predicate).Any(); } /// <inheritdoc /> - public IAssetData GetPatchHelper<T>(T data, string assetName = null) + public IAssetData GetPatchHelper<T>(T data, string? assetName = null) + where T : notnull { if (data == null) throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); assetName ??= $"temp/{Guid.NewGuid():N}"; - return new AssetDataForObject(this.CurrentLocale, assetName, data, this.NormalizeAssetName); + + return new AssetDataForObject( + locale: this.CurrentLocale, + assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/), + data: data, + getNormalizedPath: this.NormalizeAssetName, + reflection: this.Reflection + ); } diff --git a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs index d39abc7d..9f4a7ceb 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentPackHelper.cs @@ -22,11 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="contentPacks">The content packs loaded for this mod.</param> /// <param name="createContentPack">Create a temporary content pack.</param> - public ContentPackHelper(string modID, Lazy<IContentPack[]> contentPacks, Func<string, IManifest, IContentPack> createContentPack) - : base(modID) + public ContentPackHelper(IModMetadata mod, Lazy<IContentPack[]> contentPacks, Func<string, IManifest, IContentPack> createContentPack) + : base(mod) { this.ContentPacks = contentPacks; this.CreateContentPack = createContentPack; diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 4cbfd73f..2eaa940a 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -26,11 +26,11 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="jsonHelper">The absolute path to the mod folder.</param> - public DataHelper(string modID, string modFolderPath, JsonHelper jsonHelper) - : base(modID) + public DataHelper(IModMetadata mod, string modFolderPath, JsonHelper jsonHelper) + : base(mod) { this.ModFolderPath = modFolderPath; this.JsonHelper = jsonHelper; @@ -40,19 +40,21 @@ namespace StardewModdingAPI.Framework.ModHelpers ** JSON file ****/ /// <inheritdoc /> - public TModel ReadJsonFile<TModel>(string path) where TModel : class + public TModel? ReadJsonFile<TModel>(string path) + where TModel : class { if (!PathUtilities.IsSafeRelativePath(path)) throw new InvalidOperationException($"You must call {nameof(IModHelper.Data)}.{nameof(this.ReadJsonFile)} with a relative path."); path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path)); - return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data) ? data : null; } /// <inheritdoc /> - public void WriteJsonFile<TModel>(string path, TModel data) where TModel : class + public void WriteJsonFile<TModel>(string path, TModel? data) + where TModel : class { if (!PathUtilities.IsSafeRelativePath(path)) throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonFile)} with a relative path (without directory climbing)."); @@ -69,7 +71,8 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Save file ****/ /// <inheritdoc /> - public TModel ReadSaveData<TModel>(string key) where TModel : class + public TModel? ReadSaveData<TModel>(string key) + where TModel : class { if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.ReadSaveData)} when a save file isn't loaded."); @@ -80,14 +83,15 @@ namespace StardewModdingAPI.Framework.ModHelpers string internalKey = this.GetSaveFileKey(key); foreach (IDictionary<string, string> dataField in this.GetDataFields(Context.LoadStage)) { - if (dataField.TryGetValue(internalKey, out string value)) + if (dataField.TryGetValue(internalKey, out string? value)) return this.JsonHelper.Deserialize<TModel>(value); } return null; } /// <inheritdoc /> - public void WriteSaveData<TModel>(string key, TModel model) where TModel : class + public void WriteSaveData<TModel>(string key, TModel? model) + where TModel : class { if (Context.LoadStage == LoadStage.None) throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when a save file isn't loaded."); @@ -95,7 +99,7 @@ namespace StardewModdingAPI.Framework.ModHelpers throw new InvalidOperationException($"Can't use {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteSaveData)} when connected to a remote host. (Save files are stored on the main player's computer.)"); string internalKey = this.GetSaveFileKey(key); - string data = model != null + string? data = model != null ? this.JsonHelper.Serialize(model, Formatting.None) : null; @@ -112,16 +116,18 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Global app data ****/ /// <inheritdoc /> - public TModel ReadGlobalData<TModel>(string key) where TModel : class + public TModel? ReadGlobalData<TModel>(string key) + where TModel : class { string path = this.GetGlobalDataPath(key); - return this.JsonHelper.ReadJsonFileIfExists(path, out TModel data) + return this.JsonHelper.ReadJsonFileIfExists(path, out TModel? data) ? data : null; } /// <inheritdoc /> - public void WriteGlobalData<TModel>(string key, TModel data) where TModel : class + public void WriteGlobalData<TModel>(string key, TModel? data) + where TModel : class { string path = this.GetGlobalDataPath(key); if (data != null) diff --git a/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs new file mode 100644 index 00000000..232e9287 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/GameContentHelper.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// <inheritdoc cref="IGameContentHelper"/> + internal class GameContentHelper : BaseHelper, IGameContentHelper + { + /********* + ** Fields + *********/ + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCoordinator ContentCore; + + /// <summary>The underlying game content manager.</summary> + private readonly IContentManager GameContentManager; + + /// <summary>The friendly mod name for use in errors.</summary> + private readonly string ModName; + + /// <summary>Encapsulates monitoring and logging.</summary> + private readonly IMonitor Monitor; + + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + + + /********* + ** Accessors + *********/ + /// <inheritdoc /> + public string CurrentLocale => this.GameContentManager.GetLocale(); + + /// <inheritdoc /> + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="contentCore">SMAPI's core content logic.</param> + /// <param name="mod">The mod using this instance.</param> + /// <param name="modName">The friendly mod name for use in errors.</param> + /// <param name="monitor">Encapsulates monitoring and logging.</param> + /// <param name="reflection">Simplifies access to private code.</param> + public GameContentHelper(ContentCoordinator contentCore, IModMetadata mod, string modName, IMonitor monitor, Reflector reflection) + : base(mod) + { + string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID); + + this.ContentCore = contentCore; + this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content"); + this.ModName = modName; + this.Monitor = monitor; + this.Reflection = reflection; + } + + /// <inheritdoc /> + public IAssetName ParseAssetName(string rawName) + { + return this.ContentCore.ParseAssetName(rawName, allowLocales: true); + } + + /// <inheritdoc /> + public T Load<T>(string key) + where T : notnull + { + IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: true); + return this.Load<T>(assetName); + } + + /// <inheritdoc /> + public T Load<T>(IAssetName assetName) + where T : notnull + { + try + { + return this.GameContentManager.LoadLocalized<T>(assetName, this.CurrentLocaleConstant, useCache: true); + } + catch (Exception ex) when (ex is not SContentLoadException) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{assetName}' from the game content.", ex); + } + } + + /// <inheritdoc /> + public bool InvalidateCache(string key) + { + IAssetName assetName = this.ParseAssetName(key); + return this.InvalidateCache(assetName); + } + + /// <inheritdoc /> + public bool InvalidateCache(IAssetName assetName) + { + this.Monitor.Log($"Requested cache invalidation for '{assetName}'."); + return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(assetName)).Any(); + } + + /// <inheritdoc /> + public bool InvalidateCache<T>() + where T : notnull + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible."); + return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any(); + } + + /// <inheritdoc /> + public bool InvalidateCache(Func<IAssetInfo, bool> predicate) + { + this.Monitor.Log("Requested cache invalidation for all assets matching a predicate."); + return this.ContentCore.InvalidateCache(predicate).Any(); + } + + /// <inheritdoc /> + public IAssetData GetPatchHelper<T>(T data, string? assetName = null) + where T : notnull + { + if (data == null) + throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); + + assetName ??= $"temp/{Guid.NewGuid():N}"; + + return new AssetDataForObject( + locale: this.CurrentLocale, + assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true), + data: data, + getNormalizedPath: key => this.ParseAssetName(key).Name, + reflection: this.Reflection + ); + } + + /// <summary>Get the underlying game content manager.</summary> + internal IContentManager GetUnderlyingContentManager() + { + return this.GameContentManager; + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index 88caf4c3..6c158258 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -18,10 +18,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param> - public InputHelper(string modID, Func<SInputState> currentInputState) - : base(modID) + public InputHelper(IModMetadata mod, Func<SInputState> currentInputState) + : base(mod) { this.CurrentInputState = currentInputState; } diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs new file mode 100644 index 00000000..def0b728 --- /dev/null +++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.Xna.Framework.Content; +using StardewModdingAPI.Framework.Content; +using StardewModdingAPI.Framework.ContentManagers; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Framework.ModHelpers +{ + /// <inheritdoc cref="IModContentHelper"/> + internal class ModContentHelper : BaseHelper, IModContentHelper + { + /********* + ** Fields + *********/ + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCoordinator ContentCore; + + /// <summary>A content manager for this mod which manages files from the mod's folder.</summary> + private readonly ModContentManager ModContentManager; + + /// <summary>The friendly mod name for use in errors.</summary> + private readonly string ModName; + + /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary> + private readonly CaseInsensitivePathLookup RelativePathCache; + + /// <summary>Simplifies access to private code.</summary> + private readonly Reflector Reflection; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="contentCore">SMAPI's core content logic.</param> + /// <param name="modFolderPath">The absolute path to the mod folder.</param> + /// <param name="mod">The mod using this instance.</param> + /// <param name="modName">The friendly mod name for use in errors.</param> + /// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param> + /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="relativePathCache"/>.</param> + /// <param name="reflection">Simplifies access to private code.</param> + public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, string modName, IContentManager gameContentManager, CaseInsensitivePathLookup relativePathCache, Reflector reflection) + : base(mod) + { + string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID); + + this.ContentCore = contentCore; + this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager); + this.ModName = modName; + this.RelativePathCache = relativePathCache; + this.Reflection = reflection; + } + + /// <inheritdoc /> + public T Load<T>(string relativePath) + where T : notnull + { + relativePath = this.RelativePathCache.GetAssetName(relativePath); + + IAssetName assetName = this.ContentCore.ParseAssetName(relativePath, allowLocales: false); + + try + { + return this.ModContentManager.LoadExact<T>(assetName, useCache: false); + } + catch (Exception ex) when (ex is not SContentLoadException) + { + throw new SContentLoadException($"{this.ModName} failed loading content asset '{relativePath}' from its mod folder.", ex); + } + } + + /// <inheritdoc /> + public IAssetName GetInternalAssetName(string relativePath) + { + relativePath = this.RelativePathCache.GetAssetName(relativePath); + return this.ModContentManager.GetInternalAssetKey(relativePath); + } + + /// <inheritdoc /> + public IAssetData GetPatchHelper<T>(T data, string? relativePath = null) + where T : notnull + { + if (data == null) + throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); + + relativePath = relativePath != null + ? this.RelativePathCache.GetAssetName(relativePath) + : $"temp/{Guid.NewGuid():N}"; + + return new AssetDataForObject( + locale: this.ContentCore.GetLocale(), + assetName: this.ContentCore.ParseAssetName(relativePath, allowLocales: false), + data: data, + getNormalizedPath: key => this.ContentCore.ParseAssetName(key, allowLocales: false).Name, + reflection: this.Reflection + ); + } + } +} diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs index 058bff83..a23a9beb 100644 --- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs @@ -1,6 +1,7 @@ using System; using System.IO; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Deprecations; using StardewModdingAPI.Framework.Input; namespace StardewModdingAPI.Framework.ModHelpers @@ -9,6 +10,14 @@ namespace StardewModdingAPI.Framework.ModHelpers internal class ModHelper : BaseHelper, IModHelper, IDisposable { /********* + ** Fields + *********/ + /// <summary>The backing field for <see cref="Content"/>.</summary> + [Obsolete] + private readonly ContentHelper ContentImpl; + + + /********* ** Accessors *********/ /// <inheritdoc /> @@ -18,7 +27,27 @@ namespace StardewModdingAPI.Framework.ModHelpers public IModEvents Events { get; } /// <inheritdoc /> - public IContentHelper Content { get; } + [Obsolete] + public IContentHelper Content + { + get + { + SCore.DeprecationManager.Warn( + source: SCore.DeprecationManager.GetMod(this.ModID), + nounPhrase: $"{nameof(IModHelper)}.{nameof(IModHelper.Content)}", + version: "3.14.0", + severity: DeprecationLevel.Notice + ); + + return this.ContentImpl; + } + } + + /// <inheritdoc /> + public IGameContentHelper GameContent { get; } + + /// <inheritdoc /> + public IModContentHelper ModContent { get; } /// <inheritdoc /> public IContentPackHelper ContentPacks { get; } @@ -49,11 +78,13 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The mod's unique ID.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="modDirectory">The full path to the mod's folder.</param> /// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param> /// <param name="events">Manages access to events raised by SMAPI.</param> /// <param name="contentHelper">An API for loading content assets.</param> + /// <param name="gameContentHelper">An API for loading content assets from the game's <c>Content</c> folder or via <see cref="IModEvents.Content"/>.</param> + /// <param name="modContentHelper">An API for loading content assets from your mod's files.</param> /// <param name="contentPackHelper">An API for managing content packs.</param> /// <param name="commandHelper">An API for managing console commands.</param> /// <param name="dataHelper">An API for reading and writing persistent mod data.</param> @@ -63,8 +94,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - public ModHelper(string modID, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IContentHelper contentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper) - : base(modID) + public ModHelper( + IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events, +#pragma warning disable CS0612 // deprecated code + ContentHelper contentHelper, +#pragma warning restore CS0612 + IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper + ) + : base(mod) { // validate directory if (string.IsNullOrWhiteSpace(modDirectory)) @@ -74,10 +111,14 @@ namespace StardewModdingAPI.Framework.ModHelpers // initialize this.DirectoryPath = modDirectory; - this.Content = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); +#pragma warning disable CS0612 // deprecated code + this.ContentImpl = contentHelper ?? throw new ArgumentNullException(nameof(contentHelper)); +#pragma warning restore CS0612 + this.GameContent = gameContentHelper ?? throw new ArgumentNullException(nameof(gameContentHelper)); + this.ModContent = modContentHelper ?? throw new ArgumentNullException(nameof(modContentHelper)); this.ContentPacks = contentPackHelper ?? throw new ArgumentNullException(nameof(contentPackHelper)); this.Data = dataHelper ?? throw new ArgumentNullException(nameof(dataHelper)); - this.Input = new InputHelper(modID, currentInputState); + this.Input = new InputHelper(mod, currentInputState); this.ModRegistry = modRegistry ?? throw new ArgumentNullException(nameof(modRegistry)); this.ConsoleCommands = commandHelper ?? throw new ArgumentNullException(nameof(commandHelper)); this.Reflection = reflectionHelper ?? throw new ArgumentNullException(nameof(reflectionHelper)); @@ -86,6 +127,13 @@ namespace StardewModdingAPI.Framework.ModHelpers this.Events = events; } + /// <summary>Get the underlying instance for <see cref="IContentHelper"/>.</summary> + [Obsolete] + public ContentHelper GetLegacyContentHelper() + { + return this.ContentImpl; + } + /**** ** Mod config file ****/ diff --git a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs index ef1ad30c..348ba225 100644 --- a/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ModRegistryHelper.cs @@ -16,22 +16,22 @@ namespace StardewModdingAPI.Framework.ModHelpers private readonly IMonitor Monitor; /// <summary>The mod IDs for APIs accessed by this instanced.</summary> - private readonly HashSet<string> AccessedModApis = new HashSet<string>(); + private readonly HashSet<string> AccessedModApis = new(); /// <summary>Generates proxy classes to access mod APIs through an arbitrary interface.</summary> - private readonly InterfaceProxyFactory ProxyFactory; + private readonly IInterfaceProxyFactory ProxyFactory; /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="registry">The underlying mod registry.</param> /// <param name="proxyFactory">Generates proxy classes to access mod APIs through an arbitrary interface.</param> /// <param name="monitor">Encapsulates monitoring and logging for the mod.</param> - public ModRegistryHelper(string modID, ModRegistry registry, InterfaceProxyFactory proxyFactory, IMonitor monitor) - : base(modID) + public ModRegistryHelper(IModMetadata mod, ModRegistry registry, IInterfaceProxyFactory proxyFactory, IMonitor monitor) + : base(mod) { this.Registry = registry; this.ProxyFactory = proxyFactory; @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> - public IModInfo Get(string uniqueID) + public IModInfo? Get(string uniqueID) { return this.Registry.Get(uniqueID); } @@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> - public object GetApi(string uniqueID) + public object? GetApi(string uniqueID) { // validate ready if (!this.Registry.AreAllModsInitialized) @@ -67,17 +67,18 @@ namespace StardewModdingAPI.Framework.ModHelpers } // get raw API - IModMetadata mod = this.Registry.Get(uniqueID); + IModMetadata? mod = this.Registry.Get(uniqueID); if (mod?.Api != null && this.AccessedModApis.Add(mod.Manifest.UniqueID)) - this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}.", LogLevel.Trace); + this.Monitor.Log($"Accessed mod-provided API for {mod.DisplayName}."); return mod?.Api; } /// <inheritdoc /> - public TInterface GetApi<TInterface>(string uniqueID) where TInterface : class + public TInterface? GetApi<TInterface>(string uniqueID) + where TInterface : class { // get raw API - object api = this.GetApi(uniqueID); + object? api = this.GetApi(uniqueID); if (api == null) return null; @@ -94,9 +95,9 @@ namespace StardewModdingAPI.Framework.ModHelpers } // get API of type - if (api is TInterface castApi) - return castApi; - return this.ProxyFactory.CreateProxy<TInterface>(api, this.ModID, uniqueID); + return api is TInterface castApi + ? castApi + : this.ProxyFactory.CreateProxy<TInterface>(api, sourceModID: this.ModID, targetModID: uniqueID); } } } diff --git a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs index a7ce8692..6900a1d2 100644 --- a/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/MultiplayerHelper.cs @@ -18,10 +18,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="multiplayer">SMAPI's core multiplayer utility.</param> - public MultiplayerHelper(string modID, SMultiplayer multiplayer) - : base(modID) + public MultiplayerHelper(IModMetadata mod, SMultiplayer multiplayer) + : base(mod) { this.Multiplayer = multiplayer; } @@ -39,9 +39,9 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> - public IMultiplayerPeer GetConnectedPlayer(long id) + public IMultiplayerPeer? GetConnectedPlayer(long id) { - return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer peer) + return this.Multiplayer.Peers.TryGetValue(id, out MultiplayerPeer? peer) ? peer : null; } @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> - public void SendMessage<TMessage>(TMessage message, string messageType, string[] modIDs = null, long[] playerIDs = null) + public void SendMessage<TMessage>(TMessage message, string messageType, string[]? modIDs = null, long[]? playerIDs = null) { this.Multiplayer.BroadcastModMessage( message: message, diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 5a4ea742..a559906b 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -22,11 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="modName">The mod name for error messages.</param> /// <param name="reflector">The underlying reflection helper.</param> - public ReflectionHelper(string modID, string modName, Reflector reflector) - : base(modID) + public ReflectionHelper(IModMetadata mod, string modName, Reflector reflector) + : base(mod) { this.ModName = modName; this.Reflector = reflector; @@ -37,7 +37,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetField<TValue>(obj, name, required) - ); + )!; } /// <inheritdoc /> @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetField<TValue>(type, name, required) - ); + )!; } /// <inheritdoc /> @@ -53,7 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetProperty<TValue>(obj, name, required) - ); + )!; } /// <inheritdoc /> @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetProperty<TValue>(type, name, required) - ); + )!; } /// <inheritdoc /> @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetMethod(obj, name, required) - ); + )!; } /// <inheritdoc /> @@ -77,7 +77,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { return this.AssertAccessAllowed( this.Reflector.GetMethod(type, name, required) - ); + )!; } @@ -88,7 +88,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <typeparam name="T">The field value type.</typeparam> /// <param name="field">The field being accessed.</param> /// <returns>Returns the same field instance for convenience.</returns> - private IReflectedField<T> AssertAccessAllowed<T>(IReflectedField<T> field) + private IReflectedField<T>? AssertAccessAllowed<T>(IReflectedField<T>? field) { this.AssertAccessAllowed(field?.FieldInfo); return field; @@ -98,7 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <typeparam name="T">The property value type.</typeparam> /// <param name="property">The property being accessed.</param> /// <returns>Returns the same property instance for convenience.</returns> - private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property) + private IReflectedProperty<T>? AssertAccessAllowed<T>(IReflectedProperty<T>? property) { this.AssertAccessAllowed(property?.PropertyInfo.GetMethod?.GetBaseDefinition()); this.AssertAccessAllowed(property?.PropertyInfo.SetMethod?.GetBaseDefinition()); @@ -108,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Assert that mods can use the reflection helper to access the given member.</summary> /// <param name="method">The method being accessed.</param> /// <returns>Returns the same method instance for convenience.</returns> - private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) + private IReflectedMethod? AssertAccessAllowed(IReflectedMethod? method) { this.AssertAccessAllowed(method?.MethodInfo.GetBaseDefinition()); return method; @@ -116,18 +116,18 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <summary>Assert that mods can use the reflection helper to access the given member.</summary> /// <param name="member">The member being accessed.</param> - private void AssertAccessAllowed(MemberInfo member) + private void AssertAccessAllowed(MemberInfo? member) { if (member == null) return; // get type which defines the member - Type declaringType = member.DeclaringType; + Type? declaringType = member.DeclaringType; if (declaringType == null) throw new InvalidOperationException($"Can't validate access to {member.MemberType} {member.Name} because it has no declaring type."); // should never happen // validate access - string rootNamespace = typeof(Program).Namespace; + string? rootNamespace = typeof(Program).Namespace; if (declaringType.Namespace == rootNamespace || declaringType.Namespace?.StartsWith(rootNamespace + ".") == true) throw new InvalidOperationException($"SMAPI blocked access by {this.ModName} to its internals through the reflection API. Accessing the SMAPI internals is strongly discouraged since they're subject to change, which means the mod can break without warning. (Detected access to {declaringType.FullName}.{member.Name}.)"); } diff --git a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs index 869664fe..ae49d651 100644 --- a/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/TranslationHelper.cs @@ -27,11 +27,11 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modID">The unique ID of the relevant mod.</param> + /// <param name="mod">The mod using this instance.</param> /// <param name="locale">The initial locale.</param> /// <param name="languageCode">The game's current language code.</param> - public TranslationHelper(string modID, string locale, LocalizedContentManager.LanguageCode languageCode) - : base(modID) + public TranslationHelper(IModMetadata mod, string locale, LocalizedContentManager.LanguageCode languageCode) + : base(mod) { this.Translator = new Translator(); this.Translator.SetLocale(locale, languageCode); @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// <inheritdoc /> - public Translation Get(string key, object tokens) + public Translation Get(string key, object? tokens) { return this.Translator.Get(key, tokens); } @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Framework.ModHelpers return this; } - /// <summary>Set the current locale and precache translations.</summary> + /// <summary>Set the current locale and pre-cache translations.</summary> /// <param name="locale">The current locale.</param> /// <param name="localeEnum">The game's current language code.</param> internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) |