From a4713ea88238e6a6d62447aef97b35321e63c010 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 12 Jun 2017 18:44:36 -0400 Subject: add separate list of obsolete mods --- .../Framework/ModLoading/ModResolver.cs | 22 +++++++++++++++++----- .../Framework/Models/DisabledMod.cs | 22 ++++++++++++++++++++++ src/StardewModdingAPI/Framework/Models/SConfig.cs | 3 +++ 3 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/DisabledMod.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index f5139ce5..e8308f3e 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -17,10 +17,13 @@ namespace StardewModdingAPI.Framework.ModLoading /// The root path to search for mods. /// The JSON helper with which to read manifests. /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + /// Metadata about mods that SMAPI should consider obsolete and not load. /// Returns the manifests by relative folder. - public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords) + public IEnumerable ReadManifests(string rootPath, JsonHelper jsonHelper, IEnumerable compatibilityRecords, IEnumerable disabledMods) { compatibilityRecords = compatibilityRecords.ToArray(); + disabledMods = disabledMods.ToArray(); + foreach (DirectoryInfo modDir in this.GetModFolders(rootPath)) { // read file @@ -47,20 +50,29 @@ namespace StardewModdingAPI.Framework.ModLoading error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; } - // get compatibility record + // validate metadata ModCompatibility compatibility = null; if (manifest != null) { + // get unique key for lookups string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + + // check if mod should be disabled + DisabledMod disabledMod = disabledMods.FirstOrDefault(mod => mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase)); + if (disabledMod != null) + error = $"it's obsolete: {disabledMod.ReasonPhrase}"; + + // get compatibility record compatibility = ( from mod in compatibilityRecords where - mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) - && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) - && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) select mod ).FirstOrDefault(); } + // build metadata string displayName = !string.IsNullOrWhiteSpace(manifest?.Name) ? manifest.Name diff --git a/src/StardewModdingAPI/Framework/Models/DisabledMod.cs b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs new file mode 100644 index 00000000..170fa760 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/DisabledMod.cs @@ -0,0 +1,22 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata about for a mod that should never be loaded. + internal class DisabledMod + { + /********* + ** Accessors + *********/ + /**** + ** From config + ****/ + /// The unique mod IDs. + public string[] ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The reason phrase to show in the warning, or null to use the default value. + /// "this mod is no longer supported or used" + public string ReasonPhrase { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index c3f0816e..b2ca4113 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -17,5 +17,8 @@ /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. public ModCompatibility[] ModCompatibility { get; set; } + + /// A list of mods which should be considered obsolete and not loaded. + public DisabledMod[] DisabledMods { get; set; } } } -- cgit From 3c3953a7fdca6e79f50a4a5474be69ca6aab6446 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 18:18:04 -0400 Subject: add support for minimum dependency versions (#286) --- .../Framework/ModLoading/ModResolver.cs | 44 ++++++++++++++-------- .../Framework/Models/ManifestDependency.cs | 9 ++++- .../Serialisation/ManifestFieldConverter.cs | 3 +- 3 files changed, 38 insertions(+), 18 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index e8308f3e..dc140483 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -205,20 +205,40 @@ namespace StardewModdingAPI.Framework.ModLoading return states[mod] = ModDependencyStatus.Sorted; } + // get dependencies + var dependencies = + ( + from entry in mod.Manifest.Dependencies + let dependencyMod = mods.FirstOrDefault(m => string.Equals(m.Manifest?.UniqueID, entry.UniqueID, StringComparison.InvariantCultureIgnoreCase)) + orderby entry.UniqueID + select (ID: entry.UniqueID, MinVersion: entry.MinimumVersion, Mod: dependencyMod) + ) + .ToArray(); + // missing required dependencies, mark failed { - string[] missingModIDs = + string[] failedIDs = (from entry in dependencies where entry.Mod == null select entry.ID).ToArray(); + if (failedIDs.Any()) + { + sortedMods.Push(mod); + mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedIDs)})."); + return states[mod] = ModDependencyStatus.Failed; + } + } + + // dependency min version not met, mark failed + { + string[] failedLabels = ( - from dependency in mod.Manifest.Dependencies - where mods.All(m => m.Manifest?.UniqueID != dependency.UniqueID) - orderby dependency.UniqueID - select dependency.UniqueID + from entry in dependencies + where entry.MinVersion != null && entry.MinVersion.IsNewerThan(entry.Mod.Manifest.Version) + select $"{entry.Mod.DisplayName} (needs {entry.MinVersion} or later)" ) .ToArray(); - if (missingModIDs.Any()) + if (failedLabels.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", missingModIDs)})."); + mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return states[mod] = ModDependencyStatus.Failed; } } @@ -227,16 +247,8 @@ namespace StardewModdingAPI.Framework.ModLoading { states[mod] = ModDependencyStatus.Checking; - // get mods to load first - IModMetadata[] modsToLoadFirst = - ( - from other in mods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest?.UniqueID) - select other - ) - .ToArray(); - // recursively sort dependencies + IModMetadata[] modsToLoadFirst = dependencies.Select(p => p.Mod).ToArray(); foreach (IModMetadata requiredMod in modsToLoadFirst) { var subchain = new List(currentChain) { mod }; diff --git a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs index 2f580c1d..a0ff0c90 100644 --- a/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs +++ b/src/StardewModdingAPI/Framework/Models/ManifestDependency.cs @@ -9,15 +9,22 @@ /// The unique mod ID to require. public string UniqueID { get; set; } + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + /********* ** Public methods *********/ /// Construct an instance. /// The unique mod ID to require. - public ManifestDependency(string uniqueID) + /// The minimum required version (if any). + public ManifestDependency(string uniqueID, string minimumVersion) { this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 6b5a6aaa..7acb5fd0 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -51,7 +51,8 @@ namespace StardewModdingAPI.Framework.Serialisation foreach (JObject obj in JArray.Load(reader).Children()) { string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - result.Add(new ManifestDependency(uniqueID)); + string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); + result.Add(new ManifestDependency(uniqueID, minVersion)); } return result.ToArray(); } -- cgit From b46776a4fbabe765b81751f8c4984cdd8a207419 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:08:56 -0400 Subject: enable string versions in manifest.json (#308) --- .../Serialisation/ManifestFieldConverter.cs | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7acb5fd0..7a59f134 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -36,12 +36,25 @@ namespace StardewModdingAPI.Framework.Serialisation // semantic version if (objectType == typeof(ISemanticVersion)) { - JObject obj = JObject.Load(reader); - int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); - string build = obj.Value(nameof(ISemanticVersion.Build)); - return new SemanticVersion(major, minor, patch, build); + JToken token = JToken.Load(reader); + switch (token.Type) + { + case JTokenType.Object: + { + JObject obj = (JObject)token; + int major = obj.Value(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.Value(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.Value(nameof(ISemanticVersion.PatchVersion)); + string build = obj.Value(nameof(ISemanticVersion.Build)); + return new SemanticVersion(major, minor, patch, build); + } + + case JTokenType.String: + return new SemanticVersion(token.Value()); + + default: + throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + } } // manifest dependency -- cgit From fb8fefea00aacd603e68fbdbaecd27e4c451cc82 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 18 Jun 2017 22:11:48 -0400 Subject: show friendly error when parsing a manifest version fails (#308) --- .../Framework/Exceptions/SParseException.cs | 17 +++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 5 +++++ .../Framework/Serialisation/ManifestFieldConverter.cs | 12 ++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SParseException.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs new file mode 100644 index 00000000..f7133ee7 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index dc140483..045b175c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.Serialisation; @@ -45,6 +46,10 @@ namespace StardewModdingAPI.Framework.ModLoading else if (string.IsNullOrWhiteSpace(manifest.EntryDll)) error = "its manifest doesn't set an entry DLL."; } + catch (SParseException ex) + { + error = $"parsing its manifest failed: {ex.Message}"; + } catch (Exception ex) { error = $"parsing its manifest failed:\n{ex.GetLogSummary()}"; diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs index 7a59f134..e6d62d50 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework.Serialisation @@ -50,10 +51,17 @@ namespace StardewModdingAPI.Framework.Serialisation } case JTokenType.String: - return new SemanticVersion(token.Value()); + { + string str = token.Value(); + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, out ISemanticVersion version)) + throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta."); + return version; + } default: - throw new FormatException($"Can't parse {token.Type} token as a semantic version, must be an object or string."); + throw new SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); } } -- cgit From 8d7b5b372657c0f96196cb2a902b2bdcce184fe4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Jun 2017 11:01:47 -0400 Subject: improve logging when SMAPI loads mods --- .../Framework/ModLoading/AssemblyLoader.cs | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 42bd7bfb..406d49e1 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -71,13 +71,15 @@ namespace StardewModdingAPI.Framework.ModLoading } // rewrite & load assemblies in leaf-to-root order + bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { - bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { - this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}.dll (rewritten in memory)...", LogLevel.Trace); using (MemoryStream outStream = new MemoryStream()) { assembly.Definition.Write(outStream); @@ -87,7 +89,8 @@ namespace StardewModdingAPI.Framework.ModLoading } else { - this.Monitor.Log($"Loading {assembly.File.Name}...", LogLevel.Trace); + if (!oneAssembly) + this.Monitor.Log($" Loading {assembly.File.Name}.dll...", LogLevel.Trace); lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } } @@ -161,12 +164,14 @@ namespace StardewModdingAPI.Framework.ModLoading /// Rewrite the types referenced by an assembly. /// The assembly to rewrite. /// Assume the mod is compatible, even if incompatible code is detected. + /// A string to prefix to log messages. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) + private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible, string logPrefix) { ModuleDefinition module = assembly.MainModule; HashSet loggedMessages = new HashSet(); + string filename = $"{assembly.Name.Name}.dll"; // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; @@ -175,7 +180,7 @@ namespace StardewModdingAPI.Framework.ModLoading // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewriting {filename} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -205,15 +210,15 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, method, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } catch (IncompatibleInstructionException) { if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } @@ -227,15 +232,15 @@ namespace StardewModdingAPI.Framework.ModLoading { if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) { - this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Rewrote {filename} to fix {rewriter.NounPhrase}..."); anyRewritten = true; } } catch (IncompatibleInstructionException) { if (!assumeCompatible) - throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}."); + this.LogOnce(this.Monitor, loggedMessages, $"{logPrefix}Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {filename}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); } } } -- cgit From 6073d24cabe3fa93ddbba7e4a613e7342a8b20c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 26 Jun 2017 11:08:45 -0400 Subject: change manifest.MinimumApiVersion to ISemanticVersion --- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 14 +++----------- src/StardewModdingAPI/Framework/Models/Manifest.cs | 3 ++- 2 files changed, 5 insertions(+), 12 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 045b175c..cefc860b 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -122,18 +122,10 @@ namespace StardewModdingAPI.Framework.ModLoading } // validate SMAPI version - if (!string.IsNullOrWhiteSpace(mod.Manifest.MinimumApiVersion)) + if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { - if (!SemanticVersion.TryParse(mod.Manifest.MinimumApiVersion, out ISemanticVersion minVersion)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it has an invalid minimum SMAPI version '{mod.Manifest.MinimumApiVersion}'. This should be a semantic version number like {apiVersion}."); - continue; - } - if (minVersion.IsNewerThan(apiVersion)) - { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod."); - continue; - } + mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + continue; } // validate DLL path diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index be781585..8e5d13f8 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -25,7 +25,8 @@ namespace StardewModdingAPI.Framework.Models public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - public string MinimumApiVersion { get; set; } + [JsonConverter(typeof(ManifestFieldConverter))] + public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } -- cgit 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) --- .../Framework/Content/AssetData.cs | 44 ++++++++ .../Framework/Content/AssetDataForDictionary.cs | 45 +++++++++ .../Framework/Content/AssetDataForImage.cs | 70 +++++++++++++ .../Framework/Content/AssetDataForObject.cs | 47 +++++++++ .../Framework/Content/AssetInfo.cs | 82 +++++++++++++++ .../Framework/Content/ContentEventData.cs | 111 --------------------- .../Framework/Content/ContentEventHelper.cs | 47 --------- .../Content/ContentEventHelperForDictionary.cs | 45 --------- .../Content/ContentEventHelperForImage.cs | 70 ------------- src/StardewModdingAPI/Framework/ContentHelper.cs | 8 ++ src/StardewModdingAPI/Framework/SContentManager.cs | 64 ++++++++---- src/StardewModdingAPI/Framework/SGame.cs | 10 +- 12 files changed, 349 insertions(+), 294 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Content/AssetData.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs create mode 100644 src/StardewModdingAPI/Framework/Content/AssetInfo.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventData.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs delete mode 100644 src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Content/AssetData.cs b/src/StardewModdingAPI/Framework/Content/AssetData.cs new file mode 100644 index 00000000..1ab9eebd --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetData.cs @@ -0,0 +1,44 @@ +using System; + +namespace StardewModdingAPI.Framework.Content +{ + /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. + /// The interface value type. + internal class AssetData : AssetInfo, IAssetData + { + /********* + ** Accessors + *********/ + /// The content data being read. + public TValue Data { get; protected set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetData(string locale, string assetName, TValue data, Func getNormalisedPath) + : base(locale, assetName, data.GetType(), getNormalisedPath) + { + this.Data = data; + } + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + public void ReplaceWith(TValue value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); + if (!this.DataType.IsInstanceOfType(value)) + throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); + + this.Data = value; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs new file mode 100644 index 00000000..e9b29b12 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForDictionary.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForDictionary : AssetData>, IAssetDataForDictionary + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// The entry value. + public void Set(TKey key, TValue value) + { + this.Data[key] = value; + } + + /// Add or replace an entry in the dictionary. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + public void Set(TKey key, Func value) + { + this.Data[key] = value(this.Data[key]); + } + + /// Dynamically replace values in the dictionary. + /// A lambda which takes the current key and value for an entry, and returns the new value. + public void Set(Func replacer) + { + foreach (var pair in this.Data.ToArray()) + this.Data[pair.Key] = replacer(pair.Key, pair.Value); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs new file mode 100644 index 00000000..45c5588b --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForImage.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to dictionary content being read from a data file. + internal class AssetDataForImage : AssetData, IAssetDataForImage + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Overwrite part of the image. + /// The image to patch into the content. + /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. + /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. + /// Indicates how an image should be patched. + /// One of the arguments is null. + /// The is outside the bounds of the spritesheet. + public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) + { + // get texture + Texture2D target = this.Data; + + // get areas + sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); + targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); + + // validate + if (source == null) + throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); + if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) + throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); + if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) + throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); + if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + throw new InvalidOperationException("The source and target areas must be the same size."); + + // get source data + int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; + Color[] sourceData = new Color[pixelCount]; + source.GetData(0, sourceArea, sourceData, 0, pixelCount); + + // merge data in overlay mode + if (patchMode == PatchMode.Overlay) + { + Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; + target.GetData(0, targetArea, newData, 0, newData.Length); + for (int i = 0; i < sourceData.Length; i++) + { + Color pixel = sourceData[i]; + if (pixel.A != 0) // not transparent + newData[i] = pixel; + } + sourceData = newData; + } + + // patch target texture + target.SetData(0, targetArea, sourceData, 0, pixelCount); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs new file mode 100644 index 00000000..af2f54ae --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetDataForObject.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to content being read from a data file. + internal class AssetDataForObject : AssetData, IAssetData + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public AssetDataForObject(string locale, string assetName, object data, Func getNormalisedPath) + : base(locale, assetName, data, getNormalisedPath) { } + + /// Get a helper to manipulate the data as a dictionary. + /// The expected dictionary key. + /// The expected dictionary balue. + /// The content being read isn't a dictionary. + public IAssetDataForDictionary AsDictionary() + { + return new AssetDataForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); + } + + /// Get a helper to manipulate the data as an image. + /// The content being read isn't an image. + public IAssetDataForImage AsImage() + { + return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); + } + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + public TData GetData() + { + if (!(this.Data is TData)) + throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); + return (TData)this.Data; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/AssetInfo.cs b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs new file mode 100644 index 00000000..08bc3a03 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Content/AssetInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.Content +{ + internal class AssetInfo : IAssetInfo + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + + /********* + ** Accessors + *********/ + /// The content's locale code, if the content is localised. + public string Locale { get; } + + /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. + public string AssetName { get; } + + /// The content data type. + public Type DataType { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localised. + /// The normalised asset name being read. + /// The content type being read. + /// Normalises an asset key to match the cache key. + public AssetInfo(string locale, string assetName, Type type, Func getNormalisedPath) + { + this.Locale = locale; + this.AssetName = assetName; + this.DataType = type; + this.GetNormalisedPath = getNormalisedPath; + } + + /// Get whether the asset name being loaded matches a given name after normalisation. + /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). + public bool IsAssetName(string path) + { + path = this.GetNormalisedPath(path); + return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); + } + + + /********* + ** Protected methods + *********/ + /// Get a human-readable type name. + /// The type to name. + protected string GetFriendlyTypeName(Type type) + { + // dictionary + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type[] genericArgs = type.GetGenericArguments(); + return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; + } + + // texture + if (type == typeof(Texture2D)) + return type.Name; + + // native type + if (type == typeof(int)) + return "int"; + if (type == typeof(string)) + return "string"; + + // default + return type.FullName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventData.cs b/src/StardewModdingAPI/Framework/Content/ContentEventData.cs deleted file mode 100644 index 1a1779d4..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventData.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Base implementation for a content helper which encapsulates access and changes to content being read from a data file. - /// The interface value type. - internal class ContentEventData : EventArgs, IContentEventData - { - /********* - ** Properties - *********/ - /// Normalises an asset key to match the cache key. - protected readonly Func GetNormalisedPath; - - - /********* - ** Accessors - *********/ - /// The content's locale code, if the content is localised. - public string Locale { get; } - - /// The normalised asset name being read. The format may change between platforms; see to compare with a known path. - public string AssetName { get; } - - /// The content data being read. - public TValue Data { get; protected set; } - - /// The content data type. - public Type DataType { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventData(string locale, string assetName, TValue data, Func getNormalisedPath) - : this(locale, assetName, data, data.GetType(), getNormalisedPath) { } - - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// The content data type being read. - /// Normalises an asset key to match the cache key. - public ContentEventData(string locale, string assetName, TValue data, Type dataType, Func getNormalisedPath) - { - this.Locale = locale; - this.AssetName = assetName; - this.Data = data; - this.DataType = dataType; - this.GetNormalisedPath = getNormalisedPath; - } - - /// Get whether the asset name being loaded matches a given name after normalisation. - /// The expected asset path, relative to the game's content folder and without the .xnb extension or locale suffix (like 'Data\ObjectInformation'). - public bool IsAssetName(string path) - { - path = this.GetNormalisedPath(path); - return this.AssetName.Equals(path, StringComparison.InvariantCultureIgnoreCase); - } - - /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. - /// The new content value. - /// The is null. - /// The 's type is not compatible with the loaded asset's type. - public void ReplaceWith(TValue value) - { - if (value == null) - throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); - if (!this.DataType.IsInstanceOfType(value)) - throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.DataType)} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); - - this.Data = value; - } - - - /********* - ** Protected methods - *********/ - /// Get a human-readable type name. - /// The type to name. - protected string GetFriendlyTypeName(Type type) - { - // dictionary - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) - { - Type[] genericArgs = type.GetGenericArguments(); - return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; - } - - // texture - if (type == typeof(Texture2D)) - return type.Name; - - // native type - if (type == typeof(int)) - return "int"; - if (type == typeof(string)) - return "string"; - - // default - return type.FullName; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs deleted file mode 100644 index 9bf1ea17..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelper.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to content being read from a data file. - internal class ContentEventHelper : ContentEventData, IContentEventHelper - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelper(string locale, string assetName, object data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Get a helper to manipulate the data as a dictionary. - /// The expected dictionary key. - /// The expected dictionary balue. - /// The content being read isn't a dictionary. - public IContentEventHelperForDictionary AsDictionary() - { - return new ContentEventHelperForDictionary(this.Locale, this.AssetName, this.GetData>(), this.GetNormalisedPath); - } - - /// Get a helper to manipulate the data as an image. - /// The content being read isn't an image. - public IContentEventHelperForImage AsImage() - { - return new ContentEventHelperForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalisedPath); - } - - /// Get the data as a given type. - /// The expected data type. - /// The data can't be converted to . - public TData GetData() - { - if (!(this.Data is TData)) - throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); - return (TData)this.Data; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs deleted file mode 100644 index 26f059e4..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForDictionary.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class ContentEventHelperForDictionary : ContentEventData>, IContentEventHelperForDictionary - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelperForDictionary(string locale, string assetName, IDictionary data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// The entry value. - public void Set(TKey key, TValue value) - { - this.Data[key] = value; - } - - /// Add or replace an entry in the dictionary. - /// The entry key. - /// A callback which accepts the current value and returns the new value. - public void Set(TKey key, Func value) - { - this.Data[key] = value(this.Data[key]); - } - - /// Dynamically replace values in the dictionary. - /// A lambda which takes the current key and value for an entry, and returns the new value. - public void Set(Func replacer) - { - foreach (var pair in this.Data.ToArray()) - this.Data[pair.Key] = replacer(pair.Key, pair.Value); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs b/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs deleted file mode 100644 index da30590b..00000000 --- a/src/StardewModdingAPI/Framework/Content/ContentEventHelperForImage.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.Content -{ - /// Encapsulates access and changes to dictionary content being read from a data file. - internal class ContentEventHelperForImage : ContentEventData, IContentEventHelperForImage - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The content's locale code, if the content is localised. - /// The normalised asset name being read. - /// The content data being read. - /// Normalises an asset key to match the cache key. - public ContentEventHelperForImage(string locale, string assetName, Texture2D data, Func getNormalisedPath) - : base(locale, assetName, data, getNormalisedPath) { } - - /// Overwrite part of the image. - /// The image to patch into the content. - /// The part of the to copy (or null to take the whole texture). This must be within the bounds of the texture. - /// The part of the content to patch (or null to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet. - /// Indicates how an image should be patched. - /// One of the arguments is null. - /// The is outside the bounds of the spritesheet. - public void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace) - { - // get texture - Texture2D target = this.Data; - - // get areas - sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); - targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); - - // validate - if (source == null) - throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); - if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) - throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); - if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) - throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); - if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) - throw new InvalidOperationException("The source and target areas must be the same size."); - - // get source data - int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; - Color[] sourceData = new Color[pixelCount]; - source.GetData(0, sourceArea, sourceData, 0, pixelCount); - - // merge data in overlay mode - if (patchMode == PatchMode.Overlay) - { - Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; - target.GetData(0, targetArea, newData, 0, newData.Length); - for (int i = 0; i < sourceData.Length; i++) - { - Color pixel = sourceData[i]; - if (pixel.A != 0) // not transparent - newData[i] = pixel; - } - sourceData = newData; - } - - // patch target texture - target.SetData(0, targetArea, sourceData, 0, pixelCount); - } - } -} diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 7fd5e803..f4b541e9 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -32,6 +33,13 @@ namespace StardewModdingAPI.Framework private readonly string ModName; + /********* + ** Accessors + *********/ + /// Editors which change content assets after they're loaded. + internal IList AssetEditors { get; } = new List(); + + /********* ** Public methods *********/ 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. - ///