From ae01396d9db77789937ff26cfc1cdf90feea68b2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Jul 2017 17:26:36 -0400 Subject: fix crash in unique-ID check when mod has no manifest (#323) --- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index b75453b7..16c397be 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Framework.ModLoading #if !SMAPI_1_x { var duplicatesByID = mods - .GroupBy(mod => mod.Manifest.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) + .GroupBy(mod => mod.Manifest?.UniqueID?.Trim(), mod => mod, StringComparer.InvariantCultureIgnoreCase) .Where(p => p.Count() > 1); foreach (var group in duplicatesByID) { -- cgit From 48ced0336c5e0f41efd2ecf3b5a03f11596d79d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Jul 2017 18:30:46 -0400 Subject: use more readable colours if player has a light-backgrounded terminal (#327) --- release-notes.md | 1 + src/StardewModdingAPI/Framework/Monitor.cs | 61 +++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 9 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 924ecb90..6e6d87ab 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,6 +4,7 @@ For players: * The SMAPI console is now much simpler and easier-to-read. +* The SMAPI console now uses more readable colors in terminals with a light background. For mod developers: * Added API to edit XNB images & data loaded by the game (see [API reference](http://stardewvalleywiki.com/Modding:SMAPI_APIs#Content)). diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 5ae24a73..c2c3a689 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -25,15 +25,7 @@ namespace StardewModdingAPI.Framework private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); /// The console text color for each log level. - private static readonly Dictionary Colors = new Dictionary - { - [LogLevel.Trace] = ConsoleColor.DarkGray, - [LogLevel.Debug] = ConsoleColor.DarkGray, - [LogLevel.Info] = ConsoleColor.White, - [LogLevel.Warn] = ConsoleColor.Yellow, - [LogLevel.Error] = ConsoleColor.Red, - [LogLevel.Alert] = ConsoleColor.Magenta - }; + private static readonly IDictionary Colors = Monitor.GetConsoleColorScheme(); /// Propagates notification that SMAPI should exit. private readonly CancellationTokenSource ExitTokenSource; @@ -172,5 +164,56 @@ namespace StardewModdingAPI.Framework if (this.WriteToFile) this.LogFile.WriteLine(fullMessage); } + + /// Get the color scheme to use for the current console. + private static IDictionary GetConsoleColorScheme() + { +#if !SMAPI_1_x + // scheme for dark console background + if (Monitor.IsDark(Console.BackgroundColor)) + { +#endif + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.White, + [LogLevel.Warn] = ConsoleColor.Yellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.Magenta + }; +#if !SMAPI_1_x + } + + // scheme for light console background + return new Dictionary + { + [LogLevel.Trace] = ConsoleColor.DarkGray, + [LogLevel.Debug] = ConsoleColor.DarkGray, + [LogLevel.Info] = ConsoleColor.Black, + [LogLevel.Warn] = ConsoleColor.DarkYellow, + [LogLevel.Error] = ConsoleColor.Red, + [LogLevel.Alert] = ConsoleColor.DarkMagenta + }; +#endif + } + + /// Get whether a console color should be considered dark, which is subjectively defined as 'white looks better than black on this text'. + /// The color to check. + private static bool IsDark(ConsoleColor color) + { + switch (color) + { + case ConsoleColor.Black: + case ConsoleColor.Blue: + case ConsoleColor.DarkBlue: + case ConsoleColor.DarkRed: + case ConsoleColor.Red: + return true; + + default: + return false; + } + } } } -- cgit From d0e0e9427e9d407e1613a1cb9265beed103bd80f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jul 2017 00:03:41 -0400 Subject: rename ManifestFieldConverter for broader usage --- src/StardewModdingAPI/Framework/Models/Manifest.cs | 6 +- .../Serialisation/ManifestFieldConverter.cs | 99 ---------------------- .../Framework/Serialisation/SFieldConverter.cs | 99 ++++++++++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 4 files changed, 103 insertions(+), 103 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs create mode 100644 src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 1b5c2646..29c3517e 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -21,18 +21,18 @@ namespace StardewModdingAPI.Framework.Models public string Author { get; set; } /// The mod version. - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion Version { get; set; } /// The minimum SMAPI version required by this mod, if any. - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public ISemanticVersion MinimumApiVersion { get; set; } /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(ManifestFieldConverter))] + [JsonConverter(typeof(SFieldConverter))] public IManifestDependency[] Dependencies { get; set; } /// The unique mod ID. diff --git a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs deleted file mode 100644 index 6947311b..00000000 --- a/src/StardewModdingAPI/Framework/Serialisation/ManifestFieldConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Framework.Exceptions; -using StardewModdingAPI.Framework.Models; - -namespace StardewModdingAPI.Framework.Serialisation -{ - /// Overrides how SMAPI reads and writes and fields. - internal class ManifestFieldConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - // semantic version - if (objectType == typeof(ISemanticVersion)) - { - 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: - { - 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 SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); - } - } - - // manifest dependency - if (objectType == typeof(IManifestDependency[])) - { - List result = new List(); - foreach (JObject obj in JArray.Load(reader).Children()) - { - string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); - string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); -#if SMAPI_1_x - result.Add(new ManifestDependency(uniqueID, minVersion)); -#else - bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); -#endif - } - return result.ToArray(); - } - - // unknown - throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs new file mode 100644 index 00000000..9dc62b6a --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.Models; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Overrides how SMAPI reads and writes and fields. + internal class SFieldConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // semantic version + if (objectType == typeof(ISemanticVersion)) + { + 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: + { + 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 SParseException($"Can't parse semantic version from {token.Type}, must be an object or string."); + } + } + + // manifest dependency + if (objectType == typeof(IManifestDependency[])) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.Value(nameof(IManifestDependency.UniqueID)); + string minVersion = obj.Value(nameof(IManifestDependency.MinimumVersion)); +#if SMAPI_1_x + result.Add(new ManifestDependency(uniqueID, minVersion)); +#else + bool required = obj.Value(nameof(IManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); +#endif + } + return result.ToArray(); + } + + // unknown + throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index f778660d..9047aea4 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -159,7 +159,7 @@ - + -- cgit From 6ddcef61e94a81711ad25a378f0fa03d7799f2dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jul 2017 00:05:39 -0400 Subject: simplify mod compatibility model parsing --- .../Framework/ModLoading/ModResolver.cs | 6 ++-- .../Framework/Models/ModCompatibility.cs | 35 ++++------------------ 2 files changed, 9 insertions(+), 32 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 16c397be..cf43eb45 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -72,8 +72,8 @@ namespace StardewModdingAPI.Framework.ModLoading 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.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) + && !manifest.Version.IsNewerThan(mod.UpperVersion) select mod ).FirstOrDefault(); } @@ -113,7 +113,7 @@ namespace StardewModdingAPI.Framework.ModLoading bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; - string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion} here:"; + string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; if (hasOfficialUrl) error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; if (hasUnofficialUrl) diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index 90cbd237..eb312eff 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -1,5 +1,5 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; +using Newtonsoft.Json; +using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework.Models { @@ -19,10 +19,12 @@ namespace StardewModdingAPI.Framework.Models public string Name { get; set; } /// The oldest incompatible mod version, or null for all past versions. - public string LowerVersion { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion LowerVersion { get; set; } /// The most recent incompatible mod version. - public string UpperVersion { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ISemanticVersion UpperVersion { get; set; } /// A label to show to the user instead of , when the manifest version differs from the user-facing version. public string UpperVersionLabel { get; set; } @@ -39,30 +41,5 @@ namespace StardewModdingAPI.Framework.Models /// Indicates how SMAPI should consider the mod. public ModCompatibilityType Compatibility { get; set; } - - - /**** - ** Injected - ****/ - /// The semantic version corresponding to . - [JsonIgnore] - public ISemanticVersion LowerSemanticVersion { get; set; } - - /// The semantic version corresponding to . - [JsonIgnore] - public ISemanticVersion UpperSemanticVersion { get; set; } - - - /********* - ** Private methods - *********/ - /// The method called when the model finishes deserialising. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; - this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null; - } } } -- cgit From 7d73b0bf0c545b281d9d84bc73ad40932764b483 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jul 2017 00:37:58 -0400 Subject: simplify compatibility skip message in 2.0 & combine update URL fields --- .../Framework/ModLoading/ModResolver.cs | 18 +++- .../Framework/Models/ModCompatibility.cs | 7 +- .../StardewModdingAPI.config.json | 104 +++++++++------------ 3 files changed, 62 insertions(+), 67 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index cf43eb45..fdcbdaa7 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -109,15 +109,25 @@ namespace StardewModdingAPI.Framework.ModLoading ModCompatibility compatibility = mod.Compatibility; if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(mod.Compatibility.UnofficialUpdateUrl); +#if SMAPI_1_x + bool hasOfficialUrl = mod.Compatibility.UpdateUrls.Length > 0; + bool hasUnofficialUrl = mod.Compatibility.UpdateUrls.Length > 1; string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game or SMAPI"; string error = $"{reasonPhrase}. Please check for a version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()} here:"; if (hasOfficialUrl) - error += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + error += !hasUnofficialUrl ? $" {compatibility.UpdateUrls[0]}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrls[0]}"; if (hasUnofficialUrl) - error += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + error += $"{Environment.NewLine}- unofficial update: {compatibility.UpdateUrls[1]}"; +#else + string reasonPhrase = compatibility.ReasonPhrase ?? "it's no longer compatible"; + string error = $"{reasonPhrase}. Please check for a "; + if (mod.Manifest.Version.Equals(compatibility.UpperVersion) && compatibility.UpperVersionLabel == null) + error += "newer version"; + else + error += $"version newer than {compatibility.UpperVersionLabel ?? compatibility.UpperVersion.ToString()}"; + error += " at " + string.Join(" or ", compatibility.UpdateUrls); +#endif mod.SetStatus(ModMetadataStatus.Failed, error); continue; diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index eb312eff..72b6f2af 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -29,11 +29,8 @@ namespace StardewModdingAPI.Framework.Models /// A label to show to the user instead of , when the manifest version differs from the user-facing version. public string UpperVersionLabel { get; set; } - /// The URL the user can check for an official updated version. - public string UpdateUrl { get; set; } - - /// The URL the user can check for an unofficial updated version. - public string UnofficialUpdateUrl { get; set; } + /// The URLs the user can check for a newer version. + public string[] UpdateUrls { get; set; } /// The reason phrase to show in the warning, or null to use the default value. /// "this version is incompatible with the latest version of the game" diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 4e871636..90bf29c9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -54,8 +54,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "AccessChestAnywhere" ], "UpperVersion": "1.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Needs update for SDV 1.1." }, { @@ -63,7 +62,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "AlmightyTool.dll" ], "UpperVersion": "1.1.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], "Notes": "Needs update for SDV 1.2." }, { @@ -71,8 +70,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], "UpperVersion": "2.3.1-pathoschild-update", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." }, { @@ -80,8 +78,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], "UpperVersion": "1.2.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/276", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -89,8 +86,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SPDChestLabel" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.1." }, { @@ -98,7 +94,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "ChestPooling.dll" ], "UpperVersion": "1.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111988", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Needs update for SDV 1.2." }, { @@ -106,7 +102,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], "UpperVersion": "1.9-beta", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Needs update for SDV 1.2." }, { @@ -114,22 +110,21 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "CJBAutomation" ], "UpperVersion": "1.4", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1063", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], "Notes": "Needs update for SDV 1.2." }, { "Name": "CJB Cheats Menu", "ID": [ "CJBCheatsMenu" ], "UpperVersion": "1.12", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/4" ], "Notes": "Needs update for SDV 1.1." }, { "Name": "CJB Item Spawner", "ID": [ "CJBItemSpawner" ], "UpperVersion": "1.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Needs update for SDV 1.1." }, { @@ -137,7 +132,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "CJBShowItemSellPrice" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Needs update for SDV 1.2." }, { @@ -145,7 +140,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "CookingSkill" ], "UpperVersion": "1.0.3", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], "Notes": "Needs update for SDV 1.2." }, { @@ -153,8 +148,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], "UpperVersion": "1.0.4", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/569", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -162,8 +156,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SPDHealthBar" ], "UpperVersion": "1.7", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", - "UnofficialUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -171,7 +164,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], "UpperVersion": "1.7.5", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4228", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], "Notes": "Needs update for SDV 1.2." }, { @@ -180,7 +173,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpperVersion": "1.0", "UpperVersionLabel": "0.94", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485" ], "Notes": "Needs update for SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." }, { @@ -188,7 +181,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "FarmAutomation.ItemCollector.dll", /*since 0.4*/ "Maddy99.FarmAutomation.ItemCollector" ], "UpperVersion": "0.4", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/125172", + "UpdateUrls": [ "http://community.playstarbound.com/threads/125172" ], "Notes": "Needs update for SDV 1.2." }, { @@ -196,8 +189,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "InstantGeode" ], "UpperVersion": "1.12", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/109038", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -205,7 +197,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], "UpperVersion": "1.0.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111988", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Needs update for SDV 1.2." }, { @@ -213,7 +205,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], "UpperVersion": "3.3", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], "Notes": "Needs update for SDV 1.2." }, { @@ -221,7 +213,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], "UpperVersion": "2.3.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/229", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], "Notes": "Needs update for SDV 1.2." }, { @@ -229,7 +221,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], "UpperVersion": "1.10.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], "Notes": "Needs update for SDV 1.2." }, { @@ -237,7 +229,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], "Compatibility": "AssumeBroken", "UpperVersion": "0.3.3", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501" ], "Notes": "Needs update for SDV 1.2." }, { @@ -245,8 +237,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], "UpperVersion": "0.5", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, { @@ -255,7 +246,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "LowerVersion": "1.42", "UpperVersion": "1.43", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], "ReasonPhrase": "These versions have an update check error which crash the game." }, { @@ -263,7 +254,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SB_PotC" ], "UpperVersion": "1.0.8", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/923", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], "ReasonPhrase": "Needs update for SDV 1.2." }, { @@ -271,7 +262,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "PointAndPlant.dll" ], "UpperVersion": "1.0.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572" ], "Notes": "Needs update for SDV 1.2." }, { @@ -279,8 +270,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "PrairieKingMadeEasy.dll" ], "UpperVersion": "1.0.0", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/3594", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -288,7 +278,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], "UpperVersion": "1.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/605", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], "Notes": "Needs update for SDV 1.2." }, { @@ -296,7 +286,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SaveAnywhere" ], "UpperVersion": "2.3", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444" ], "Notes": "Needs update for SDV 1.2." }, { @@ -304,7 +294,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SimpleSprinkler.dll" ], "UpperVersion": "1.4", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/76", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76" ], "Notes": "Needs update for SDV 1.2." }, { @@ -312,8 +302,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.0", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/3531", - "UnofficialUpdateUrl": "http://community.playstarbound.com/resources/4201", + "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://community.playstarbound.com/resources/4201" ], "Notes": "Needs update for SDV 1.2." }, { @@ -321,7 +310,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4201", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], "Notes": "Needs update for SDV 1.2." }, { @@ -329,7 +318,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "StackSplitX.dll" ], "UpperVersion": "1.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], "Notes": "Needs update for SDV 1.2." }, { @@ -337,7 +326,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "Teleporter" ], "UpperVersion": "1.0.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4374", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4374" ], "Notes": "Needs update for SDV 1.2." }, { @@ -345,7 +334,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "4108e859-333c-4fec-a1a7-d2e18c1019fe", /*since 2.1*/ "community.TimeSpeed" ], "UpperVersion": "2.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/169", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." }, { @@ -354,7 +343,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpperVersion": "0.5", "UpperVersionLabel": "1.0", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/1023", + "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023" ], "Notes": "Needs update for SDV 1.2. Actual upper version is 1.0, but mod incorrectly sets it to 0.5 in the manifest." }, { @@ -362,8 +351,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "WeatherController.dll" ], "UpperVersion": "1.0.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/111526", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", + "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, { @@ -371,7 +359,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "zdailyincrease" ], "UpperVersion": "1.2", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4247", + "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], "Notes": "Needs update for SDV 1.2." }, { @@ -379,7 +367,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "ZoomMod" ], "UpperVersion": "0.1", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/threads/115028", + "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], "Notes": "Needs update for SDV 1.2." }, { @@ -387,7 +375,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, { @@ -395,7 +383,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, { @@ -403,7 +391,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, { @@ -411,7 +399,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], "UpperVersion": "1.7", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, { @@ -419,7 +407,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, { @@ -427,7 +415,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "https://github.com/Zoryn4163/SMAPI-Mods/releases", + "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." } ] -- cgit From 7c1ac555a401819bf01ebe0e515b02416bc49b15 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Jul 2017 00:47:28 -0400 Subject: simplify compatibility list by defaulting compatibility type, update readme --- README.md | 14 +++++-- .../Framework/Models/ModCompatibility.cs | 2 +- .../StardewModdingAPI.config.json | 47 +--------------------- 3 files changed, 14 insertions(+), 49 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/README.md b/README.md index bc5da52e..332a3754 100644 --- a/README.md +++ b/README.md @@ -142,14 +142,22 @@ on the wiki for the first-time setup. ## Advanced usage ### Configuration file You can customise the SMAPI behaviour by editing the `StardewModdingAPI.config.json` file in your -game folder. It contains these fields: +game folder. + +Basic fields: field | purpose ----- | ------- `DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). `CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. Changing this field is not recommended and may destabilise your game. -`VerboseLogging` | Whether SMAPI should log more information about the game context. +`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. + +Advanced fields (changing these isn't recommended and may destabilise your game): + +field | purpose +----- | ------- +`DisabledMods` | A list of mods to consider obsolete and not load. +`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. This can be used to force SMAPI to load an incompatible mod, though that isn't recommended. ### Command-line arguments SMAPI recognises the following command-line arguments. These are intended for internal use or diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index 72b6f2af..91be1d38 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -37,6 +37,6 @@ namespace StardewModdingAPI.Framework.Models public string ReasonPhrase { get; set; } /// Indicates how SMAPI should consider the mod. - public ModCompatibilityType Compatibility { get; set; } + public ModCompatibilityType Compatibility { get; set; } = ModCompatibilityType.AssumeBroken; } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 90bf29c9..ed03d936 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -45,7 +45,8 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha /** * A list of mod versions SMAPI should consider compatible or broken regardless of whether it - * detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. + * detects incompatible code. The default for each record is to assume broken; to force SMAPI to + * load a mod regardless of compatibility checks, add a "Compatibility": "AssumeCompatible" field. * Changing this field is not recommended and may destabilise your game. */ "ModCompatibility": [ @@ -53,7 +54,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "AccessChestAnywhere", "ID": [ "AccessChestAnywhere" ], "UpperVersion": "1.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/257", "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Needs update for SDV 1.1." }, @@ -61,7 +61,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Almighty Tool", "ID": [ "AlmightyTool.dll" ], "UpperVersion": "1.1.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/439" ], "Notes": "Needs update for SDV 1.2." }, @@ -69,7 +68,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Better Sprinklers", "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], "UpperVersion": "2.3.1-pathoschild-update", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/41", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." }, @@ -77,7 +75,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Birthday Mail", "ID": [ "005e02dc-d900-425c-9c68-1ff55c5a295d" ], "UpperVersion": "1.2.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/276", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -85,7 +82,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Chest Label System", "ID": [ "SPDChestLabel" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/242", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.1." }, @@ -93,7 +89,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Chest Pooling", "ID": [ "ChestPooling.dll" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Needs update for SDV 1.2." }, @@ -101,7 +96,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Chests Anywhere", "ID": [ "ChestsAnywhere", /*since 1.9*/ "Pathoschild.ChestsAnywhere" ], "UpperVersion": "1.9-beta", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/518" ], "Notes": "Needs update for SDV 1.2." }, @@ -109,7 +103,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "CJB Automation", "ID": [ "CJBAutomation" ], "UpperVersion": "1.4", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/211", "http://www.nexusmods.com/stardewvalley/mods/1063" ], "Notes": "Needs update for SDV 1.2." }, @@ -131,7 +124,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "CJB Show Item Sell Price", "ID": [ "CJBShowItemSellPrice" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/93" ], "Notes": "Needs update for SDV 1.2." }, @@ -139,7 +131,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Cooking Skill", "ID": [ "CookingSkill" ], "UpperVersion": "1.0.3", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/522" ], "Notes": "Needs update for SDV 1.2." }, @@ -147,7 +138,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Cooking Skill Prestige Adapter", "ID": [ "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63" ], "UpperVersion": "1.0.4", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/569", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -155,7 +145,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Enemy Health Bars", "ID": [ "SPDHealthBar" ], "UpperVersion": "1.7", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/193", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -163,7 +152,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Entoarox Framework", "ID": [ "eacdb74b-4080-4452-b16b-93773cda5cf9", /*since ???*/ "Entoarox.EntoaroxFramework" ], "UpperVersion": "1.7.5", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/4228" ], "Notes": "Needs update for SDV 1.2." }, @@ -172,7 +160,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "Mystra007ExtendedFridge" ], "UpperVersion": "1.0", "UpperVersionLabel": "0.94", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/485" ], "Notes": "Needs update for SDV 1.2. Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest." }, @@ -180,7 +167,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "FarmAutomation.ItemCollector", "ID": [ "FarmAutomation.ItemCollector.dll", /*since 0.4*/ "Maddy99.FarmAutomation.ItemCollector" ], "UpperVersion": "0.4", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/125172" ], "Notes": "Needs update for SDV 1.2." }, @@ -188,7 +174,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Instant Geode", "ID": [ "InstantGeode" ], "UpperVersion": "1.12", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/109038", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -196,7 +181,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Gate Opener", "ID": [ "GateOpener.dll", /*since 1.1*/ "mralbobo.GateOpener" ], "UpperVersion": "1.0.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/111988" ], "Notes": "Needs update for SDV 1.2." }, @@ -204,7 +188,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Get Dressed", "ID": [ "GetDressed.dll", /*since 3.3*/ "Advize.GetDressed" ], "UpperVersion": "3.3", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/331" ], "Notes": "Needs update for SDV 1.2." }, @@ -212,7 +195,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Gift Taste Helper", "ID": [ "8008db57-fa67-4730-978e-34b37ef191d6" ], "UpperVersion": "2.3.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/229" ], "Notes": "Needs update for SDV 1.2." }, @@ -220,14 +202,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Lookup Anything", "ID": [ "LookupAnything", /*since 1.10.1*/ "Pathoschild.LookupAnything" ], "UpperVersion": "1.10.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/541" ], "Notes": "Needs update for SDV 1.2." }, { "Name": "Makeshift Multiplayer", "ID": [ "StardewValleyMP", /*since 0.3*/ "spacechase0.StardewValleyMP" ], - "Compatibility": "AssumeBroken", "UpperVersion": "0.3.3", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/501" ], "Notes": "Needs update for SDV 1.2." @@ -236,7 +216,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "NoSoilDecay", "ID": [ "289dee03-5f38-4d8e-8ffc-e440198e8610" ], "UpperVersion": "0.5", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/237", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2, and uses Assembly.GetExecutingAssembly().Location." }, @@ -245,7 +224,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "NPCMapLocationsMod" ], "LowerVersion": "1.42", "UpperVersion": "1.43", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/239" ], "ReasonPhrase": "These versions have an update check error which crash the game." }, @@ -253,7 +231,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Part of the Community", "ID": [ "SB_PotC" ], "UpperVersion": "1.0.8", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/923" ], "ReasonPhrase": "Needs update for SDV 1.2." }, @@ -261,7 +238,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Point-and-Plant", "ID": [ "PointAndPlant.dll" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/572" ], "Notes": "Needs update for SDV 1.2." }, @@ -269,7 +245,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "PrairieKingMadeEasy", "ID": [ "PrairieKingMadeEasy.dll" ], "UpperVersion": "1.0.0", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/3594", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -277,7 +252,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Rush Orders", "ID": [ "RushOrders", /*since 1.1*/ "spacechase0.RushOrders" ], "UpperVersion": "1.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/605" ], "Notes": "Needs update for SDV 1.2." }, @@ -285,7 +259,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Save Anywhere", "ID": [ "SaveAnywhere" ], "UpperVersion": "2.3", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/444" ], "Notes": "Needs update for SDV 1.2." }, @@ -293,7 +266,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Simple Sprinklers", "ID": [ "SimpleSprinkler.dll" ], "UpperVersion": "1.4", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/76" ], "Notes": "Needs update for SDV 1.2." }, @@ -301,7 +273,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Sprint and Dash", "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.0", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/3531", "http://community.playstarbound.com/resources/4201" ], "Notes": "Needs update for SDV 1.2." }, @@ -309,7 +280,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Sprint and Dash Redux", "ID": [ "SPDSprintAndDash" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/4201" ], "Notes": "Needs update for SDV 1.2." }, @@ -317,7 +287,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "StackSplitX", "ID": [ "StackSplitX.dll" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/798" ], "Notes": "Needs update for SDV 1.2." }, @@ -325,7 +294,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Teleporter", "ID": [ "Teleporter" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/4374" ], "Notes": "Needs update for SDV 1.2." }, @@ -333,7 +301,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "TimeSpeed", "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "4108e859-333c-4fec-a1a7-d2e18c1019fe", /*since 2.1*/ "community.TimeSpeed" ], "UpperVersion": "2.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/169" ], "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." }, @@ -342,7 +309,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "ID": [ "Demiacle.UiModSuite" ], "UpperVersion": "0.5", "UpperVersionLabel": "1.0", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://www.nexusmods.com/stardewvalley/mods/1023" ], "Notes": "Needs update for SDV 1.2. Actual upper version is 1.0, but mod incorrectly sets it to 0.5 in the manifest." }, @@ -350,7 +316,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Weather Controller", "ID": [ "WeatherController.dll" ], "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/111526", "http://community.playstarbound.com/threads/132096" ], "Notes": "Needs update for SDV 1.2." }, @@ -358,7 +323,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "zDailyIncrease", "ID": [ "zdailyincrease" ], "UpperVersion": "1.2", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/resources/4247" ], "Notes": "Needs update for SDV 1.2." }, @@ -366,7 +330,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoom Out Extreme", "ID": [ "ZoomMod" ], "UpperVersion": "0.1", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "http://community.playstarbound.com/threads/115028" ], "Notes": "Needs update for SDV 1.2." }, @@ -374,7 +337,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Better RNG", "ID": [ "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", /*since 1.6*/ "Zoryn.BetterRNG" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, @@ -382,7 +344,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Calendar Anywhere", "ID": [ "a41c01cd-0437-43eb-944f-78cb5a53002a", /*since 1.6*/ "Zoryn.CalendarAnywhere" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, @@ -390,7 +351,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Health Bars", "ID": [ "HealthBars.dll", /*since 1.6*/ "Zoryn.HealthBars" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, @@ -398,7 +358,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Junimo Deposit Anywhere", "ID": [ "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", /*since 1.6*/ "Zoryn.JunimoDepositAnywhere" ], "UpperVersion": "1.7", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, @@ -406,7 +365,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Movement Mod", "ID": [ "8a632929-8335-484f-87dd-c29d2ba3215d", /*since 1.6*/ "Zoryn.MovementModifier" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." }, @@ -414,7 +372,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Name": "Zoryn's Regen Mod", "ID": [ "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", /*since 1.6*/ "Zoryn.RegenMod" ], "UpperVersion": "1.6", - "Compatibility": "AssumeBroken", "UpdateUrls": [ "https://github.com/Zoryn4163/SMAPI-Mods/releases" ], "Notes": "Needs update for SDV 1.2." } -- cgit From c20b21bcaa799a9961799316b7f4b7d8f929ca3f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 21 Jul 2017 01:51:05 -0400 Subject: add support for disambiguating IDs in mod compatibility list --- .../Framework/ModLoading/ModResolver.cs | 6 +-- .../Framework/Models/ModCompatibility.cs | 6 +-- .../Framework/Models/ModCompatibilityID.cs | 57 ++++++++++++++++++++++ .../Framework/Serialisation/SFieldConverter.cs | 19 +++++++- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index fdcbdaa7..6b19db5c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -71,9 +71,9 @@ namespace StardewModdingAPI.Framework.ModLoading compatibility = ( from mod in compatibilityRecords where - mod.ID.Contains(key, StringComparer.InvariantCultureIgnoreCase) - && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) - && !manifest.Version.IsNewerThan(mod.UpperVersion) + mod.ID.Any(p => p.Matches(key, manifest)) + && (mod.LowerVersion == null || !manifest.Version.IsOlderThan(mod.LowerVersion)) + && !manifest.Version.IsNewerThan(mod.UpperVersion) select mod ).FirstOrDefault(); } diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs index 91be1d38..d3a9c533 100644 --- a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -9,11 +9,9 @@ namespace StardewModdingAPI.Framework.Models /********* ** Accessors *********/ - /**** - ** From config - ****/ /// The unique mod IDs. - public string[] ID { get; set; } + [JsonConverter(typeof(SFieldConverter))] + public ModCompatibilityID[] ID { get; set; } /// The mod name. public string Name { get; set; } diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs new file mode 100644 index 00000000..98e70116 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibilityID.cs @@ -0,0 +1,57 @@ +using System; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.Models +{ + /// Uniquely identifies a mod for compatibility checks. + internal class ModCompatibilityID + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The mod name to disambiguate non-unique IDs, or null to ignore the mod name. + public string Name { get; set; } + + /// The author name to disambiguate non-unique IDs, or null to ignore the author. + public string Author { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModCompatibilityID() { } + + /// Construct an instance. + /// The mod ID or a JSON string matching the fields. + public ModCompatibilityID(string data) + { + // JSON can be stuffed into the ID string as a convenience hack to keep JSON mod lists + // formatted readably. The tradeoff is that the format is a bit more magical, but that's + // probably acceptable since players aren't meant to edit it. It's also fairly clear what + // the JSON strings do, if not necessarily how. + if (data.StartsWith("{")) + JsonConvert.PopulateObject(data, this); + else + this.ID = data; + } + + /// Get whether this ID matches a given mod manifest. + /// The mod's unique ID, or a substitute ID if it isn't set in the manifest. + /// The manifest to check. + public bool Matches(string id, IManifest manifest) + { + return + this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase) + && ( + this.Author == null + || this.Author.Equals(manifest.Author, StringComparison.InvariantCultureIgnoreCase) + || (manifest.ExtraFields.ContainsKey("Authour") && this.Author.Equals(manifest.ExtraFields["Authour"].ToString(), StringComparison.InvariantCultureIgnoreCase)) + ) + && (this.Name == null || this.Name.Equals(manifest.Name, StringComparison.InvariantCultureIgnoreCase)); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs index 9dc62b6a..11ffdccb 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/SFieldConverter.cs @@ -24,7 +24,10 @@ namespace StardewModdingAPI.Framework.Serialisation /// The object type. public override bool CanConvert(Type objectType) { - return objectType == typeof(ISemanticVersion) || objectType == typeof(IManifestDependency[]); + return + objectType == typeof(ISemanticVersion) + || objectType == typeof(IManifestDependency[]) + || objectType == typeof(ModCompatibilityID[]); } /// Reads the JSON representation of the object. @@ -83,6 +86,20 @@ namespace StardewModdingAPI.Framework.Serialisation return result.ToArray(); } + // mod compatibility ID + if (objectType == typeof(ModCompatibilityID[])) + { + List result = new List(); + foreach (JToken child in JArray.Load(reader).Children()) + { + result.Add(child is JValue value + ? new ModCompatibilityID(value.Value()) + : child.ToObject() + ); + } + return result.ToArray(); + } + // unknown throw new NotSupportedException($"Unknown type '{objectType?.FullName}'."); } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 9047aea4..f7992122 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -128,6 +128,7 @@ + -- cgit From 74be6f13114e8e4cb8421a684009d160c4e861f1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 13:15:28 -0400 Subject: improve handling of legacy non-semantic game versions (#333) --- .../Utilities/SemanticVersionTests.cs | 30 ++++++++++ src/StardewModdingAPI/Constants.cs | 28 ++------- src/StardewModdingAPI/Framework/GameVersion.cs | 68 ++++++++++++++++++++++ src/StardewModdingAPI/Program.cs | 16 ++--- src/StardewModdingAPI/SemanticVersion.cs | 3 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 6 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/GameVersion.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs b/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs index 95d0d74f..db46aee4 100644 --- a/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/StardewModdingAPI.Tests/Utilities/SemanticVersionTests.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using NUnit.Framework; +using StardewModdingAPI.Framework; namespace StardewModdingAPI.Tests.Utilities { @@ -206,6 +207,35 @@ namespace StardewModdingAPI.Tests.Utilities return version.IsBetween(lower, upper); } + /**** + ** GameVersion + ****/ + [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] + [TestCase("1.0")] + [TestCase("1.01")] + [TestCase("1.02")] + [TestCase("1.03")] + [TestCase("1.04")] + [TestCase("1.05")] + [TestCase("1.051")] + [TestCase("1.051b")] + [TestCase("1.06")] + [TestCase("1.07")] + [TestCase("1.07a")] + [TestCase("1.1")] + [TestCase("1.11")] + [TestCase("1.2")] + [TestCase("1.2.15")] + public void GameVersion(string versionStr) + { + // act + GameVersion version = new GameVersion(versionStr); + + // assert + Assert.AreEqual(versionStr, version.ToString(), "The game version did not round-trip to the same value."); + Assert.IsTrue(version.IsOlderThan(new SemanticVersion("1.2.30")), "The game version should be considered older than the later semantic versions."); + } + /********* ** Private methods diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 8217a6a5..ee57be0f 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -9,6 +9,7 @@ using StardewModdingAPI.AssemblyRewriters.Finders; using StardewModdingAPI.AssemblyRewriters.Rewriters; using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework; using StardewValley; namespace StardewModdingAPI @@ -86,7 +87,7 @@ namespace StardewModdingAPI internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); /// The game's current semantic version. - internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion(); + internal static ISemanticVersion GameVersion { get; } = new GameVersion(Constants.GetGameVersion()); /// The target game platform. internal static Platform TargetPlatform { get; } = @@ -219,19 +220,6 @@ namespace StardewModdingAPI }; } - /// Get game current version as it should be displayed to players. - /// The semantic game version. - internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) - { - switch (version.ToString()) - { - case "1.1.1": - return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 - default: - return version; - } - } - /// Get the name of a save directory for the current player. private static string GetSaveFolderName() { @@ -239,20 +227,14 @@ namespace StardewModdingAPI return $"{prefix}_{Game1.uniqueIDForThisGame}"; } - /// Get the game's current semantic version. - private static ISemanticVersion GetGameVersion() + /// Get the game's current version string. + private static string GetGameVersion() { - // get raw version // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); if (field == null) throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); - string version = (string)field.GetValue(null); - - // get semantic version - if (version == "1.11") - version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks - return new SemanticVersion(version); + return (string)field.GetValue(null); } } } diff --git a/src/StardewModdingAPI/Framework/GameVersion.cs b/src/StardewModdingAPI/Framework/GameVersion.cs new file mode 100644 index 00000000..48159f61 --- /dev/null +++ b/src/StardewModdingAPI/Framework/GameVersion.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework +{ + /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. + internal class GameVersion : SemanticVersion + { + /********* + ** Private methods + *********/ + /// A mapping of game to semantic versions. + private static readonly IDictionary VersionMap = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["1.01"] = "1.0.1", + ["1.02"] = "1.0.2", + ["1.03"] = "1.0.3", + ["1.04"] = "1.0.4", + ["1.05"] = "1.0.5", + ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. + ["1.051b"] = "1.0.6-prelease2", + ["1.06"] = "1.0.6", + ["1.07"] = "1.0.7", + ["1.07a"] = "1.0.8-prerelease1", + ["1.11"] = "1.1.1" + }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The game version string. + public GameVersion(string version) + : base(GameVersion.GetSemanticVersionString(version)) { } + + /// Get a string representation of the version. + public override string ToString() + { + return GameVersion.GetGameVersionString(base.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Convert a game version string to a semantic version string. + /// The game version string. + private static string GetSemanticVersionString(string gameVersion) + { + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; + } + + /// Convert a game version string to a semantic version string. + /// The game version string. + private static string GetGameVersionString(string gameVersion) + { + foreach (var mapping in GameVersion.VersionMap) + { + if (mapping.Value.Equals(gameVersion, StringComparison.InvariantCultureIgnoreCase)) + return mapping.Key; + } + return gameVersion; + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 75bb991d..ff73962e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -125,7 +125,7 @@ namespace StardewModdingAPI try { // init logging - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); this.Monitor.Log($"Mods go here: {Constants.ModPath}"); #if SMAPI_1_x this.Monitor.Log("Preparing SMAPI..."); @@ -138,13 +138,13 @@ namespace StardewModdingAPI // validate game version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); this.PressAnyKeyToExit(); return; } if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.GetGameDisplayVersion(Constants.MaximumGameVersion)}. Please check for a newer version of SMAPI.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -191,8 +191,8 @@ namespace StardewModdingAPI ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; } catch (Exception ex) { @@ -315,7 +315,7 @@ namespace StardewModdingAPI if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) { PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) - ? $"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI." + ? $"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI." : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." ); } @@ -445,8 +445,8 @@ namespace StardewModdingAPI // update window titles int modsLoaded = this.ModRegistry.GetMods().Count(); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; // start SMAPI console new Thread(this.RunConsoleLoop).Start(); diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index f30c43cd..782a962b 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -117,8 +117,7 @@ namespace StardewModdingAPI { // compare numerically if possible { - int curNum, otherNum; - if (int.TryParse(curParts[i], out curNum) && int.TryParse(otherParts[i], out otherNum)) + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) return curNum.CompareTo(otherNum); } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index f7992122..4cef91d9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -126,6 +126,7 @@ + -- cgit From 4ea6a4102bb69b72391334c4825bd393eff6ac97 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 15:08:14 -0400 Subject: add support for partial cache invalidation (#335) --- src/StardewModdingAPI/Constants.cs | 68 +++++++- src/StardewModdingAPI/Framework/SContentManager.cs | 173 +++++++++++++++------ src/StardewModdingAPI/Program.cs | 16 +- 3 files changed, 202 insertions(+), 55 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index ee57be0f..e85d7e2b 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -11,6 +11,9 @@ using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Objects; +using StardewValley.Projectiles; namespace StardewModdingAPI { @@ -99,7 +102,7 @@ namespace StardewModdingAPI /********* - ** Protected methods + ** Internal methods *********/ /// Get metadata for mapping assemblies to the current platform. /// The target game platform. @@ -220,6 +223,69 @@ namespace StardewModdingAPI }; } + /// Get the game's static asset setters by (non-normalised) asset name. + /// Derived from . + internal static IDictionary> GetCoreAssetSetters() + { + return new Dictionary> + { + // from Game1.loadContent + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), + ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), + ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), + ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), + ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), + ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), + ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), + ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), + ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), + ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), + ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), + ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), + ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), + ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), + ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), + ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), + ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), + ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), + ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), + ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), + ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), + ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), + ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), + ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), + ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), + ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), + ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), + ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), + ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), + ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), + ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), + ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), + ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), + ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), + ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), + + // from Farmer constructor + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + } + }; + } + + + /********* + ** Private methods + *********/ /// Get the name of a save directory for the current player. private static string GetSaveFolderName() { diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 669b0e7a..e6b0cac2 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -5,14 +5,10 @@ using System.IO; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Objects; -using StardewValley.Projectiles; namespace StardewModdingAPI.Framework { @@ -40,6 +36,12 @@ namespace StardewModdingAPI.Framework /// The private method which generates the locale portion of an asset name. private readonly IPrivateMethod GetKeyLocale; + /// The language codes used in asset keys. + private IDictionary KeyLocales; + + /// The game's static asset setters by normalised asset name. + private readonly IDictionary CoreAssetSetters; + /********* ** Accessors @@ -86,6 +88,48 @@ namespace StardewModdingAPI.Framework } else this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic + + // get asset key locales + this.KeyLocales = this.GetKeyLocales(reflection); + this.CoreAssetSetters = this.GetCoreAssetSetters(); + + } + + /// Get methods to reload core game assets by normalised key. + private IDictionary GetCoreAssetSetters() + { + return Constants.GetCoreAssetSetters() + .ToDictionary>, string, Action>( + p => this.NormaliseAssetName(p.Key), + p => () => p.Value(this, p.Key) + ); + } + + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; } /// Normalise path separators in a file path. For asset keys, see instead. @@ -159,54 +203,55 @@ namespace StardewModdingAPI.Framework return this.GetKeyLocale.Invoke(); } + /// Get the cached asset keys. + public IEnumerable GetAssetKeys() + { + IEnumerable GetAllAssetKeys() + { + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string _); + yield return assetKey; + } + } + + return GetAllAssetKeys().Distinct(); + } + /// Reset the asset cache and reload the game's static assets. + /// Matches the asset keys to invalidate. /// This implementation is derived from . - public void Reset() + public void InvalidateCache(Func predicate) { - this.Monitor.Log("Resetting asset cache...", LogLevel.Trace); - this.Cache.Clear(); - - // from Game1.LoadContent - Game1.daybg = this.Load("LooseSprites\\daybg"); - Game1.nightbg = this.Load("LooseSprites\\nightbg"); - Game1.menuTexture = this.Load("Maps\\MenuTiles"); - Game1.lantern = this.Load("LooseSprites\\Lighting\\lantern"); - Game1.windowLight = this.Load("LooseSprites\\Lighting\\windowLight"); - Game1.sconceLight = this.Load("LooseSprites\\Lighting\\sconceLight"); - Game1.cauldronLight = this.Load("LooseSprites\\Lighting\\greenLight"); - Game1.indoorWindowLight = this.Load("LooseSprites\\Lighting\\indoorWindowLight"); - Game1.shadowTexture = this.Load("LooseSprites\\shadow"); - Game1.mouseCursors = this.Load("LooseSprites\\Cursors"); - Game1.controllerMaps = this.Load("LooseSprites\\ControllerMaps"); - Game1.animations = this.Load("TileSheets\\animations"); - Game1.achievements = this.Load>("Data\\Achievements"); - Game1.NPCGiftTastes = this.Load>("Data\\NPCGiftTastes"); - Game1.dialogueFont = this.Load("Fonts\\SpriteFont1"); - Game1.smallFont = this.Load("Fonts\\SmallFont"); - Game1.tinyFont = this.Load("Fonts\\tinyFont"); - Game1.tinyFontBorder = this.Load("Fonts\\tinyFontBorder"); - Game1.objectSpriteSheet = this.Load("Maps\\springobjects"); - Game1.cropSpriteSheet = this.Load("TileSheets\\crops"); - Game1.emoteSpriteSheet = this.Load("TileSheets\\emotes"); - Game1.debrisSpriteSheet = this.Load("TileSheets\\debris"); - Game1.bigCraftableSpriteSheet = this.Load("TileSheets\\Craftables"); - Game1.rainTexture = this.Load("TileSheets\\rain"); - Game1.buffsIcons = this.Load("TileSheets\\BuffsIcons"); - Game1.objectInformation = this.Load>("Data\\ObjectInformation"); - Game1.bigCraftablesInformation = this.Load>("Data\\BigCraftablesInformation"); - FarmerRenderer.hairStylesTexture = this.Load("Characters\\Farmer\\hairstyles"); - FarmerRenderer.shirtsTexture = this.Load("Characters\\Farmer\\shirts"); - FarmerRenderer.hatsTexture = this.Load("Characters\\Farmer\\hats"); - FarmerRenderer.accessoriesTexture = this.Load("Characters\\Farmer\\accessories"); - Furniture.furnitureTexture = this.Load("TileSheets\\furniture"); - SpriteText.spriteTexture = this.Load("LooseSprites\\font_bold"); - SpriteText.coloredTexture = this.Load("LooseSprites\\font_colored"); - Tool.weaponsTexture = this.Load("TileSheets\\weapons"); - Projectile.projectileSheet = this.Load("TileSheets\\Projectiles"); - - // from Farmer constructor - if (Game1.player != null) - Game1.player.FarmerRenderer = new FarmerRenderer(this.Load("Characters\\Farmer\\farmer_" + (Game1.player.isMale ? "" : "girl_") + "base")); + // find matching asset keys + HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); + foreach (string cacheKey in this.Cache.Keys) + { + this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); + if (predicate(assetKey)) + { + purgeAssetKeys.Add(assetKey); + purgeCacheKeys.Add(cacheKey); + } + } + + // purge from cache + foreach (string key in purgeCacheKeys) + this.Cache.Remove(key); + + // reload core game assets + int reloaded = 0; + foreach (string key in purgeAssetKeys) + { + if (this.CoreAssetSetters.TryGetValue(key, out Action reloadAsset)) + { + reloadAsset(); + reloaded++; + } + } + + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); } @@ -221,6 +266,33 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } + /// Parse a cache key into its component parts. + /// The input cache key. + /// The original asset key. + /// The asset locale code (or null if not localised). + private void ParseCacheKey(string cacheKey, out string assetKey, out string localeCode) + { + // handle localised key + if (!string.IsNullOrWhiteSpace(cacheKey)) + { + int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture); + if (lastSepIndex >= 0) + { + string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + if (this.KeyLocales.ContainsKey(suffix)) + { + assetKey = cacheKey.Substring(0, lastSepIndex); + localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1); + return; + } + } + } + + // handle simple key + assetKey = cacheKey; + localeCode = null; + } + /// Load the initial asset from the registered . /// The basic asset metadata. /// Returns the loaded asset metadata, or null if no loader matched. @@ -365,7 +437,8 @@ namespace StardewModdingAPI.Framework // can't know which assets are meant to be disposed. Here we remove current assets from // the cache, but don't dispose them to avoid crashing any code that still references // them. The garbage collector will eventually clean up any unused assets. - this.Reset(); + this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); + this.InvalidateCache(p => true); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 50ab4e25..56c56431 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -796,7 +796,7 @@ namespace StardewModdingAPI if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) deprecationWarnings.Add(() => this.DeprecationManager.Warn(metadata.DisplayName, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.PendingRemoval)); #else - if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] {typeof(IModHelper)})) + if (!this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(IModHelper) })) this.Monitor.Log($"{metadata.DisplayName} doesn't implement Entry() and may not work correctly.", LogLevel.Error); #endif } @@ -812,19 +812,27 @@ namespace StardewModdingAPI { if (metadata.Mod.Helper.Content is ContentHelper helper) { + // TODO: optimise by only reloading assets the new editors/loaders can intercept helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) - this.ContentManager.Reset(); + { + this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); + this.ContentManager.InvalidateCache(p => true); + } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) - this.ContentManager.Reset(); + { + this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); + this.ContentManager.InvalidateCache(p => true); + } }; } } - this.ContentManager.Reset(); + this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); + this.ContentManager.InvalidateCache(p => true); } /// Reload translations for all mods. -- cgit From 467ad2ffd45f7c034b89b668883bb5271524821d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 17:36:31 -0400 Subject: let mods invalidate cached assets by name or type (#335) --- .../Framework/ModHelpers/ContentHelper.cs | 27 +++++++++++++++++++++- src/StardewModdingAPI/Framework/SContentManager.cs | 17 ++++++++++---- src/StardewModdingAPI/IContentHelper.cs | 14 +++++++++++ src/StardewModdingAPI/Program.cs | 11 +++++---- 4 files changed, 59 insertions(+), 10 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 5f72176e..c052759f 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -33,6 +33,9 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The friendly mod name for use in errors. private readonly string ModName; + /// Encapsulates monitoring and logging for a given module. + private readonly IMonitor Monitor; + /********* ** Accessors @@ -58,13 +61,15 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The absolute path to the mod folder. /// The unique ID of the relevant mod. /// The friendly mod name for use in errors. - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName) + /// Encapsulates monitoring and logging. + public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; this.ModFolderPathFromContent = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + this.Monitor = monitor; } /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. @@ -176,6 +181,26 @@ namespace StardewModdingAPI.Framework.ModHelpers } } + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder) + { + this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, source); + return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); + } + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + public bool InvalidateCache() + { + this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); + return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + } /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index e6b0cac2..0741d04d 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -220,8 +220,9 @@ namespace StardewModdingAPI.Framework /// Reset the asset cache and reload the game's static assets. /// Matches the asset keys to invalidate. + /// Returns whether any cache entries were invalidated. /// This implementation is derived from . - public void InvalidateCache(Func predicate) + public bool InvalidateCache(Func predicate) { // find matching asset keys HashSet purgeCacheKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); @@ -229,7 +230,8 @@ namespace StardewModdingAPI.Framework foreach (string cacheKey in this.Cache.Keys) { this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); - if (predicate(assetKey)) + Type type = this.Cache[cacheKey].GetType(); + if (predicate(assetKey, type)) { purgeAssetKeys.Add(assetKey); purgeCacheKeys.Add(cacheKey); @@ -251,7 +253,14 @@ namespace StardewModdingAPI.Framework } } - this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + // report result + if (purgeCacheKeys.Any()) + { + this.Monitor.Log($"Invalidated {purgeCacheKeys.Count} cache entries for {purgeAssetKeys.Count} asset keys: {string.Join(", ", purgeCacheKeys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + return true; + } + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + return false; } @@ -438,7 +447,7 @@ namespace StardewModdingAPI.Framework // the cache, but don't dispose them to avoid crashing any code that still references // them. The garbage collector will eventually clean up any unused assets. this.Monitor.Log("Content manager disposed, resetting cache.", LogLevel.Trace); - this.InvalidateCache(p => true); + this.InvalidateCache((key, type) => true); } } } diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 32a9ff19..9fe29e4d 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -20,5 +20,19 @@ namespace StardewModdingAPI /// Where to search for a matching content asset. /// The is empty or contains invalid characters. string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder); + +#if !SMAPI_1_x + /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. + /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. + /// Where to search for a matching content asset. + /// The is empty or contains invalid characters. + /// Returns whether the given asset key was cached. + bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder); + + /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. + /// The asset type to remove from the cache. + /// Returns whether any assets were invalidated. + bool InvalidateCache(); +#endif } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 56c56431..969695aa 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -707,15 +707,16 @@ namespace StardewModdingAPI // inject data { + IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName); + IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); mod.ModManifest = manifest; mod.Helper = new ModHelper(manifest.UniqueID, metadata.DirectoryPath, jsonHelper, contentHelper, commandHelper, modRegistryHelper, reflectionHelper, translationHelper); - mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); + mod.Monitor = monitor; #if SMAPI_1_x mod.PathOnDisk = metadata.DirectoryPath; #endif @@ -818,7 +819,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => @@ -826,13 +827,13 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } }; } } this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); - this.ContentManager.InvalidateCache(p => true); + this.ContentManager.InvalidateCache((key, type) => true); } /// Reload translations for all mods. -- cgit From eeee6b185d5438e5d44ed0da7c23cf19813b29ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 23 Jul 2017 19:39:17 -0400 Subject: use more flexible approach to core asset reloading (#335) --- src/StardewModdingAPI/Constants.cs | 62 ------------ src/StardewModdingAPI/Framework/SContentManager.cs | 26 ++--- src/StardewModdingAPI/Metadata/CoreAssets.cs | 107 +++++++++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 115 insertions(+), 81 deletions(-) create mode 100644 src/StardewModdingAPI/Metadata/CoreAssets.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index e85d7e2b..af120850 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -11,9 +11,6 @@ using StardewModdingAPI.AssemblyRewriters.Rewriters.Wrappers; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewValley; -using StardewValley.BellsAndWhistles; -using StardewValley.Objects; -using StardewValley.Projectiles; namespace StardewModdingAPI { @@ -223,65 +220,6 @@ namespace StardewModdingAPI }; } - /// Get the game's static asset setters by (non-normalised) asset name. - /// Derived from . - internal static IDictionary> GetCoreAssetSetters() - { - return new Dictionary> - { - // from Game1.loadContent - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), - ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), - ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), - ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), - ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), - ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), - ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), - ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), - ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), - ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), - ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), - ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), - ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), - ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), - ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), - ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), - ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), - ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), - ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), - ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), - ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), - ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), - ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), - ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), - ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), - ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), - ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), - ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), - ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), - ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), - ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), - ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), - ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), - ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), - ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), - ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), - - // from Farmer constructor - ["Characters\\Farmer\\farmer_base"] = (content, key) => - { - if (Game1.player != null && Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - }, - ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => - { - if (Game1.player != null && !Game1.player.isMale) - Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); - } - }; - } - /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0741d04d..0854c379 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -8,6 +8,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Metadata; using StardewValley; namespace StardewModdingAPI.Framework @@ -37,10 +38,10 @@ namespace StardewModdingAPI.Framework private readonly IPrivateMethod GetKeyLocale; /// The language codes used in asset keys. - private IDictionary KeyLocales; + private readonly IDictionary KeyLocales; - /// The game's static asset setters by normalised asset name. - private readonly IDictionary CoreAssetSetters; + /// Provides metadata for core game assets. + private readonly CoreAssets CoreAssets; /********* @@ -89,22 +90,12 @@ namespace StardewModdingAPI.Framework else this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic - // get asset key locales + // get asset data + this.CoreAssets = new CoreAssets(this.NormaliseAssetName); this.KeyLocales = this.GetKeyLocales(reflection); - this.CoreAssetSetters = this.GetCoreAssetSetters(); } - /// Get methods to reload core game assets by normalised key. - private IDictionary GetCoreAssetSetters() - { - return Constants.GetCoreAssetSetters() - .ToDictionary>, string, Action>( - p => this.NormaliseAssetName(p.Key), - p => () => p.Value(this, p.Key) - ); - } - /// Get the locale codes (like ja-JP) used in asset keys. /// Simplifies access to private game code. private IDictionary GetKeyLocales(Reflector reflection) @@ -246,11 +237,8 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in purgeAssetKeys) { - if (this.CoreAssetSetters.TryGetValue(key, out Action reloadAsset)) - { - reloadAsset(); + if(this.CoreAssets.ReloadForKey(this, key)) reloaded++; - } } // report result diff --git a/src/StardewModdingAPI/Metadata/CoreAssets.cs b/src/StardewModdingAPI/Metadata/CoreAssets.cs new file mode 100644 index 00000000..256a911a --- /dev/null +++ b/src/StardewModdingAPI/Metadata/CoreAssets.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework; +using StardewValley; +using StardewValley.BellsAndWhistles; +using StardewValley.Objects; +using StardewValley.Projectiles; + +namespace StardewModdingAPI.Metadata +{ + /// Provides metadata about core assets in the game. + internal class CoreAssets + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + protected readonly Func GetNormalisedPath; + + /// The static asset setters. + private readonly IDictionary> StaticSetters; + + + /********* + ** Public methods + *********/ + /// Initialise the core asset data. + /// Normalises an asset key to match the cache key. + public CoreAssets(Func getNormalisedPath) + { + this.GetNormalisedPath = getNormalisedPath; + this.StaticSetters = + new Dictionary> + { + // from Game1.loadContent + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\daybg"] = (content, key) => Game1.daybg = content.Load(key), + ["LooseSprites\\nightbg"] = (content, key) => Game1.nightbg = content.Load(key), + ["Maps\\MenuTiles"] = (content, key) => Game1.menuTexture = content.Load(key), + ["LooseSprites\\Lighting\\lantern"] = (content, key) => Game1.lantern = content.Load(key), + ["LooseSprites\\Lighting\\windowLight"] = (content, key) => Game1.windowLight = content.Load(key), + ["LooseSprites\\Lighting\\sconceLight"] = (content, key) => Game1.sconceLight = content.Load(key), + ["LooseSprites\\Lighting\\greenLight"] = (content, key) => Game1.cauldronLight = content.Load(key), + ["LooseSprites\\Lighting\\indoorWindowLight"] = (content, key) => Game1.indoorWindowLight = content.Load(key), + ["LooseSprites\\shadow"] = (content, key) => Game1.shadowTexture = content.Load(key), + ["LooseSprites\\Cursors"] = (content, key) => Game1.mouseCursors = content.Load(key), + ["LooseSprites\\ControllerMaps"] = (content, key) => Game1.controllerMaps = content.Load(key), + ["TileSheets\\animations"] = (content, key) => Game1.animations = content.Load(key), + ["Data\\Achievements"] = (content, key) => Game1.achievements = content.Load>(key), + ["Data\\NPCGiftTastes"] = (content, key) => Game1.NPCGiftTastes = content.Load>(key), + ["Fonts\\SpriteFont1"] = (content, key) => Game1.dialogueFont = content.Load(key), + ["Fonts\\SmallFont"] = (content, key) => Game1.smallFont = content.Load(key), + ["Fonts\\tinyFont"] = (content, key) => Game1.tinyFont = content.Load(key), + ["Fonts\\tinyFontBorder"] = (content, key) => Game1.tinyFontBorder = content.Load(key), + ["Maps\\springobjects"] = (content, key) => Game1.objectSpriteSheet = content.Load(key), + ["TileSheets\\crops"] = (content, key) => Game1.cropSpriteSheet = content.Load(key), + ["TileSheets\\emotes"] = (content, key) => Game1.emoteSpriteSheet = content.Load(key), + ["TileSheets\\debris"] = (content, key) => Game1.debrisSpriteSheet = content.Load(key), + ["TileSheets\\Craftables"] = (content, key) => Game1.bigCraftableSpriteSheet = content.Load(key), + ["TileSheets\\rain"] = (content, key) => Game1.rainTexture = content.Load(key), + ["TileSheets\\BuffsIcons"] = (content, key) => Game1.buffsIcons = content.Load(key), + ["Data\\ObjectInformation"] = (content, key) => Game1.objectInformation = content.Load>(key), + ["Data\\BigCraftablesInformation"] = (content, key) => Game1.bigCraftablesInformation = content.Load>(key), + ["Characters\\Farmer\\hairstyles"] = (content, key) => FarmerRenderer.hairStylesTexture = content.Load(key), + ["Characters\\Farmer\\shirts"] = (content, key) => FarmerRenderer.shirtsTexture = content.Load(key), + ["Characters\\Farmer\\hats"] = (content, key) => FarmerRenderer.hatsTexture = content.Load(key), + ["Characters\\Farmer\\accessories"] = (content, key) => FarmerRenderer.accessoriesTexture = content.Load(key), + ["TileSheets\\furniture"] = (content, key) => Furniture.furnitureTexture = content.Load(key), + ["LooseSprites\\font_bold"] = (content, key) => SpriteText.spriteTexture = content.Load(key), + ["LooseSprites\\font_colored"] = (content, key) => SpriteText.coloredTexture = content.Load(key), + ["TileSheets\\weapons"] = (content, key) => Tool.weaponsTexture = content.Load(key), + ["TileSheets\\Projectiles"] = (content, key) => Projectile.projectileSheet = content.Load(key), + + // from Farmer constructor + ["Characters\\Farmer\\farmer_base"] = (content, key) => + { + if (Game1.player != null && Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + }, + ["Characters\\Farmer\\farmer_girl_base"] = (content, key) => + { + if (Game1.player != null && !Game1.player.isMale) + Game1.player.FarmerRenderer = new FarmerRenderer(content.Load(key)); + } + } + .ToDictionary(p => getNormalisedPath(p.Key), p => p.Value); + } + + /// Reload one of the game's core assets (if applicable). + /// The content manager through which to reload the asset. + /// The asset key to reload. + /// Returns whether an asset was reloaded. + public bool ReloadForKey(SContentManager content, string key) + { + // static assets + if (this.StaticSetters.TryGetValue(key, out Action reload)) + { + reload(content, key); + return true; + } + + return false; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 4cef91d9..8bbafca1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + -- cgit From 17acf248b66861217d48826e77f24cc311b4a22e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Jul 2017 21:54:46 -0400 Subject: prevent mods from accessing SMAPI internals using its own reflection helper (#334) --- release-notes.md | 8 ++-- .../Framework/ModHelpers/ReflectionHelper.cs | 46 +++++++++++++++++++++- src/StardewModdingAPI/Program.cs | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 75b9bbd1..bb7b7dfa 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,10 +17,12 @@ For mod developers: * Added support for optional dependencies. * Added support for string versions (like `"1.0-alpha"`) in `manifest.json`. * Added `IEquatable` to `ISemanticVersion`. -* Removed all deprecated code. -* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. -* Removed support for mods with a non-unique `UniqueID` value in their manifest. * Removed the TrainerMod `save` and `load` commands. +* **Breaking changes:** + * Removed all deprecated code. + * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. + * Removed support for mods with a non-unique `UniqueID` value in their manifest. + * Mods can no longer access SMAPI internals using the reflection helper, to discourage fragile mods. ## 1.15.2 For players: diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs index 9411a97a..14a339da 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using StardewModdingAPI.Framework.Reflection; namespace StardewModdingAPI.Framework.ModHelpers @@ -13,16 +13,21 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The underlying reflection helper. private readonly Reflector Reflector; + /// The mod name for error messages. + private readonly string ModName; + /********* ** Public methods *********/ /// Construct an instance. /// The unique ID of the relevant mod. + /// The mod name for error messages. /// The underlying reflection helper. - public ReflectionHelper(string modID, Reflector reflector) + public ReflectionHelper(string modID, string modName, Reflector reflector) : base(modID) { + this.ModName = modName; this.Reflector = reflector; } @@ -37,6 +42,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Returns the field wrapper, or null if the field doesn't exist and is false. public IPrivateField GetPrivateField(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateField(obj, name, required); } @@ -47,6 +53,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateField GetPrivateField(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateField(type, name, required); } @@ -60,6 +67,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateProperty(obj, name, required); } @@ -70,6 +78,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private property is not found. public IPrivateProperty GetPrivateProperty(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateProperty(type, name, required); } @@ -89,6 +98,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); IPrivateField field = this.GetPrivateField(obj, name, required); return field != null ? field.GetValue() @@ -107,6 +117,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// public TValue GetPrivateValue(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); IPrivateField field = this.GetPrivateField(type, name, required); return field != null ? field.GetValue() @@ -122,6 +133,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, required); } @@ -131,6 +143,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, required); } @@ -144,6 +157,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(object obj, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(obj); return this.Reflector.GetPrivateMethod(obj, name, argumentTypes, required); } @@ -154,7 +168,35 @@ namespace StardewModdingAPI.Framework.ModHelpers /// Whether to throw an exception if the private field is not found. public IPrivateMethod GetPrivateMethod(Type type, string name, Type[] argumentTypes, bool required = true) { + this.AssertAccessAllowed(type); return this.Reflector.GetPrivateMethod(type, name, argumentTypes, required); } + + + /********* + ** Private methods + *********/ + /// Assert that mods can use the reflection helper to access the given type. + /// The type being accessed. + private void AssertAccessAllowed(Type type) + { +#if !SMAPI_1_x + // validate type namespace + if (type.Namespace != null) + { + string rootSmapiNamespace = typeof(Program).Namespace; + if (type.Namespace == rootSmapiNamespace || type.Namespace.StartsWith(rootSmapiNamespace + ".")) + 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."); + } +#endif + } + + /// Assert that mods can use the reflection helper to access the given type. + /// The object being accessed. + private void AssertAccessAllowed(object obj) + { + if (obj != null) + this.AssertAccessAllowed(obj.GetType()); + } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 969695aa..b51917d9 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -710,7 +710,7 @@ namespace StardewModdingAPI IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); - IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, this.Reflection); + IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry); ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); -- cgit From 7992b52f035be5c6229ff0912bfd91084d41d5dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Jul 2017 23:18:49 -0400 Subject: fix AfterDayStarted event being raised during the new-game intro (#332) --- release-notes.md | 10 +++++----- src/StardewModdingAPI/Framework/SGame.cs | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index bb7b7dfa..4da9132d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -18,11 +18,11 @@ For mod developers: * Added support for string versions (like `"1.0-alpha"`) in `manifest.json`. * Added `IEquatable` to `ISemanticVersion`. * Removed the TrainerMod `save` and `load` commands. -* **Breaking changes:** - * Removed all deprecated code. - * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. - * Removed support for mods with a non-unique `UniqueID` value in their manifest. - * Mods can no longer access SMAPI internals using the reflection helper, to discourage fragile mods. +* Removed all deprecated code. +* Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. +* Removed support for mods with a non-unique `UniqueID` value in their manifest. +* Restrict mods from accessing SMAPI internals using its reflection helper, to discourage fragile mods. +* Fixed issue where `TimeEvents.AfterDayStarted` was raised during the new-game intro. ## 1.15.2 For players: diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index d6f1a05b..bec6538b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -19,7 +19,6 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; -using SFarmer = StardewValley.Farmer; namespace StardewModdingAPI.Framework { @@ -318,6 +317,11 @@ namespace StardewModdingAPI.Framework *********/ if (Context.IsSaveLoaded && !SaveGame.IsProcessing /*still loading save*/ && this.AfterLoadTimer >= 0) { +#if !SMAPI_1_x + if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) +#endif + this.AfterLoadTimer--; + if (this.AfterLoadTimer == 0) { this.Monitor.Log($"Context: loaded saved game '{Constants.SaveFolderName}', starting {Game1.currentSeason} {Game1.dayOfMonth} Y{Game1.year}.", LogLevel.Trace); @@ -329,7 +333,6 @@ namespace StardewModdingAPI.Framework #endif TimeEvents.InvokeAfterDayStarted(this.Monitor); } - this.AfterLoadTimer--; } /********* -- cgit From 9b22f3e004b35f66d9be6af211f20fe126fae209 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 31 Jul 2017 23:48:53 -0400 Subject: fix GraphicsEvents.Resize being raised before the game updates its window data (#328) --- release-notes.md | 3 ++- src/StardewModdingAPI/Events/GraphicsEvents.cs | 6 ++---- src/StardewModdingAPI/Framework/SGame.cs | 23 ++++++++++++++++++----- src/StardewModdingAPI/Program.cs | 1 - 4 files changed, 22 insertions(+), 11 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 4da9132d..14d0179c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,7 +22,8 @@ For mod developers: * Removed support for mods with no `Name`, `Version`, or `UniqueID` in their manifest. * Removed support for mods with a non-unique `UniqueID` value in their manifest. * Restrict mods from accessing SMAPI internals using its reflection helper, to discourage fragile mods. -* Fixed issue where `TimeEvents.AfterDayStarted` was raised during the new-game intro. +* Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. +* Fixed `TimeEvents.AfterDayStarted` being raised during the new-game intro. ## 1.15.2 For players: diff --git a/src/StardewModdingAPI/Events/GraphicsEvents.cs b/src/StardewModdingAPI/Events/GraphicsEvents.cs index 25b976f1..fff51bed 100644 --- a/src/StardewModdingAPI/Events/GraphicsEvents.cs +++ b/src/StardewModdingAPI/Events/GraphicsEvents.cs @@ -51,11 +51,9 @@ namespace StardewModdingAPI.Events ****/ /// Raise a event. /// Encapsulates monitoring and logging. - /// The object which raised the event. - /// The event arguments. - internal static void InvokeResize(IMonitor monitor, object sender, EventArgs e) + internal static void InvokeResize(IMonitor monitor) { - monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList(), sender, e); + monitor.SafelyRaisePlainEvent($"{nameof(GraphicsEvents)}.{nameof(GraphicsEvents.Resize)}", GraphicsEvents.Resize?.GetInvocationList()); } /**** diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index bec6538b..65191931 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -53,10 +53,6 @@ namespace StardewModdingAPI.Framework /// Whether the game is saving and SMAPI has already raised . private bool IsBetweenSaveEvents; - /// Whether the game's zoom level is at 100% (i.e. nothing should be scaled). - public bool ZoomLevelIsOne => Game1.options.zoomLevel.Equals(1.0f); - - /**** ** Game state ****/ @@ -75,7 +71,10 @@ namespace StardewModdingAPI.Framework /// The previous mouse position on the screen adjusted for the zoom level. private Point PreviousMousePosition; - /// The previous save ID at last check. + /// The window size value at last check. + private Point PreviousWindowSize; + + /// The save ID at last check. private ulong PreviousSaveID; /// A hash of at last check. @@ -352,6 +351,20 @@ namespace StardewModdingAPI.Framework SaveEvents.InvokeAfterReturnToTitle(this.Monitor); } + /********* + ** Window events + *********/ + // Here we depend on the game's viewport instead of listening to the Window.Resize + // event because we need to notify mods after the game handles the resize, so the + // game's metadata (like Game1.viewport) are updated. That's a bit complicated + // since the game adds & removes its own handler on the fly. + if (Game1.viewport.Width != this.PreviousWindowSize.X || Game1.viewport.Height != this.PreviousWindowSize.Y) + { + Point size = new Point(Game1.viewport.Width, Game1.viewport.Height); + GraphicsEvents.InvokeResize(this.Monitor); + this.PreviousWindowSize = size; + } + /********* ** Input events (if window has focus) *********/ diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b51917d9..0e1930ac 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -185,7 +185,6 @@ namespace StardewModdingAPI ((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose(); #endif this.GameInstance.Exiting += (sender, e) => this.Dispose(); - this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); -- cgit From 3599daee459ee27bfe9374c675eb71d086fcfd81 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 1 Aug 2017 00:51:27 -0400 Subject: remove support for invalidating mod assets per discussion (#335) --- src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 9 ++++----- src/StardewModdingAPI/IContentHelper.cs | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index c052759f..0456ce14 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -182,14 +182,13 @@ namespace StardewModdingAPI.Framework.ModHelpers } /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. + /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. /// Returns whether the given asset key was cached. - public bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder) + public bool InvalidateCache(string key) { - this.Monitor.Log($"Requested cache invalidation for '{key}' in {source}.", LogLevel.Trace); - string actualKey = this.GetActualAssetKey(key, source); + this.Monitor.Log($"Requested cache invalidation for '{key}'.", LogLevel.Trace); + string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); return this.ContentManager.InvalidateCache((otherKey, type) => otherKey.Equals(actualKey, StringComparison.InvariantCultureIgnoreCase)); } diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index 9fe29e4d..beaaf5d4 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -23,11 +23,10 @@ namespace StardewModdingAPI #if !SMAPI_1_x /// Remove an asset from the content cache so it's reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content. - /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. - /// Where to search for a matching content asset. + /// The asset key to invalidate in the content folder. /// The is empty or contains invalid characters. /// Returns whether the given asset key was cached. - bool InvalidateCache(string key, ContentSource source = ContentSource.ModFolder); + bool InvalidateCache(string key); /// Remove all assets of the given type from the cache so they're reloaded on the next request. This can be a very expensive operation and should only be used in very specific cases. This will reload core game assets if needed, but references to the former assets will still show the previous content. /// The asset type to remove from the cache. -- cgit From baeaf826a9bb185c78732b5f2b91c3f499246f1a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 7 Aug 2017 14:12:18 -0400 Subject: add asset editors & loaders to content API in 2.0 (#255) --- .../Framework/ModHelpers/ContentHelper.cs | 4 ++-- src/StardewModdingAPI/IContentHelper.cs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 0456ce14..1e987f00 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -47,10 +47,10 @@ namespace StardewModdingAPI.Framework.ModHelpers internal ObservableCollection ObservableAssetLoaders { get; } = new ObservableCollection(); /// Interceptors which provide the initial versions of matching content assets. - internal IList AssetLoaders => this.ObservableAssetLoaders; + public IList AssetLoaders => this.ObservableAssetLoaders; /// Interceptors which edit matching content assets after they're loaded. - internal IList AssetEditors => this.ObservableAssetEditors; + public IList AssetEditors => this.ObservableAssetEditors; /********* diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index beaaf5d4..f32c1d18 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; @@ -7,6 +8,21 @@ namespace StardewModdingAPI /// Provides an API for loading content assets. public interface IContentHelper : IModLinked { + /********* + ** Accessors + *********/ +#if !SMAPI_1_x + /// Interceptors which provide the initial versions of matching content assets. + IList AssetLoaders { get; } + + /// Interceptors which edit matching content assets after they're loaded. + IList AssetEditors { get; } +#endif + + + /********* + ** Public methods + *********/ /// Load content from the game folder or mod folder (if not already cached), and return it. When loading a .png file, this must be called outside the game's draw loop. /// The expected data type. The main supported types are and dictionaries; other types may be supported by the game's content pipeline. /// The asset key to fetch (if the is ), or the local path to a content file relative to the mod folder. -- cgit From dafebd1626b6c3b34a818f5f161289a6e32fe4af Mon Sep 17 00:00:00 2001 From: spacechase0 Date: Wed, 9 Aug 2017 16:40:10 -0400 Subject: Fix building SMAPI 1.x --- src/StardewModdingAPI/Framework/SGame.cs | 3 +++ src/StardewModdingAPI/Utilities/SDate.cs | 2 ++ 2 files changed, 5 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 65191931..755f0274 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -19,6 +19,9 @@ using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; using xTile.Layers; +#if SMAPI_1_x +using SFarmer = StardewValley.Farmer; +#endif namespace StardewModdingAPI.Framework { diff --git a/src/StardewModdingAPI/Utilities/SDate.cs b/src/StardewModdingAPI/Utilities/SDate.cs index bc3a2b38..d7631598 100644 --- a/src/StardewModdingAPI/Utilities/SDate.cs +++ b/src/StardewModdingAPI/Utilities/SDate.cs @@ -69,7 +69,9 @@ namespace StardewModdingAPI.Utilities this.Day = day; this.Season = season; this.Year = year; +#if !SMAPI_1_x this.DayOfWeek = this.GetDayOfWeek(); +#endif } /// Get the current in-game date. -- cgit From 021672e43db021ae436e23ff45f3d85e2a595cb0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 14 Aug 2017 01:57:11 -0400 Subject: add content helper properties for the current language --- release-notes.md | 1 + src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs | 6 ++++++ src/StardewModdingAPI/IContentHelper.cs | 7 +++++++ 3 files changed, 14 insertions(+) (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 90ca4e2c..9bfd1981 100644 --- a/release-notes.md +++ b/release-notes.md @@ -33,6 +33,7 @@ For players: For mod developers: * Added `Context.CanPlayerMove` value for mod convenience. +* Added `helper.Content` properties for the current language. * Fixed `GraphicsEvents.Resize` being raised before the game updates its window data. * Fixed `Context.IsPlayerFree` being incorrectly false in some cases (e.g. when using a tool). diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index 1e987f00..e94d309e 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -40,6 +40,12 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Accessors *********/ + /// The game's current locale code (like pt-BR). + public string CurrentLocale => this.ContentManager.GetLocale(); + + /// The game's current locale as an enum value. + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + /// The observable implementation of . internal ObservableCollection ObservableAssetEditors { get; } = new ObservableCollection(); diff --git a/src/StardewModdingAPI/IContentHelper.cs b/src/StardewModdingAPI/IContentHelper.cs index f32c1d18..b4557134 100644 --- a/src/StardewModdingAPI/IContentHelper.cs +++ b/src/StardewModdingAPI/IContentHelper.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewValley; namespace StardewModdingAPI { @@ -19,6 +20,12 @@ namespace StardewModdingAPI IList AssetEditors { get; } #endif + /// The game's current locale code (like pt-BR). + string CurrentLocale { get; } + + /// The game's current locale as an enum value. + LocalizedContentManager.LanguageCode CurrentLocaleConstant { get; } + /********* ** Public methods -- cgit From 2ec0e0e26a16b94ba4a12d3bb4561d64a7411b34 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 Aug 2017 23:27:07 -0400 Subject: only invalidate cache entries matched by new interceptors --- .../Framework/ModHelpers/ContentHelper.cs | 2 +- src/StardewModdingAPI/Framework/SContentManager.cs | 94 ++++++++++++++-------- src/StardewModdingAPI/Program.cs | 26 +++--- 3 files changed, 79 insertions(+), 43 deletions(-) (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs index e94d309e..ffa78ff6 100644 --- a/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelpers/ContentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 0854c379..9e086870 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; @@ -96,33 +97,6 @@ namespace StardewModdingAPI.Framework } - /// Get the locale codes (like ja-JP) used in asset keys. - /// Simplifies access to private game code. - private IDictionary GetKeyLocales(Reflector reflection) - { - // get the private code field directly to avoid changed-code logic - IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); - - // remember previous settings - LanguageCode previousCode = codeField.GetValue(); - string previousOverride = this.LanguageCodeOverride; - - // create locale => code map - IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.LanguageCodeOverride = null; - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) - { - codeField.SetValue(code); - map[this.GetKeyLocale.Invoke()] = code; - } - - // restore previous settings - codeField.SetValue(previousCode); - this.LanguageCodeOverride = previousOverride; - - return map; - } - /// Normalise path separators in a file path. For asset keys, see instead. /// The file path to normalise. public string NormalisePathSeparators(string path) @@ -209,10 +183,39 @@ namespace StardewModdingAPI.Framework return GetAllAssetKeys().Distinct(); } - /// Reset the asset cache and reload the game's static assets. + /// Purge assets from the cache that match one of the interceptors. + /// The asset editors for which to purge matching assets. + /// The asset loaders for which to purge matching assets. + /// Returns whether any cache entries were invalidated. + public bool InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) + { + if (!editors.Any() && !loaders.Any()) + return false; + + // get CanEdit/Load methods + MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); + MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); + + // invalidate matching keys + return this.InvalidateCache((assetName, assetType) => + { + // get asset metadata + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, assetType, this.NormaliseAssetName); + + // check loaders + MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(assetType); + if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { info }))) + return true; + + // check editors + MethodInfo canEditGeneric = canEdit.MakeGenericMethod(assetType); + return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { info })); + }); + } + + /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Returns whether any cache entries were invalidated. - /// This implementation is derived from . public bool InvalidateCache(Func predicate) { // find matching asset keys @@ -220,7 +223,7 @@ namespace StardewModdingAPI.Framework HashSet purgeAssetKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase); foreach (string cacheKey in this.Cache.Keys) { - this.ParseCacheKey(cacheKey, out string assetKey, out string localeCode); + this.ParseCacheKey(cacheKey, out string assetKey, out _); Type type = this.Cache[cacheKey].GetType(); if (predicate(assetKey, type)) { @@ -237,7 +240,7 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in purgeAssetKeys) { - if(this.CoreAssets.ReloadForKey(this, key)) + if (this.CoreAssets.ReloadForKey(this, key)) reloaded++; } @@ -263,6 +266,33 @@ namespace StardewModdingAPI.Framework || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset } + /// Get the locale codes (like ja-JP) used in asset keys. + /// Simplifies access to private game code. + private IDictionary GetKeyLocales(Reflector reflection) + { + // get the private code field directly to avoid changed-code logic + IPrivateField codeField = reflection.GetPrivateField(typeof(LocalizedContentManager), "_currentLangCode"); + + // remember previous settings + LanguageCode previousCode = codeField.GetValue(); + string previousOverride = this.LanguageCodeOverride; + + // create locale => code map + IDictionary map = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.LanguageCodeOverride = null; + foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + { + codeField.SetValue(code); + map[this.GetKeyLocale.Invoke()] = code; + } + + // restore previous settings + codeField.SetValue(previousCode); + this.LanguageCodeOverride = previousOverride; + + return map; + } + /// Parse a cache key into its component parts. /// The input cache key. /// The original asset key. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0e1930ac..79f8e801 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; @@ -806,33 +806,39 @@ namespace StardewModdingAPI } } - // reset cache when needed - // only register listeners after Entry to avoid repeatedly reloading assets during load + // invalidate cache entries when needed + // (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.) foreach (IModMetadata metadata in loadedMods) { if (metadata.Mod.Helper.Content is ContentHelper helper) { - // TODO: optimise by only reloading assets the new editors/loaders can intercept helper.ObservableAssetEditors.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset editor, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => { if (e.NewItems.Count > 0) { - this.Monitor.Log("Detected new asset loader, resetting cache...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray()); } }; } } - this.Monitor.Log("Resetting cache to enable interception...", LogLevel.Trace); - this.ContentManager.InvalidateCache((key, type) => true); + + // reset cache now if any editors or loaders were added during entry + IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray(); + IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray(); + if (editors.Any() || loaders.Any()) + { + this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); + this.ContentManager.InvalidateCacheFor(editors, loaders); + } } /// Reload translations for all mods. -- cgit From 723ddc255e1c2b399dfb734306fd00912a741e62 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 17 Aug 2017 17:46:45 -0400 Subject: break loops when loading assets through a mod loader --- src/StardewModdingAPI/Framework/Countdown.cs | 44 --------------- src/StardewModdingAPI/Framework/SContentManager.cs | 22 ++++++-- src/StardewModdingAPI/Framework/SGame.cs | 5 +- .../Framework/Utilities/ContextHash.cs | 62 ++++++++++++++++++++++ .../Framework/Utilities/Countdown.cs | 44 +++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 5 +- 6 files changed, 130 insertions(+), 52 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Countdown.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/ContextHash.cs create mode 100644 src/StardewModdingAPI/Framework/Utilities/Countdown.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/src/StardewModdingAPI/Framework/Countdown.cs b/src/StardewModdingAPI/Framework/Countdown.cs deleted file mode 100644 index 25ca2546..00000000 --- a/src/StardewModdingAPI/Framework/Countdown.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace StardewModdingAPI.Framework -{ - /// Counts down from a baseline value. - internal class Countdown - { - /********* - ** Accessors - *********/ - /// The initial value from which to count down. - public int Initial { get; } - - /// The current value. - public int Current { get; private set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The initial value from which to count down. - public Countdown(int initial) - { - this.Initial = initial; - this.Current = initial; - } - - /// Reduce the current value by one. - /// Returns whether the value was decremented (i.e. wasn't already zero). - public bool Decrement() - { - if (this.Current <= 0) - return false; - - this.Current--; - return true; - } - - /// Restart the countdown. - public void Reset() - { - this.Current = this.Initial; - } - } -} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 9e086870..25775291 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -9,6 +9,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Metadata; using StardewValley; @@ -44,6 +45,9 @@ namespace StardewModdingAPI.Framework /// Provides metadata for core game assets. private readonly CoreAssets CoreAssets; + /// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset. + private readonly ContextHash AssetsBeingLoaded = new ContextHash(); + /********* ** Accessors @@ -139,11 +143,21 @@ namespace StardewModdingAPI.Framework // load asset T data; + if (this.AssetsBeingLoaded.Contains(assetName)) { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); - asset = this.ApplyEditors(info, asset); - data = (T)asset.Data; + this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn); + this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace); + data = base.Load(assetName); + } + else + { + data = this.AssetsBeingLoaded.Track(assetName, () => + { + IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader(info) ?? new AssetDataForObject(info, base.Load(assetName), this.NormaliseAssetName); + asset = this.ApplyEditors(info, asset); + return (T)asset.Data; + }); } // update cache & return data diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 755f0274..997e0c8c 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; @@ -322,7 +323,7 @@ namespace StardewModdingAPI.Framework #if !SMAPI_1_x if (Game1.dayOfMonth != 0) // wait until new-game intro finishes (world not fully initialised yet) #endif - this.AfterLoadTimer--; + this.AfterLoadTimer--; if (this.AfterLoadTimer == 0) { diff --git a/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs new file mode 100644 index 00000000..0d8487bb --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/ContextHash.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + /// A wrapper meant for tracking recursive contexts. + /// The key type. + internal class ContextHash : HashSet + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public ContextHash() { } + + /// Construct an instance. + /// The implementation to use when comparing values in the set, or null to use the default comparer for the set type. + public ContextHash(IEqualityComparer comparer) + : base(comparer) { } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The key to add. + /// The action to perform. + /// The specified key is already added. + public void Track(T key, Action action) + { + if(this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + action(); + } + finally + { + this.Remove(key); + } + } + + /// Add a key while an action is in progress, and remove it when it completes. + /// The value type returned by the method. + /// The key to add. + /// The action to perform. + public TResult Track(T key, Func action) + { + if (this.Contains(key)) + throw new InvalidOperationException($"Can't track context for key {key} because it's already added."); + + this.Add(key); + try + { + return action(); + } + finally + { + this.Remove(key); + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Utilities/Countdown.cs b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs new file mode 100644 index 00000000..921a35ce --- /dev/null +++ b/src/StardewModdingAPI/Framework/Utilities/Countdown.cs @@ -0,0 +1,44 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + /// Counts down from a baseline value. + internal class Countdown + { + /********* + ** Accessors + *********/ + /// The initial value from which to count down. + public int Initial { get; } + + /// The current value. + public int Current { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial value from which to count down. + public Countdown(int initial) + { + this.Initial = initial; + this.Current = initial; + } + + /// Reduce the current value by one. + /// Returns whether the value was decremented (i.e. wasn't already zero). + public bool Decrement() + { + if (this.Current <= 0) + return false; + + this.Current--; + return true; + } + + /// Restart the countdown. + public void Reset() + { + this.Current = this.Initial; + } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d7e10ca5..73112983 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -1,4 +1,4 @@ - + @@ -91,6 +91,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -126,7 +127,7 @@ - + -- cgit From 80fe706f19d95ef2d00887344bcbc2a064a04541 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 21 Aug 2017 14:22:19 -0400 Subject: show friendlier error when players have two copies of a mod --- release-notes.md | 1 + .../Exceptions/SAssemblyLoadFailedException.cs | 16 +++++++++++++++ .../Framework/ModLoading/AssemblyLoadStatus.cs | 15 ++++++++++++++ .../Framework/ModLoading/AssemblyLoader.cs | 24 ++++++++++++++++------ .../Framework/ModLoading/AssemblyParseResult.cs | 9 ++++++-- src/StardewModdingAPI/Program.cs | 6 ++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 2 ++ 7 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs create mode 100644 src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs (limited to 'src/StardewModdingAPI/Framework') diff --git a/release-notes.md b/release-notes.md index 803f7354..7b7e250c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ For players: * The SMAPI console is now much simpler and easier to read. * The SMAPI console now adjusts its colors when you have a light terminal background. * Updated compatibility list. +* Improved errors when a mod DLL can't be loaded. For mod developers: * Added new APIs to edit, inject, and reload XNB assets loaded by the game at any time. diff --git a/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs new file mode 100644 index 00000000..ec9279f1 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SAssemblyLoadFailedException.cs @@ -0,0 +1,16 @@ +using System; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An exception thrown when an assembly can't be loaded by SMAPI, with all the relevant details in the message. + internal class SAssemblyLoadFailedException : Exception + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + public SAssemblyLoadFailedException(string message) + : base(message) { } + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs new file mode 100644 index 00000000..11be19fc --- /dev/null +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoadStatus.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates the result of an assembly load. + internal enum AssemblyLoadStatus + { + /// The assembly was loaded successfully. + Okay = 1, + + /// The assembly could not be loaded. + Failed = 2, + + /// The assembly is already loaded. + AlreadyLoaded = 3 + } +} diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs index 406d49e1..b14ae56f 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyLoader.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -6,6 +6,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Framework.Exceptions; namespace StardewModdingAPI.Framework.ModLoading { @@ -65,16 +66,27 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyDefinitionResolver resolver = new AssemblyDefinitionResolver(); HashSet visitedAssemblyNames = new HashSet(AppDomain.CurrentDomain.GetAssemblies().Select(p => p.GetName().Name)); // don't try loading assemblies that are already loaded assemblies = this.GetReferencedLocalAssemblies(new FileInfo(assemblyPath), visitedAssemblyNames, resolver).ToArray(); - if (!assemblies.Any()) - throw new InvalidOperationException($"Could not load '{assemblyPath}' because it doesn't exist."); - resolver.Add(assemblies.Select(p => p.Definition).ToArray()); } + // validate load + if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) + { + throw new SAssemblyLoadFailedException(!File.Exists(assemblyPath) + ? $"Could not load '{assemblyPath}' because it doesn't exist." + : $"Could not load '{assemblyPath}'." + ); + } + if (assemblies[0].Status == AssemblyLoadStatus.AlreadyLoaded) + throw new SAssemblyLoadFailedException($"Could not load '{assemblyPath}' because it was already loaded. Do you have two copies of this mod?"); + // rewrite & load assemblies in leaf-to-root order bool oneAssembly = assemblies.Length == 1; Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { + if (assembly.Status == AssemblyLoadStatus.AlreadyLoaded) + continue; + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible, logPrefix: " "); if (changed) { @@ -143,7 +155,7 @@ namespace StardewModdingAPI.Framework.ModLoading // skip if already visited if (visitedAssemblyNames.Contains(assembly.Name.Name)) - yield break; + yield return new AssemblyParseResult(file, null, AssemblyLoadStatus.AlreadyLoaded); visitedAssemblyNames.Add(assembly.Name.Name); // yield referenced assemblies @@ -155,7 +167,7 @@ namespace StardewModdingAPI.Framework.ModLoading } // yield assembly - yield return new AssemblyParseResult(file, assembly); + yield return new AssemblyParseResult(file, assembly, AssemblyLoadStatus.Okay); } /**** diff --git a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs index 69c99afe..b56a776c 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/AssemblyParseResult.cs @@ -15,6 +15,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly definition. public readonly AssemblyDefinition Definition; + /// The result of the assembly load. + public AssemblyLoadStatus Status; + /********* ** Public methods @@ -22,10 +25,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// Construct an instance. /// The original assembly file. /// The assembly definition. - public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly) + /// The result of the assembly load. + public AssemblyParseResult(FileInfo file, AssemblyDefinition assembly, AssemblyLoadStatus status) { this.File = file; this.Definition = assembly; + this.Status = status; } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 57ff011f..108e9273 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; @@ -655,6 +656,11 @@ namespace StardewModdingAPI #endif continue; } + catch (SAssemblyLoadFailedException ex) + { + TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded: {ex.Message}"); + continue; + } catch (Exception ex) { TrackSkip(metadata, $"its DLL '{manifest.EntryDll}' couldn't be loaded:\n{ex.GetLogSummary()}"); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 73112983..8c7279a1 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -91,6 +91,8 @@ Properties\GlobalAssemblyInfo.cs + + -- cgit