From e54a5d05692f2f5ae852dbd3ed0a247920591418 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 3 Aug 2020 22:35:51 -0400 Subject: update Content Patcher schema for 1.16 --- docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index c6123bac..c139ac1a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,10 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> +## Upcoming release +* For the web UI: + * Updated the JSON validator/schema for Content Patcher 1.16. + ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. -- cgit From 066f1857a145c8b9e80a095d2dee1be6419f957b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Aug 2020 11:56:40 -0400 Subject: fix error when mods add/remove events asynchronously --- docs/release-notes.md | 5 +++- src/SMAPI/Framework/Events/ManagedEvent.cs | 43 ++++++++++++++++++------------ src/SMAPI/PatchMode.cs | 2 +- 3 files changed, 31 insertions(+), 19 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index c139ac1a..7f522ce0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,9 @@ --> ## Upcoming release +* For players: + * Fixed rare error when a mod adds/removes event handlers asynchronously. + * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.16. @@ -16,7 +19,7 @@ Released 02 August 2020 for Stardew Valley 1.4.1 or later. * For players: * Improved compatibility with some Linux terminals (thanks to jlaw and Spatterjaaay!). - * Fixed rare crash when a mod adds/removes an event handler from an event handler. + * Fixed rare error when a mod adds/removes an event handler from an event handler. * Fixed string sorting/comparison for some special characters. * For the Console Commands mod: diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 8b25a9b5..f2dfb2ab 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -70,27 +70,33 @@ namespace StardewModdingAPI.Framework.Events /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { - EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; - var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); + lock (this.Handlers) + { + EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; + var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); - this.Handlers.Add(managedHandler); - this.CachedHandlers = null; - this.HasNewHandlers = true; + this.Handlers.Add(managedHandler); + this.CachedHandlers = null; + this.HasNewHandlers = true; + } } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - // match C# events: if a handler is listed multiple times, remove the last one added - for (int i = this.Handlers.Count - 1; i >= 0; i--) + lock (this.Handlers) { - if (this.Handlers[i].Handler != handler) - continue; + // match C# events: if a handler is listed multiple times, remove the last one added + for (int i = this.Handlers.Count - 1; i >= 0; i--) + { + if (this.Handlers[i].Handler != handler) + continue; - this.Handlers.RemoveAt(i); - this.CachedHandlers = null; - break; + this.Handlers.RemoveAt(i); + this.CachedHandlers = null; + break; + } } } @@ -106,14 +112,17 @@ namespace StardewModdingAPI.Framework.Events // update cached data // (This is debounced here to avoid repeatedly sorting when handlers are added/removed, // and keeping a separate cached list allows changes during enumeration.) - var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event + var handlers = this.CachedHandlers; // iterate local copy in case a mod adds/removes a handler while handling the event, which will set this field to null if (handlers == null) { - if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) - this.Handlers.Sort(); + lock (this.Handlers) + { + if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) + this.Handlers.Sort(); - this.CachedHandlers = handlers = this.Handlers.ToArray(); - this.HasNewHandlers = false; + this.CachedHandlers = handlers = this.Handlers.ToArray(); + this.HasNewHandlers = false; + } } // raise event diff --git a/src/SMAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs index b4286a89..34d3007d 100644 --- a/src/SMAPI/PatchMode.cs +++ b/src/SMAPI/PatchMode.cs @@ -1,4 +1,4 @@ -namespace StardewModdingAPI +namespace StardewModdingAPI { /// Indicates how an image should be patched. public enum PatchMode -- cgit From 48eb5e6be02feae26a6e374992cfeed9d60a5757 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Aug 2020 19:10:54 -0400 Subject: add support for read/writing SDate to JSON --- docs/release-notes.md | 3 +++ src/SMAPI/Utilities/SDate.cs | 5 +++++ 2 files changed, 8 insertions(+) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7f522ce0..2e1e050e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,9 @@ * For players: * Fixed rare error when a mod adds/removes event handlers asynchronously. +* For modders: + * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). + * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.16. diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 03230334..165667a4 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Newtonsoft.Json; using StardewModdingAPI.Framework; using StardewValley; @@ -35,15 +36,18 @@ namespace StardewModdingAPI.Utilities /// The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter). /// This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. ). + [JsonIgnore] public int SeasonIndex { get; } /// The year. public int Year { get; } /// The day of week. + [JsonIgnore] public DayOfWeek DayOfWeek { get; } /// The number of days since the game began (starting at 1 for the first day of spring in Y1). + [JsonIgnore] public int DaysSinceStart { get; } @@ -62,6 +66,7 @@ namespace StardewModdingAPI.Utilities /// The season name. /// The year. /// One of the arguments has an invalid value (like day 35). + [JsonConstructor] public SDate(int day, string season, int year) : this(day, season, year, allowDayZero: false) { } -- cgit From 544919ad1328c0f8b283941b1c2c5c8864a3a84d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 13 Aug 2020 20:01:22 -0400 Subject: remove experimental RewriteInParallel option --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 ++-- .../ModLoading/Framework/RecursiveRewriter.cs | 57 ++++------------------ src/SMAPI/Framework/Models/SConfig.cs | 4 -- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/SMAPI.config.json | 9 ---- 6 files changed, 16 insertions(+), 69 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2e1e050e..2ff4ec0a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For players: * Fixed rare error when a mod adds/removes event handlers asynchronously. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index f8c901e0..dbb5f696 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,10 +76,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod for which the assembly is being loaded. /// The assembly file path. /// Assume the mod is compatible, even if incompatible code is detected. - /// Whether to enable experimental parallel rewriting. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -109,7 +108,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -263,10 +262,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly to rewrite. /// The messages that have already been logged for this mod. /// A string to prefix to log messages. - /// Whether to enable experimental parallel rewriting. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix, bool rewriteInParallel) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -315,7 +313,7 @@ namespace StardewModdingAPI.Framework.ModLoading return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); + bool anyRewritten = rewriter.RewriteModule(); // handle rewrite flags foreach (IInstructionHandler handler in handlers) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 34c78c7d..fb651465 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -57,59 +55,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } /// Rewrite the loaded module code. - /// Whether to enable experimental parallel rewriting. /// Returns whether the module was modified. - public bool RewriteModule(bool rewriteInParallel) + public bool RewriteModule() { IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - // experimental parallel rewriting - // This may cause intermittent startup errors and is disabled by default: https://github.com/Pathoschild/SMAPI/issues/721 - if (rewriteInParallel) - { - int typesChanged = 0; - Exception exception = null; - - Parallel.ForEach(types, type => - { - if (exception != null) - return; - - bool changed = false; - try - { - changed = this.RewriteTypeDefinition(type); - } - catch (Exception ex) - { - exception ??= ex; - } - - if (changed) - Interlocked.Increment(ref typesChanged); - }); + bool changed = false; - return exception == null - ? typesChanged > 0 - : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + try + { + foreach (var type in types) + changed |= this.RewriteTypeDefinition(type); } - - // non-parallel rewriting + catch (Exception ex) { - bool changed = false; - - try - { - foreach (var type in types) - changed |= this.RewriteTypeDefinition(type); - } - catch (Exception ex) - { - throw new Exception($"Rewriting {this.Module.Name} failed.", ex); - } - - return changed; + throw new Exception($"Rewriting {this.Module.Name} failed.", ex); } + + return changed; } diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index 1c682f96..3a3f6960 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -16,7 +16,6 @@ namespace StardewModdingAPI.Framework.Models { [nameof(CheckForUpdates)] = true, [nameof(ParanoidWarnings)] = Constants.IsDebugBuild, - [nameof(RewriteInParallel)] = Constants.IsDebugBuild, [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", @@ -41,9 +40,6 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } - /// Whether to enable experimental parallel rewriting. - public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)]; - /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access. public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)]; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 72ef9095..fd8d7034 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -337,8 +337,6 @@ namespace StardewModdingAPI.Framework // add headers if (this.Settings.DeveloperMode) this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); - if (this.Settings.RewriteInParallel) - this.Monitor.Log($"You enabled experimental parallel rewriting. This may result in faster startup times, but intermittent startup errors. You can disable it by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Info); if (!this.Settings.CheckForUpdates) this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) @@ -983,7 +981,7 @@ namespace StardewModdingAPI.Framework Assembly modAssembly; try { - modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible, rewriteInParallel: this.Settings.RewriteInParallel); + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); this.ModRegistry.TrackAssemblies(mod, modAssembly); } catch (IncompatibleInstructionException) // details already in trace logs diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 0a6d8372..6ba64fe7 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -33,15 +33,6 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "DeveloperMode": true, - /** - * Whether to enable experimental parallel rewriting when SMAPI is loading mods. This can - * reduce startup time when you have many mods installed, but is experimental and may cause - * intermittent startup errors. - * - * When this is commented out, it'll be true for local debug builds and false otherwise. - */ - //"RewriteInParallel": false, - /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as -- cgit From d6a830f7e84ea656f9967e83d82a568bfd4a2400 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 15 Aug 2020 13:05:32 -0400 Subject: fix broken URL in update alerts for unofficial versions --- docs/release-notes.md | 3 ++- src/SMAPI.Web/Controllers/ModsApiController.cs | 4 ++-- src/SMAPI.Web/Framework/Extensions.cs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2ff4ec0a..73b66a12 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,8 +9,9 @@ ## Upcoming release * For players: - * Fixed rare error when a mod adds/removes event handlers asynchronously. * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. + * Fixed broken URL in update alerts for unofficial versions. + * Fixed rare error when a mod adds/removes event handlers asynchronously. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index cd5b6779..9e90a71c 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -156,7 +156,7 @@ namespace StardewModdingAPI.Web.Controllers // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) - unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}"); + unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) @@ -166,7 +166,7 @@ namespace StardewModdingAPI.Web.Controllers if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}") + ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}") : null; } else diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index 3a246245..5305b142 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Web.Framework /**** ** View helpers ****/ - /// Get a URL with the absolute path for an action method. Unlike , only the specified are added to the URL without merging values from the current HTTP request. + /// Get a URL for an action method. Unlike , only the specified are added to the URL without merging values from the current HTTP request. /// The URL helper to extend. /// The name of the action method. /// The name of the controller. -- cgit From 497192fab2a4310f345bbd473fa42649bb264c57 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 15 Aug 2020 13:17:42 -0400 Subject: tweak update alert rules --- docs/release-notes.md | 4 ++++ src/SMAPI.Web/Controllers/ModsApiController.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 73b66a12..5735e388 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ ## Upcoming release * For players: * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. + * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. @@ -19,6 +20,9 @@ * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.16. +* For SMAPI developers: + * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). + ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 9e90a71c..1956bf29 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -198,9 +198,9 @@ namespace StardewModdingAPI.Web.Controllers List updates = new List(); if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) updates.Add(main); - if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease())) + if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease() || search.IsBroken)) updates.Add(optional); - if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: search.IsBroken)) + if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true)) updates.Add(unofficial); if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) updates.Add(unofficialForBeta); -- cgit From d6dc1364bed4eb93bb8d376f9c1dec364ab82881 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 16 Aug 2020 11:28:13 -0400 Subject: update schema for Content Patcher 1.17 --- docs/release-notes.md | 2 +- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5735e388..e6679a5e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,7 +18,7 @@ * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). * For the web UI: - * Updated the JSON validator/schema for Content Patcher 1.16. + * Updated the JSON validator/schema for Content Patcher 1.16 and 1.17. * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index f2c06825..a854e16f 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -11,9 +11,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "const": "1.16.0", + "const": "1.17.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.16.0'." + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.17.0'." } }, "ConfigSchema": { @@ -124,6 +124,9 @@ "title": "Enabled", "description": "Whether to apply this patch. Default true. This fields supports immutable tokens (e.g. config tokens) if they return true/false.", "anyOf": [ + { + "type": "boolean" + }, { "type": "string", "enum": [ "true", "false" ] @@ -131,15 +134,18 @@ { "type": "string", "pattern": "\\{\\{[^{}]+\\}\\}" - }, - { - "type": "boolean" } ], "@errorMessages": { "anyOf": "Invalid value; must be true, false, or a single token which evaluates to true or false." } }, + "Update": { + "title": "Update", + "description": "When the patch should update if it changed. The possible values are 'OnDayStart' and 'OnLocationChange' (defaults to OnDayStart).", + "type": "string", + "enum": [ "OnDayStart", "OnLocationChange" ] + }, "FromFile": { "title": "Source file", "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin or .tmx (map), or .xnb file. This field supports tokens and capitalization doesn't matter.", -- cgit From 58fd6c71a2f9ad92879b04554b7443584eb7a267 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 17 Aug 2020 20:36:03 -0400 Subject: update release notes --- docs/release-notes.md | 1 + 1 file changed, 1 insertion(+) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index e6679a5e..2282bc3d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * For players: * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). + * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. -- cgit From 046deb2d56b6d4665280cc5478d9e683ec1d777d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 19:25:57 -0400 Subject: simplify console interception flow The console interceptor now uses a marker in the string (instead of a state field) to track whether the message should intercepted. This makes each write more atomic, so it's less affected by multithreading in some cases. --- docs/release-notes.md | 4 +- .../Logging/ConsoleInterceptionManager.cs | 59 ---------------------- .../Framework/Logging/InterceptingTextWriter.cs | 55 +++++++++++++------- src/SMAPI/Framework/Logging/LogManager.cs | 15 +++--- src/SMAPI/Framework/Monitor.cs | 19 +++---- 5 files changed, 56 insertions(+), 96 deletions(-) delete mode 100644 src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2282bc3d..a65db68c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,11 +9,12 @@ ## Upcoming release * For players: - * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent unpredictable errors when enabled. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. + * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). @@ -23,6 +24,7 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). + * Internal refactoring to simplify future game updates. ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs deleted file mode 100644 index ef42e536..00000000 --- a/src/SMAPI/Framework/Logging/ConsoleInterceptionManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Logging -{ - /// Manages console output interception. - internal class ConsoleInterceptionManager : IDisposable - { - /********* - ** Fields - *********/ - /// The intercepting console writer. - private readonly InterceptingTextWriter Output; - - - /********* - ** Accessors - *********/ - /// The event raised when a message is written to the console directly. - public event Action OnMessageIntercepted; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ConsoleInterceptionManager() - { - // redirect output through interceptor - this.Output = new InterceptingTextWriter(Console.Out); - this.Output.OnMessageIntercepted += line => this.OnMessageIntercepted?.Invoke(line); - Console.SetOut(this.Output); - } - - /// Get an exclusive lock and write to the console output without interception. - /// The action to perform within the exclusive write block. - public void ExclusiveWriteWithoutInterception(Action action) - { - lock (Console.Out) - { - try - { - this.Output.ShouldIntercept = false; - action(); - } - finally - { - this.Output.ShouldIntercept = true; - } - } - } - - /// Release all resources. - public void Dispose() - { - Console.SetOut(this.Output.Out); - this.Output.Dispose(); - } - } -} diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs index 9ca61b59..d99f1dd2 100644 --- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs +++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs @@ -7,18 +7,22 @@ namespace StardewModdingAPI.Framework.Logging /// A text writer which allows intercepting output. internal class InterceptingTextWriter : TextWriter { + /********* + ** Fields + *********/ + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar; + + /********* ** Accessors *********/ /// The underlying console output. public TextWriter Out { get; } - /// The character encoding in which the output is written. + /// public override Encoding Encoding => this.Out.Encoding; - /// Whether to intercept console output. - public bool ShouldIntercept { get; set; } - /// The event raised when a message is written to the console directly. public event Action OnMessageIntercepted; @@ -28,36 +32,53 @@ namespace StardewModdingAPI.Framework.Logging *********/ /// Construct an instance. /// The underlying output writer. - public InterceptingTextWriter(TextWriter output) + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + public InterceptingTextWriter(TextWriter output, char ignoreChar) { this.Out = output; + this.IgnoreChar = ignoreChar; } - /// Writes a subarray of characters to the text string or stream. - /// The character array to write data from. - /// The character position in the buffer at which to start retrieving data. - /// The number of characters to write. + /// public override void Write(char[] buffer, int index, int count) { - if (this.ShouldIntercept) - this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); - else + if (buffer.Length == 0) this.Out.Write(buffer, index, count); + else if (buffer[0] == this.IgnoreChar) + this.Out.Write(buffer, index + 1, count - 1); + else if (this.IsEmptyOrNewline(buffer)) + this.Out.Write(buffer, index, count); + else + this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n')); } - /// Writes a character to the text string or stream. - /// The character to write to the text stream. - /// Console log messages from the game should be caught by . This method passes through anything that bypasses that method for some reason, since it's better to show it to users than hide it from everyone. + /// public override void Write(char ch) { this.Out.Write(ch); } - /// Releases the unmanaged resources used by the and optionally releases the managed resources. - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + /// protected override void Dispose(bool disposing) { this.OnMessageIntercepted = null; } + + + /********* + ** Private methods + *********/ + /// Get whether a buffer represents a line break. + /// The buffer to check. + private bool IsEmptyOrNewline(char[] buffer) + { + foreach (char ch in buffer) + { + if (ch != '\n' && ch != '\r') + return false; + } + + return true; + } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 3786e940..d0936f3f 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -21,8 +21,8 @@ namespace StardewModdingAPI.Framework.Logging /// The log file to which to write messages. private readonly LogFileManager LogFile; - /// Manages console output interception. - private readonly ConsoleInterceptionManager InterceptionManager = new ConsoleInterceptionManager(); + /// Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar = '\u200B'; /// Get a named monitor instance. private readonly Func GetMonitorImpl; @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Logging public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode) { // init construction logic - this.GetMonitorImpl = name => new Monitor(name, this.InterceptionManager, this.LogFile, colorConfig, isVerbose) + this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, @@ -99,8 +99,10 @@ namespace StardewModdingAPI.Framework.Logging this.MonitorForGame = this.GetMonitor("game"); // redirect direct console output - if (this.MonitorForGame.WriteToConsole) - this.InterceptionManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar); + if (writeToConsole) + output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); + Console.SetOut(output); } /// Get a monitor instance derived from SMAPI's current settings. @@ -167,7 +169,7 @@ namespace StardewModdingAPI.Framework.Logging public void PressAnyKeyToExit(bool showMessage) { if (showMessage) - Console.WriteLine("Game has ended. Press any key to exit."); + this.Monitor.Log("Game has ended. Press any key to exit."); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); @@ -339,7 +341,6 @@ namespace StardewModdingAPI.Framework.Logging /// public void Dispose() { - this.InterceptionManager.Dispose(); this.LogFile.Dispose(); } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 44eeabe6..527cba64 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -18,8 +18,8 @@ namespace StardewModdingAPI.Framework /// Handles writing text to the console. private readonly IConsoleWriter ConsoleWriter; - /// Manages access to the console output. - private readonly ConsoleInterceptionManager ConsoleInterceptor; + /// Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.) + private readonly char IgnoreChar; /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -52,11 +52,11 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. - /// Intercepts access to the console output. + /// A character which indicates the message should not be intercepted if it appears as the first character of a string written to the console. The character itself is not logged in that case. /// The log file to which to write messages. /// The colors to use for text written to the SMAPI console. /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. - public Monitor(string source, ConsoleInterceptionManager consoleInterceptor, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) + public Monitor(string source, char ignoreChar, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); - this.ConsoleInterceptor = consoleInterceptor; + this.IgnoreChar = ignoreChar; this.IsVerbose = isVerbose; } @@ -99,7 +99,7 @@ namespace StardewModdingAPI.Framework internal void Newline() { if (this.WriteToConsole) - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(Console.WriteLine); + Console.WriteLine(); this.LogFile.WriteLine(""); } @@ -136,12 +136,7 @@ namespace StardewModdingAPI.Framework // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) - { - this.ConsoleInterceptor.ExclusiveWriteWithoutInterception(() => - { - this.ConsoleWriter.WriteLine(consoleMessage, level); - }); - } + this.ConsoleWriter.WriteLine(this.IgnoreChar + consoleMessage, level); // write to log file this.LogFile.WriteLine(fullMessage); -- cgit From 94b8262692d2452e77d57fa22046dded231cdb0a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 20:11:56 -0400 Subject: add heuristic field-to-property rewriter --- docs/release-notes.md | 3 +- .../ModLoading/Framework/BaseInstructionHandler.cs | 2 +- .../Rewriters/FieldToPropertyRewriter.cs | 63 ++++++++++++---------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 ++ 4 files changed, 42 insertions(+), 29 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index a65db68c..3f7d5198 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,12 +9,13 @@ ## Upcoming release * For players: - * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. + * Added heuristic compatibility rewrites for some common cases. That fixes some mods which previously broke on Android or in newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. + * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index fde37d68..611b2cf2 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Protected methods *********/ /// Construct an instance. - /// A brief noun phrase indicating what the handler matches. + /// A brief noun phrase indicating what the handler matches, used if is empty. protected BaseInstructionHandler(string defaultPhrase) { this.DefaultPhrase = defaultPhrase; diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index c3b5854e..514691cf 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,47 +1,33 @@ using System; +using System.Collections.Generic; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { - /// Rewrites field references into property references. + /// Rewrites references to fields which no longer exist, but which have an equivalent property with the exact same name. internal class FieldToPropertyRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// The type containing the field to which references should be rewritten. - private readonly Type Type; - - /// The field name to which references should be rewritten. - private readonly string FromFieldName; - - /// The new property name. - private readonly string ToPropertyName; + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; /********* ** Public methods *********/ /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - /// The property name (if different). - public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") + /// The assembly names to which to rewrite broken references. + public FieldToPropertyRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "field changed to property") // ignored since we specify phrases { - this.Type = type; - this.FromFieldName = fieldName; - this.ToPropertyName = propertyName; + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); } - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - public FieldToPropertyRewriter(Type type, string fieldName) - : this(type, fieldName, fieldName) { } - /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. @@ -52,14 +38,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // get field ref FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) + if (fieldRef == null || !this.ShouldValidate(fieldRef.DeclaringType)) + return false; + + // skip if not broken + if (fieldRef.Resolve() != null) + return false; + + // get equivalent property + PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld + ? property?.GetMethod + : property?.SetMethod; + if (method == null) return false; - // replace with property - string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.ToPropertyName}")); + // rewrite field to property + MethodReference propertyRef = module.ImportReference(method); replaceWith(cil.Create(OpCodes.Call, propertyRef)); + this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} (field => property)"); return this.MarkRewritten(); } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 79d7a7a8..fca809f8 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -31,6 +31,9 @@ namespace StardewModdingAPI.Metadata /**** ** rewrite CIL to fix incompatible code ****/ + // generic rewrites + yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); + // rewrite for crossplatform compatibility if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); -- cgit From 1bd67baae116b0307b351222b056a0615107eb3c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 21:39:50 -0400 Subject: support mapping fields to a different type in FieldReplaceRewriter --- docs/release-notes.md | 4 ++++ .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 22 ++++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3f7d5198..efab21d5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,10 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). + * Internal refactoring to simplify game updates: + * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. + * `FieldToPropertyRewriter` now auto-rewrites broken field references into properties if possible, so we no longer need to map fields manually. + * `FieldReplaceRewriter` now supports mapping to a different target type. * Internal refactoring to simplify future game updates. ## 3.6.2 diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index 8043b13a..9166ab86 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -26,17 +26,27 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Public methods *********/ /// Construct an instance. - /// The type whose field to rewrite. + /// The type whose field to rewrite. /// The field name to rewrite. + /// The new type which will have the field. /// The new field name to reference. - public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : base(defaultPhrase: $"{type.FullName}.{fromFieldName} field") + public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName) + : base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field") { - this.Type = type; + this.Type = fromType; this.FromFieldName = fromFieldName; - this.ToField = type.GetField(toFieldName); + this.ToField = toType.GetField(toFieldName); if (this.ToField == null) - throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); + throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field."); + } + + /// Construct an instance. + /// The type whose field to rewrite. + /// The field name to rewrite. + /// The new field name to reference. + public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) + : this(type, fromFieldName, type, toFieldName) + { } /// Rewrite a CIL instruction reference if needed. -- cgit From 3a890408760d0d38a418d9830374262043e2ba13 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Aug 2020 22:16:48 -0400 Subject: add rewriter for method references with missing optional parameters --- docs/release-notes.md | 5 +- .../MethodWithMissingOptionalParameterRewriter.cs | 113 +++++++++++++++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 7 +- 3 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index efab21d5..7e928aed 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites for some common cases. That fixes some mods which previously broke on Android or in newer game versions. + * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed broken URL in update alerts for unofficial versions. @@ -27,7 +27,8 @@ * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). * Internal refactoring to simplify game updates: * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. - * `FieldToPropertyRewriter` now auto-rewrites broken field references into properties if possible, so we no longer need to map fields manually. + * Added rewriter for any method broken due to new optional parameters. + * Added rewriter for any field which was replaced by a property. * `FieldReplaceRewriter` now supports mapping to a different target type. * Internal refactoring to simplify future game updates. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs new file mode 100644 index 00000000..9db3c3fd --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodWithMissingOptionalParameterRewriter.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites references to methods which only broke because the definition has new optional parameters. + internal class MethodWithMissingOptionalParameterRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// The assembly names to which to rewrite broken references. + private readonly HashSet RewriteReferencesToAssemblies; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The assembly names to which to rewrite broken references. + public MethodWithMissingOptionalParameterRewriter(string[] rewriteReferencesToAssemblies) + : base(defaultPhrase: "methods with missing parameters") // ignored since we specify phrases + { + this.RewriteReferencesToAssemblies = new HashSet(rewriteReferencesToAssemblies); + } + + /// Rewrite a CIL instruction reference if needed. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + { + // get method ref + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef == null || !this.ShouldValidate(methodRef.DeclaringType)) + return false; + + // skip if not broken + if (methodRef.Resolve() != null) + return false; + + // get type + var type = methodRef.DeclaringType.Resolve(); + if (type == null) + return false; + + // get method definition + MethodDefinition method = null; + foreach (var match in type.Methods.Where(p => p.Name == methodRef.Name)) + { + // reference matches initial parameters of definition + if (methodRef.Parameters.Count >= match.Parameters.Count || !this.InitialParametersMatch(methodRef, match)) + continue; + + // all remaining parameters in definition are optional + if (!match.Parameters.Skip(methodRef.Parameters.Count).All(p => p.IsOptional)) + continue; + + method = match; + break; + } + if (method == null) + return false; + + // add extra parameters + foreach (ParameterDefinition parameter in method.Parameters.Skip(methodRef.Parameters.Count)) + { + methodRef.Parameters.Add(new ParameterDefinition( + name: parameter.Name, + attributes: parameter.Attributes, + parameterType: module.ImportReference(parameter.ParameterType) + )); + } + + this.Phrases.Add($"{methodRef.DeclaringType.Name}.{methodRef.Name} (added missing optional parameters)"); + return this.MarkRewritten(); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Get whether every parameter in the method reference matches the exact order and type of the parameters in the method definition. This ignores extra parameters in the definition. + /// The method reference whose parameters to check. + /// The method definition whose parameters to check against. + private bool InitialParametersMatch(MethodReference methodRef, MethodDefinition method) + { + if (methodRef.Parameters.Count > method.Parameters.Count) + return false; + + for (int i = 0; i < methodRef.Parameters.Count; i++) + { + if (!RewriteHelper.IsSameType(methodRef.Parameters[i].ParameterType, method.Parameters[i].ParameterType)) + return false; + } + + return true; + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index fca809f8..972ed91d 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -31,9 +31,6 @@ namespace StardewModdingAPI.Metadata /**** ** rewrite CIL to fix incompatible code ****/ - // generic rewrites - yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); - // rewrite for crossplatform compatibility if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); @@ -41,6 +38,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); + // generic rewrites + yield return new FieldToPropertyRewriter(this.ValidateReferencesToAssemblies); + yield return new MethodWithMissingOptionalParameterRewriter(this.ValidateReferencesToAssemblies); + #if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); -- cgit From 54e7fb7a0bcd994f6d49348c879cb96902dbe07b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 22:32:59 -0400 Subject: fix some broken field references not detected --- docs/release-notes.md | 1 + .../Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 7e928aed..21f9d213 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. + * Fixed SMAPI not always detecting broken field references in mod code. * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. * For modders: diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index 9afd1de0..75575c97 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -35,8 +34,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); if (fieldRef != null && this.ShouldValidate(fieldRef.DeclaringType)) { - FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); - if (target == null) + FieldDefinition target = fieldRef.Resolve(); + if (target == null || target.HasConstant) { this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); return false; -- cgit From 0bf692addc3e309a8448de9ffb2a41cb701cfddf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 26 Aug 2020 23:11:41 -0400 Subject: add heuristic rewrite for field => const changes --- docs/release-notes.md | 12 ++-- .../ModLoading/Framework/RewriteHelper.cs | 18 ++++++ .../ModLoading/Rewriters/HeuristicFieldRewriter.cs | 65 +++++++++++++++++----- .../Rewriters/HeuristicMethodRewriter.cs | 20 +------ .../Rewriters/StaticFieldToConstantRewriter.cs | 65 ---------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 3 - 6 files changed, 76 insertions(+), 107 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 21f9d213..554b9518 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -26,12 +26,12 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). - * Internal refactoring to simplify game updates: - * Reorganised SMAPI core to reduce coupling to `Game1` and make it easier to navigate. - * Added rewriter for any method broken due to new optional parameters. - * Added rewriter for any field which was replaced by a property. - * `FieldReplaceRewriter` now supports mapping to a different target type. - * Internal refactoring to simplify future game updates. + * Reorganised SMAPI core to reduce coupling to `Game1`, make it easier to navigate, and simplify future game updates. + * SMAPI now automatically fixes code broken by these changes in game code, so manual rewriters are no longer needed: + * reference to a method with new optional parameters; + * reference to a field replaced by a property; + * reference to a field replaced by a `const` field. + * `FieldReplaceRewriter` now supports mapping to a different target type. ## 3.6.2 Released 02 August 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 36058b86..4b88148f 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -59,6 +59,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework : null; } + /// Get the CIL instruction to load a value onto the stack. + /// The constant value to inject. + /// Returns the instruction, or null if the value type isn't supported. + public static Instruction GetLoadValueInstruction(object rawValue) + { + return rawValue switch + { + null => Instruction.Create(OpCodes.Ldnull), + bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), + int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 + long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 + float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 + double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 + string value => Instruction.Create(OpCodes.Ldstr, value), + _ => null + }; + } + /// Get whether a type matches a type reference. /// The defined type. /// The type reference. diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs index 5a088ed8..ca04205c 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicFieldRewriter.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { - /// Automatically fix references to fields that have been replaced by a property. + /// Automatically fix references to fields that have been replaced by a property or const field. internal class HeuristicFieldRewriter : BaseInstructionHandler { /********* @@ -36,14 +36,40 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return false; // skip if not broken - if (fieldRef.Resolve() != null) + FieldDefinition fieldDefinition = fieldRef.Resolve(); + if (fieldDefinition != null && !fieldDefinition.HasConstant) return false; + // rewrite if possible + TypeDefinition declaringType = fieldRef.DeclaringType.Resolve(); + bool isRead = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld; + return + this.TryRewriteToProperty(module, instruction, fieldRef, declaringType, isRead) + || this.TryRewriteToConstField(instruction, fieldDefinition); + } + + + /********* + ** Private methods + *********/ + /// Whether references to the given type should be validated. + /// The type reference. + private bool ShouldValidate(TypeReference type) + { + return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + } + + /// Try rewriting the field into a matching property. + /// The assembly module containing the instruction. + /// The CIL instruction to rewrite. + /// The field reference. + /// The type on which the field was defined. + /// Whether the field is being read; else it's being written to. + private bool TryRewriteToProperty(ModuleDefinition module, Instruction instruction, FieldReference fieldRef, TypeDefinition declaringType, bool isRead) + { // get equivalent property - PropertyDefinition property = fieldRef.DeclaringType.Resolve().Properties.FirstOrDefault(p => p.Name == fieldRef.Name); - MethodDefinition method = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld - ? property?.GetMethod - : property?.SetMethod; + PropertyDefinition property = declaringType.Properties.FirstOrDefault(p => p.Name == fieldRef.Name); + MethodDefinition method = isRead ? property?.GetMethod : property?.SetMethod; if (method == null) return false; @@ -55,15 +81,26 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return this.MarkRewritten(); } - - /********* - ** Private methods - *********/ - /// Whether references to the given type should be validated. - /// The type reference. - private bool ShouldValidate(TypeReference type) + /// Try rewriting the field into a matching const field. + /// The CIL instruction to rewrite. + /// The field definition. + private bool TryRewriteToConstField(Instruction instruction, FieldDefinition field) { - return type != null && this.RewriteReferencesToAssemblies.Contains(type.Scope.Name); + // must have been a static field read, and the new field must be const + if (instruction.OpCode != OpCodes.Ldsfld || field?.HasConstant != true) + return false; + + // get opcode for value type + Instruction loadInstruction = RewriteHelper.GetLoadValueInstruction(field.Constant); + if (loadInstruction == null) + return false; + + // rewrite to constant + instruction.OpCode = loadInstruction.OpCode; + instruction.Operand = loadInstruction.Operand; + + this.Phrases.Add($"{field.DeclaringType.Name}.{field.Name} (field => const)"); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs index 21b42e12..e133b6fa 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/HeuristicMethodRewriter.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters // get instructions to inject parameter values var loadInstructions = method.Parameters.Skip(methodRef.Parameters.Count) - .Select(p => this.GetLoadValueInstruction(p.Constant)) + .Select(p => RewriteHelper.GetLoadValueInstruction(p.Constant)) .ToArray(); if (loadInstructions.Any(p => p == null)) return false; // SMAPI needs to load the value onto the stack before the method call, but the optional parameter type wasn't recognized @@ -105,23 +105,5 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters return true; } - - /// Get the CIL instruction to load a value onto the stack. - /// The constant value to inject. - /// Returns the instruction, or null if the value type isn't supported. - private Instruction GetLoadValueInstruction(object rawValue) - { - return rawValue switch - { - null => Instruction.Create(OpCodes.Ldnull), - bool value => Instruction.Create(value ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0), - int value => Instruction.Create(OpCodes.Ldc_I4, value), // int32 - long value => Instruction.Create(OpCodes.Ldc_I8, value), // int64 - float value => Instruction.Create(OpCodes.Ldc_R4, value), // float32 - double value => Instruction.Create(OpCodes.Ldc_R8, value), // float64 - string value => Instruction.Create(OpCodes.Ldstr, value), - _ => null - }; - } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs deleted file mode 100644 index f34d4943..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Framework; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites static field references into constant values. - /// The constant value type. - internal class StaticFieldToConstantRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The type containing the field to which references should be rewritten. - private readonly Type Type; - - /// The field name to which references should be rewritten. - private readonly string FromFieldName; - - /// The constant value to replace with. - private readonly TValue Value; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The type whose field to which references should be rewritten. - /// The field name to rewrite. - /// The constant value to replace with. - public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) - : base(defaultPhrase: $"{type.FullName}.{fieldName} field") - { - this.Type = type; - this.FromFieldName = fieldName; - this.Value = value; - } - - /// - public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction) - { - // get field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) - return false; - - // rewrite to constant - if (typeof(TValue) == typeof(int)) - { - instruction.OpCode = OpCodes.Ldc_I4; - instruction.Operand = this.Value; - } - else if (typeof(TValue) == typeof(string)) - { - instruction.OpCode = OpCodes.Ldstr; - instruction.Operand = this.Value; - } - else - throw new NotSupportedException($"Rewriting to constant values of type {typeof(TValue)} isn't currently supported."); - - return this.MarkRewritten(); - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 86e16e1e..09a199f9 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -35,9 +35,6 @@ namespace StardewModdingAPI.Metadata if (platformChanged) yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); - // rewrite for Stardew Valley 1.3 - yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); - // heuristic rewrites yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies); yield return new HeuristicMethodRewriter(this.ValidateReferencesToAssemblies); -- cgit From a1e1b7d10393dc23fa306ef03e392aa03e0b9ee1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 28 Aug 2020 20:03:12 -0400 Subject: fix map tile rotation broken when you return to title --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 554b9518..6e531dbd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). + * Fixed map tile rotation broken when you return to the title screen and reload a save. * Fixed broken URL in update alerts for unofficial versions. * Fixed rare error when a mod adds/removes event handlers asynchronously. * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 99a809ad..06d9eac1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -142,6 +142,9 @@ namespace StardewModdingAPI.Framework /// Whether the game is creating the save file and SMAPI has already raised . private bool IsBetweenCreateEvents; + /// Whether the player just returned to the title screen. + private bool JustReturnedToTitle; + /// Asset interceptors added or removed since the last tick. private readonly List ReloadAssetInterceptorsQueue = new List(); @@ -456,9 +459,17 @@ namespace StardewModdingAPI.Framework try { + /********* + ** Safe queued work + *********/ + // print warnings/alerts SCore.DeprecationManager.PrintQueued(); SCore.PerformanceMonitor.PrintQueuedAlerts(); + // reapply overrides + if (this.JustReturnedToTitle && !(Game1.mapDisplayDevice is SDisplayDevice)) + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + /********* ** First-tick initialization *********/ @@ -1039,8 +1050,7 @@ namespace StardewModdingAPI.Framework { // perform cleanup this.Multiplayer.CleanupOnMultiplayerExit(); - if (!(Game1.mapDisplayDevice is SDisplayDevice)) - Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice); + this.JustReturnedToTitle = true; } /// Raised before the game exits. -- cgit From f57feb7319725513fadde8b14d55f4e8e4b82c24 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Sep 2020 20:56:27 -0400 Subject: extend game's input logic instead of replacing it --- docs/release-notes.md | 2 +- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 31 ++--- src/SMAPI/Framework/Input/IInputStateBuilder.cs | 4 - src/SMAPI/Framework/Input/KeyboardStateBuilder.cs | 17 +-- src/SMAPI/Framework/Input/MouseStateBuilder.cs | 39 +++--- src/SMAPI/Framework/Input/SInputState.cs | 157 ++++++++++------------ src/SMAPI/Framework/SCore.cs | 6 +- src/SMAPI/Framework/WatcherCore.cs | 2 +- 8 files changed, 109 insertions(+), 149 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6e531dbd..ae636153 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites, which fix some mods previously incompatible with Android or newer game versions. + * Added heuristic compatibility rewrites. (This fixes some mods previously broken on Android, and improves compatibility with future game updates.) * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed map tile rotation broken when you return to the title screen and reload a save. diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index 2657fd12..f5f2d916 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI.Framework.Input private GamePadState? State; /// The current button states. - private IDictionary ButtonStates; + private readonly IDictionary ButtonStates; /// The left trigger value. private float LeftTrigger; @@ -39,33 +39,26 @@ namespace StardewModdingAPI.Framework.Input ** Accessors *********/ /// Whether the gamepad is currently connected. - public bool IsConnected { get; private set; } + public bool IsConnected { get; } /********* ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public GamePadStateBuilder(GamePadState? state = null) + /// The initial state. + public GamePadStateBuilder(GamePadState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public GamePadStateBuilder Reset(GamePadState? state = null) - { - this.State = state ??= GamePad.GetState(PlayerIndex.One); - this.IsConnected = state.Value.IsConnected; + this.State = state; + this.IsConnected = state.IsConnected; if (!this.IsConnected) - return this; + return; - GamePadDPad pad = state.Value.DPad; - GamePadButtons buttons = state.Value.Buttons; - GamePadTriggers triggers = state.Value.Triggers; - GamePadThumbSticks sticks = state.Value.ThumbSticks; + GamePadDPad pad = state.DPad; + GamePadButtons buttons = state.Buttons; + GamePadTriggers triggers = state.Triggers; + GamePadThumbSticks sticks = state.ThumbSticks; this.ButtonStates = new Dictionary { [SButton.DPadUp] = pad.Up, @@ -89,8 +82,6 @@ namespace StardewModdingAPI.Framework.Input this.RightTrigger = triggers.Right; this.LeftStickPos = sticks.Left; this.RightStickPos = sticks.Right; - - return this; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/IInputStateBuilder.cs b/src/SMAPI/Framework/Input/IInputStateBuilder.cs index 193e5216..28d62439 100644 --- a/src/SMAPI/Framework/Input/IInputStateBuilder.cs +++ b/src/SMAPI/Framework/Input/IInputStateBuilder.cs @@ -12,10 +12,6 @@ namespace StardewModdingAPI.Framework.Input /********* ** Methods *********/ - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - THandler Reset(TState? state = null); - /// Override the states for a set of buttons. /// The button state overrides. THandler OverrideButtons(IDictionary overrides); diff --git a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs index f95a28bf..620ad442 100644 --- a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs +++ b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs @@ -21,23 +21,14 @@ namespace StardewModdingAPI.Framework.Input ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public KeyboardStateBuilder(KeyboardState? state = null) + /// The initial state. + public KeyboardStateBuilder(KeyboardState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public KeyboardStateBuilder Reset(KeyboardState? state = null) - { - this.State = state ??= Keyboard.GetState(); + this.State = state; this.PressedButtons.Clear(); - foreach (var button in state.Value.GetPressedKeys()) + foreach (var button in state.GetPressedKeys()) this.PressedButtons.Add(button); - - return this; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs index 1cc16ca9..a1ac5492 100644 --- a/src/SMAPI/Framework/Input/MouseStateBuilder.cs +++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs @@ -13,51 +13,42 @@ namespace StardewModdingAPI.Framework.Input private MouseState? State; /// The current button states. - private IDictionary ButtonStates; + private readonly IDictionary ButtonStates; /// The mouse wheel scroll value. - private int ScrollWheelValue; + private readonly int ScrollWheelValue; /********* ** Accessors *********/ /// The X cursor position. - public int X { get; private set; } + public int X { get; } /// The Y cursor position. - public int Y { get; private set; } + public int Y { get; } /********* ** Public methods *********/ /// Construct an instance. - /// The initial state, or null to get the latest state. - public MouseStateBuilder(MouseState? state = null) + /// The initial state. + public MouseStateBuilder(MouseState state) { - this.Reset(state); - } - - /// Reset the tracked state. - /// The state from which to reset, or null to get the latest state. - public MouseStateBuilder Reset(MouseState? state = null) - { - this.State = state ??= Mouse.GetState(); + this.State = state; this.ButtonStates = new Dictionary { - [SButton.MouseLeft] = state.Value.LeftButton, - [SButton.MouseMiddle] = state.Value.MiddleButton, - [SButton.MouseRight] = state.Value.RightButton, - [SButton.MouseX1] = state.Value.XButton1, - [SButton.MouseX2] = state.Value.XButton2 + [SButton.MouseLeft] = state.LeftButton, + [SButton.MouseMiddle] = state.MiddleButton, + [SButton.MouseRight] = state.RightButton, + [SButton.MouseX1] = state.XButton1, + [SButton.MouseX2] = state.XButton2 }; - this.X = state.Value.X; - this.Y = state.Value.Y; - this.ScrollWheelValue = state.Value.ScrollWheelValue; - - return this; + this.X = state.X; + this.Y = state.Y; + this.ScrollWheelValue = state.ScrollWheelValue; } /// Override the states for a set of buttons. diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 333f5726..3dfeb152 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -29,21 +29,24 @@ namespace StardewModdingAPI.Framework.Input /// Whether there are new overrides in or that haven't been applied to the previous state. private bool HasNewOverrides; + /// The game tick when the input state was last updated. + private uint? LastUpdateTick; + /********* ** Accessors *********/ - /// The controller state as of the last update. - public GamePadState LastController { get; private set; } + /// The controller state as of the last update, with overrides applied. + public GamePadState ControllerState { get; private set; } - /// The keyboard state as of the last update. - public KeyboardState LastKeyboard { get; private set; } + /// The keyboard state as of the last update, with overrides applied. + public KeyboardState KeyboardState { get; private set; } - /// The mouse state as of the last update. - public MouseState LastMouse { get; private set; } + /// The mouse state as of the last update, with overrides applied. + public MouseState MouseState { get; private set; } /// The buttons which were pressed, held, or released as of the last update. - public IDictionary LastButtonStates { get; private set; } = new Dictionary(); + public IDictionary ButtonStates { get; private set; } = new Dictionary(); /// The cursor position on the screen adjusted for the zoom level. public ICursorPosition CursorPosition => this.CursorPositionImpl; @@ -52,54 +55,26 @@ namespace StardewModdingAPI.Framework.Input /********* ** Public methods *********/ - /// Get a copy of the current state. - public SInputState Clone() - { - return new SInputState - { - LastButtonStates = this.LastButtonStates, - LastController = this.LastController, - LastKeyboard = this.LastKeyboard, - LastMouse = this.LastMouse, - CursorPositionImpl = this.CursorPositionImpl - }; - } - - /// Override the state for a button. - /// The button to override. - /// Whether to mark it pressed; else mark it released. - public void OverrideButton(SButton button, bool setDown) + /// Update the current button states for the given tick. This does nothing if the input has already been updated for this tick (e.g. because SMAPI updated it before the game update). + public override void Update() { - bool changed = setDown - ? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button) - : this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button); + // skip if already updated + if (this.LastUpdateTick == SCore.TicksElapsed) + return; + this.LastUpdateTick = SCore.TicksElapsed; - if (changed) - this.HasNewOverrides = true; - } + // update base state + base.Update(); - /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. - /// The button to check. - public bool IsSuppressed(SButton button) - { - return this.CustomReleasedKeys.Contains(button); - } - - /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. - [Obsolete("This method should only be called by the game itself.")] - public override void Update() { } - - /// Update the current button states for the given tick. - public void TrueUpdate() - { + // update SMAPI extended data try { float zoomMultiplier = (1f / Game1.options.zoomLevel); // get real values - var controller = new GamePadStateBuilder(); - var keyboard = new KeyboardStateBuilder(); - var mouse = new MouseStateBuilder(); + var controller = new GamePadStateBuilder(base.GetGamePadState()); + var keyboard = new KeyboardStateBuilder(base.GetKeyboardState()); + var mouse = new MouseStateBuilder(base.GetMouseState()); Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); @@ -124,18 +99,18 @@ namespace StardewModdingAPI.Framework.Input var pressedButtons = hasOverrides ? new HashSet(this.GetPressedButtons(keyboard, mouse, controller)) : reallyDown; - var activeButtons = this.DeriveStates(this.LastButtonStates, pressedButtons); + var activeButtons = this.DeriveStates(this.ButtonStates, pressedButtons); // update this.HasNewOverrides = false; - this.LastController = controller.GetState(); - this.LastKeyboard = keyboard.GetState(); - this.LastMouse = mouse.GetState(); - this.LastButtonStates = activeButtons; + this.ControllerState = controller.GetState(); + this.KeyboardState = keyboard.GetState(); + this.MouseState = mouse.GetState(); + this.ButtonStates = activeButtons; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; - this.CursorPositionImpl = this.GetCursorPosition(this.LastMouse, cursorAbsolutePos, zoomMultiplier); + this.CursorPositionImpl = this.GetCursorPosition(this.MouseState, cursorAbsolutePos, zoomMultiplier); } } catch (InvalidOperationException) @@ -144,53 +119,67 @@ namespace StardewModdingAPI.Framework.Input } } - /// Apply input overrides to the current state. - public void ApplyOverrides() - { - if (this.HasNewOverrides) - { - var controller = new GamePadStateBuilder(this.LastController); - var keyboard = new KeyboardStateBuilder(this.LastKeyboard); - var mouse = new MouseStateBuilder(this.LastMouse); - - if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) - { - this.LastController = controller.GetState(); - this.LastKeyboard = keyboard.GetState(); - this.LastMouse = mouse.GetState(); - } - } - } - /// Get the gamepad state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override GamePadState GetGamePadState() { - if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) - return new GamePadState(); - - return this.LastController; + return this.ControllerState; } /// Get the keyboard state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override KeyboardState GetKeyboardState() { - return this.LastKeyboard; + return this.KeyboardState; } /// Get the keyboard state visible to the game. - [Obsolete("This method should only be called by the game itself.")] public override MouseState GetMouseState() { - return this.LastMouse; + return this.MouseState; + } + + /// Override the state for a button. + /// The button to override. + /// Whether to mark it pressed; else mark it released. + public void OverrideButton(SButton button, bool setDown) + { + bool changed = setDown + ? this.CustomPressedKeys.Add(button) | this.CustomReleasedKeys.Remove(button) + : this.CustomPressedKeys.Remove(button) | this.CustomReleasedKeys.Add(button); + + if (changed) + this.HasNewOverrides = true; + } + + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.CustomReleasedKeys.Contains(button); + } + + /// Apply input overrides to the current state. + public void ApplyOverrides() + { + if (this.HasNewOverrides) + { + var controller = new GamePadStateBuilder(this.ControllerState); + var keyboard = new KeyboardStateBuilder(this.KeyboardState); + var mouse = new MouseStateBuilder(this.MouseState); + + if (this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, controller, keyboard, mouse)) + { + this.ControllerState = controller.GetState(); + this.KeyboardState = keyboard.GetState(); + this.MouseState = mouse.GetState(); + } + } } /// Get whether a given button was pressed or held. /// The button to check. public bool IsDown(SButton button) { - return this.GetState(this.LastButtonStates, button).IsDown(); + return this.GetState(this.ButtonStates, button).IsDown(); } /// Get whether any of the given buttons were pressed or held. @@ -204,7 +193,7 @@ namespace StardewModdingAPI.Framework.Input /// The button to check. public SButtonState GetState(SButton button) { - return this.GetState(this.LastButtonStates, button); + return this.GetState(this.ButtonStates, button); } @@ -305,7 +294,9 @@ namespace StardewModdingAPI.Framework.Input /// The button to check. private SButtonState GetState(IDictionary activeButtons, SButton button) { - return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None; + return activeButtons.TryGetValue(button, out SButtonState state) + ? state + : SButtonState.None; } /// Get the buttons pressed in the given stats. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index eedbfc64..bfe6e277 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -423,7 +423,7 @@ namespace StardewModdingAPI.Framework private void OnGameInitialized() { // set initial state - this.Input.TrueUpdate(); + this.Input.Update(); // init watchers this.Watchers = new WatcherCore(this.Input, this.Game.GetObservableLocations()); @@ -492,7 +492,7 @@ namespace StardewModdingAPI.Framework // user from doing anything on the overnight shipping screen. SInputState inputState = this.Input; if (this.Game.IsActive) - inputState.TrueUpdate(); + inputState.Update(); /********* ** Special cases @@ -795,7 +795,7 @@ namespace StardewModdingAPI.Framework } // raise input button events - foreach (var pair in inputState.LastButtonStates) + foreach (var pair in inputState.ButtonStates) { SButton button = pair.Key; SButtonState status = pair.Value; diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs index 2a5d1ee6..393f6a37 100644 --- a/src/SMAPI/Framework/WatcherCore.cs +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Framework { // init watchers this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); - this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.LastMouse.ScrollWheelValue); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.MouseState.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); -- cgit From 4088f4cb2bfe777cf6f86ac5fbf64f7d67565057 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 4 Sep 2020 22:02:59 -0400 Subject: simplify error shown for duplicate mods --- docs/release-notes.md | 3 +- src/SMAPI.Tests/Core/ModResolverTests.cs | 22 ++++++------ src/SMAPI/Framework/IModMetadata.cs | 10 +++++- src/SMAPI/Framework/Logging/LogManager.cs | 13 +++++++ src/SMAPI/Framework/ModLoading/ModFailReason.cs | 27 ++++++++++++++ src/SMAPI/Framework/ModLoading/ModMetadata.cs | 14 +++++++- src/SMAPI/Framework/ModLoading/ModResolver.cs | 48 ++++++++++++------------- src/SMAPI/Framework/SCore.cs | 21 ++++++++--- 8 files changed, 116 insertions(+), 42 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/ModFailReason.cs (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index ae636153..297d8394 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,8 +9,9 @@ ## Upcoming release * For players: - * Added heuristic compatibility rewrites. (This fixes some mods previously broken on Android, and improves compatibility with future game updates.) + * Added heuristic compatibility rewrites. (This improves mod compatibility with Android and future game updates.) * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). + * Simplified error shown for duplicate mods. * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed map tile rotation broken when you return to the title screen and reload a save. * Fixed broken URL in update alerts for unofficial versions. diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index 4f3a12cb..78056ef7 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -154,7 +154,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the minimum API version is higher than the current SMAPI version.")] @@ -169,7 +169,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -183,7 +183,7 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); + mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the metadata."); } [Test(Description = "Assert that validation fails when multiple mods have the same unique ID.")] @@ -200,8 +200,8 @@ namespace SMAPI.Tests.Core new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: key => null); // assert - modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); + modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the first mod with a unique ID."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "The validation did not fail the second mod with a unique ID."); } [Test(Description = "Assert that validation fails when the manifest references a DLL that does not exist.")] @@ -367,9 +367,9 @@ namespace SMAPI.Tests.Core Assert.AreEqual(5, mods.Length, 0, "Expected to get the same number of mods input."); Assert.AreSame(modA.Object, mods[0], "The load order is incorrect: mod A should be first since it's needed by mod B."); Assert.AreSame(modB.Object, mods[1], "The load order is incorrect: mod B should be second since it needs mod A."); - modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); - modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); - modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); + modC.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod C was expected to fail since it's part of a dependency loop."); + modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); + modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] @@ -408,7 +408,7 @@ namespace SMAPI.Tests.Core // assert Assert.AreEqual(2, mods.Length, 0, "Expected to get the same number of mods input."); - modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); + modB.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny(), It.IsAny(), It.IsAny()), Times.Once, "Mod B unexpectedly didn't fail even though it needs a newer version of Mod A."); } [Test(Description = "Assert that dependencies are accepted if they meet the minimum version.")] @@ -525,8 +525,8 @@ namespace SMAPI.Tests.Core if (allowStatusChange) { mod - .Setup(p => p.SetStatus(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((status, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{errorDetails}")) + .Setup(p => p.SetStatus(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((status, failReason, message, errorDetails) => Console.WriteLine($"<{manifest.UniqueID} changed status: [{status}] {message}\n{failReason}\n{errorDetails}")) .Returns(mod.Object); } return mod; diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 6a635b76..70cf0036 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -31,6 +31,9 @@ namespace StardewModdingAPI.Framework /// The metadata resolution status. ModMetadataStatus Status { get; } + /// The reason the mod failed to load, if applicable. + ModFailReason? FailReason { get; } + /// Indicates non-error issues with the mod. ModWarning Warnings { get; } @@ -65,12 +68,17 @@ namespace StardewModdingAPI.Framework /********* ** Public methods *********/ + /// Set the mod status to . + /// Return the instance for chaining. + IModMetadata SetStatusFound(); + /// Set the mod status. /// The metadata resolution status. + /// The reason a mod could not be loaded. /// The reason the metadata is invalid, if any. /// A detailed technical message, if any. /// Return the instance for chaining. - IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null); + IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null); /// Set a warning flag for the mod. /// The warning to set. diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index d0936f3f..094dd749 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using StardewModdingAPI.Framework.Commands; +using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Internal.ConsoleWriting; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; @@ -397,10 +398,22 @@ namespace StardewModdingAPI.Framework.Logging if (skippedMods.Any()) { // get logging logic + HashSet loggedDuplicateIds = new HashSet(); void LogSkippedMod(IModMetadata mod) { string message = $" - {mod.DisplayName}{(mod.Manifest?.Version != null ? " " + mod.Manifest.Version.ToString() : "")} because {mod.Error}"; + // handle duplicate mods + // (log first duplicate only, don't show redundant version) + if (mod.FailReason == ModFailReason.Duplicate && mod.HasManifest()) + { + if (!loggedDuplicateIds.Add(mod.Manifest.UniqueID)) + return; // already logged + + message = $" - {mod.DisplayName} because {mod.Error}"; + } + + // log message this.Monitor.Log(message, LogLevel.Error); if (mod.ErrorDetails != null) this.Monitor.Log($" ({mod.ErrorDetails})"); diff --git a/src/SMAPI/Framework/ModLoading/ModFailReason.cs b/src/SMAPI/Framework/ModLoading/ModFailReason.cs new file mode 100644 index 00000000..cd4623e7 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/ModFailReason.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Framework.ModLoading +{ + /// Indicates why a mod could not be loaded. + internal enum ModFailReason + { + /// The mod has been disabled by prefixing its folder with a dot. + DisabledByDotConvention, + + /// Multiple copies of the mod are installed. + Duplicate, + + /// The mod has incompatible code instructions, needs a newer SMAPI version, or is marked 'assume broken' in the SMAPI metadata list. + Incompatible, + + /// The mod's manifest is missing or invalid. + InvalidManifest, + + /// The mod was deemed compatible, but SMAPI failed when it tried to load it. + LoadFailed, + + /// The mod requires other mods which aren't installed, or its dependencies have a circular reference. + MissingDependencies, + + /// The mod is marked obsolete in the SMAPI metadata list. + Obsolete + } +} diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index e793b0cd..18d2b112 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -37,6 +37,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// public ModMetadataStatus Status { get; private set; } + /// + public ModFailReason? FailReason { get; private set; } + /// public ModWarning Warnings { get; private set; } @@ -93,9 +96,18 @@ namespace StardewModdingAPI.Framework.ModLoading } /// - public IModMetadata SetStatus(ModMetadataStatus status, string error = null, string errorDetails = null) + public IModMetadata SetStatusFound() + { + this.SetStatus(ModMetadataStatus.Found, ModFailReason.Incompatible, null); + this.FailReason = null; + return this; + } + + /// + public IModMetadata SetStatus(ModMetadataStatus status, ModFailReason reason, string error, string errorDetails = null) { this.Status = status; + this.FailReason = reason; this.Error = error; this.ErrorDetails = errorDetails; return this; diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs index 8bbeb2a3..08df7b76 100644 --- a/src/SMAPI/Framework/ModLoading/ModResolver.cs +++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs @@ -43,8 +43,13 @@ namespace StardewModdingAPI.Framework.ModLoading ? ModMetadataStatus.Found : ModMetadataStatus.Failed; - yield return new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore) - .SetStatus(status, shouldIgnore ? "disabled by dot convention" : folder.ManifestParseErrorText); + var metadata = new ModMetadata(folder.DisplayName, folder.Directory.FullName, rootPath, manifest, dataRecord, isIgnored: shouldIgnore); + if (shouldIgnore) + metadata.SetStatus(status, ModFailReason.DisabledByDotConvention, "disabled by dot convention"); + else + metadata.SetStatus(status, ModFailReason.InvalidManifest, folder.ManifestParseErrorText); + + yield return metadata; } } @@ -67,7 +72,7 @@ namespace StardewModdingAPI.Framework.ModLoading switch (mod.DataRecord?.Status) { case ModStatus.Obsolete: - mod.SetStatus(ModMetadataStatus.Failed, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Obsolete, $"it's obsolete: {mod.DataRecord.StatusReasonPhrase}"); continue; case ModStatus.AssumeBroken: @@ -97,7 +102,7 @@ namespace StardewModdingAPI.Framework.ModLoading error += $"version newer than {mod.DataRecord.StatusUpperVersion}"; error += " at " + string.Join(" or ", updateUrls); - mod.SetStatus(ModMetadataStatus.Failed, error); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, error); } continue; } @@ -105,7 +110,7 @@ namespace StardewModdingAPI.Framework.ModLoading // validate SMAPI version if (mod.Manifest.MinimumApiVersion?.IsNewerThan(apiVersion) == true) { - mod.SetStatus(ModMetadataStatus.Failed, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Incompatible, $"it needs SMAPI {mod.Manifest.MinimumApiVersion} or later. Please update SMAPI to the latest version to use this mod."); continue; } @@ -117,12 +122,12 @@ namespace StardewModdingAPI.Framework.ModLoading // validate field presence if (!hasDll && !isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has no {nameof(IManifest.EntryDll)} or {nameof(IManifest.ContentPackFor)} field; must specify one."); continue; } if (hasDll && isContentPack) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest sets both {nameof(IManifest.EntryDll)} and {nameof(IManifest.ContentPackFor)}, which are mutually exclusive."); continue; } @@ -132,14 +137,14 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid filename format if (mod.Manifest.EntryDll.Intersect(Path.GetInvalidFileNameChars()).Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest has invalid filename '{mod.Manifest.EntryDll}' for the EntryDLL field."); continue; } // invalid path if (!File.Exists(Path.Combine(mod.DirectoryPath, mod.Manifest.EntryDll))) { - mod.SetStatus(ModMetadataStatus.Failed, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist."); continue; } @@ -147,7 +152,7 @@ namespace StardewModdingAPI.Framework.ModLoading string actualFilename = new DirectoryInfo(mod.DirectoryPath).GetFiles(mod.Manifest.EntryDll).FirstOrDefault()?.Name; if (actualFilename != mod.Manifest.EntryDll) { - mod.SetStatus(ModMetadataStatus.Failed, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its {nameof(IManifest.EntryDll)} value '{mod.Manifest.EntryDll}' doesn't match the actual file capitalization '{actualFilename}'. The capitalization must match for crossplatform compatibility."); continue; } } @@ -158,7 +163,7 @@ namespace StardewModdingAPI.Framework.ModLoading // invalid content pack ID if (string.IsNullOrWhiteSpace(mod.Manifest.ContentPackFor.UniqueID)) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest declares {nameof(IManifest.ContentPackFor)} without its required {nameof(IManifestContentPackFor.UniqueID)} field."); continue; } } @@ -177,14 +182,14 @@ namespace StardewModdingAPI.Framework.ModLoading if (missingFields.Any()) { - mod.SetStatus(ModMetadataStatus.Failed, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its manifest is missing required fields ({string.Join(", ", missingFields)})."); continue; } } // validate ID format if (!PathUtilities.IsSlug(mod.Manifest.UniqueID)) - mod.SetStatus(ModMetadataStatus.Failed, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, "its manifest specifies an invalid ID (IDs must only contain letters, numbers, underscores, periods, or hyphens)."); } // validate IDs are unique @@ -199,13 +204,8 @@ namespace StardewModdingAPI.Framework.ModLoading if (mod.Status == ModMetadataStatus.Failed) continue; // don't replace metadata error - string folderList = string.Join(", ", - from entry in @group - let relativePath = entry.GetRelativePathWithRoot() - orderby relativePath - select $"{relativePath} ({entry.Manifest.Version})" - ); - mod.SetStatus(ModMetadataStatus.Failed, $"you have multiple copies of this mod installed. Found in folders: {folderList}."); + string folderList = string.Join(", ", group.Select(p => p.GetRelativePathWithRoot()).OrderBy(p => p)); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, $"you have multiple copies of this mod installed. To fix this, delete these folders and reinstall the mod: {folderList}."); } } } @@ -298,7 +298,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedModNames.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it requires mods which aren't installed ({string.Join(", ", failedModNames)})."); return states[mod] = ModDependencyStatus.Failed; } } @@ -315,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (failedLabels.Any()) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs newer versions of some mods: {string.Join(", ", failedLabels)}."); return states[mod] = ModDependencyStatus.Failed; } } @@ -338,7 +338,7 @@ namespace StardewModdingAPI.Framework.ModLoading if (states[requiredMod] == ModDependencyStatus.Checking) { sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"its dependencies have a circular reference: {string.Join(" => ", subchain.Select(p => p.DisplayName))} => {requiredMod.DisplayName})."); return states[mod] = ModDependencyStatus.Failed; } @@ -354,7 +354,7 @@ namespace StardewModdingAPI.Framework.ModLoading // failed, which means this mod can't be loaded either case ModDependencyStatus.Failed: sortedMods.Push(mod); - mod.SetStatus(ModMetadataStatus.Failed, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); + mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.MissingDependencies, $"it needs the '{requiredMod.DisplayName}' mod, which couldn't be loaded."); return states[mod] = ModDependencyStatus.Failed; // unexpected status diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index bfe6e277..52b4b9cf 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1339,9 +1339,10 @@ namespace StardewModdingAPI.Framework // load mods foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails)) + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string errorPhrase, out string errorDetails)) { - mod.SetStatus(ModMetadataStatus.Failed, errorPhrase, errorDetails); + failReason ??= ModFailReason.LoadFailed; + mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails); skippedMods.Add(mod); } } @@ -1437,16 +1438,17 @@ namespace StardewModdingAPI.Framework /// Load a given mod. /// The mod to load. /// The mods being loaded. - /// Preprocesses and loads mod assemblies + /// Preprocesses and loads mod assemblies. /// Generates proxy classes to access mod APIs through an arbitrary interface. /// The JSON helper with which to read mods' JSON files. /// The content manager to use for mod content. /// Handles access to SMAPI's internal mod metadata list. /// The mod IDs to ignore when validating update keys. + /// The reason the mod couldn't be loaded, if applicable. /// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable). /// More detailed details about the error intended for developers (if any). /// Returns whether the mod was successfully loaded. - private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out ModFailReason? failReason, out string errorReasonPhrase, out string errorDetails) { errorDetails = null; @@ -1469,6 +1471,7 @@ namespace StardewModdingAPI.Framework if (mod.Status == ModMetadataStatus.Failed) { this.Monitor.Log($" Failed: {mod.Error}"); + failReason = mod.FailReason; errorReasonPhrase = mod.Error; return false; } @@ -1485,6 +1488,7 @@ namespace StardewModdingAPI.Framework .FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID)) ?.DisplayName ?? dependency.UniqueID; errorReasonPhrase = $"it needs the '{dependencyName}' mod, which couldn't be loaded."; + failReason = ModFailReason.MissingDependencies; return false; } } @@ -1502,6 +1506,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry.Add(mod); errorReasonPhrase = null; + failReason = null; return true; } @@ -1524,17 +1529,20 @@ namespace StardewModdingAPI.Framework { string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://smapi.io/mods" }.Where(p => p != null).ToArray(); errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}"; + failReason = ModFailReason.Incompatible; return false; } catch (SAssemblyLoadFailedException ex) { errorReasonPhrase = $"its DLL couldn't be loaded: {ex.Message}"; + failReason = ModFailReason.LoadFailed; return false; } catch (Exception ex) { errorReasonPhrase = "its DLL couldn't be loaded."; errorDetails = $"Error: {ex.GetLogSummary()}"; + failReason = ModFailReason.LoadFailed; return false; } @@ -1543,7 +1551,10 @@ namespace StardewModdingAPI.Framework { // get mod instance if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase)) + { + failReason = ModFailReason.LoadFailed; return false; + } // get content packs IContentPack[] GetContentPacks() @@ -1591,11 +1602,13 @@ namespace StardewModdingAPI.Framework // track mod mod.SetMod(modEntry, translationHelper); this.ModRegistry.Add(mod); + failReason = null; return true; } catch (Exception ex) { errorReasonPhrase = $"initialization failed:\n{ex.GetLogSummary()}"; + failReason = ModFailReason.LoadFailed; return false; } } -- cgit From 220f3bc578399412ecff0ab350b3b94ebe1d7ea0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 Sep 2020 00:51:32 -0400 Subject: set max game version to prepare for upcoming SDV 1.5 update --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 297d8394..e335990b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Fixed rare issue where the console showed incorrect colors when mods wrote to it asynchronously. * Fixed SMAPI not always detecting broken field references in mod code. * Removed the experimental `RewriteInParallel` option added in SMAPI 3.6 (it was already disabled by default). Unfortunately this caused intermittent and unpredictable errors when enabled. + * Internal changes to prepare for upcoming game updates. * For modders: * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index b7977fb7..485e35fa 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -57,7 +57,7 @@ namespace StardewModdingAPI public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); /// The maximum supported version of Stardew Valley. - public static ISemanticVersion MaximumGameVersion { get; } = null; + public static ISemanticVersion MaximumGameVersion { get; } = new GameVersion("1.4.5"); /// The target game platform. public static GamePlatform TargetPlatform { get; } = EarlyConstants.Platform; -- cgit From 4f3d7eaafc056a7a2b17b1657e069eb456f60f52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 5 Sep 2020 15:00:38 -0400 Subject: make PathUtilities available to mods --- docs/release-notes.md | 1 + src/SMAPI/Utilities/PathUtilities.cs | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/SMAPI/Utilities/PathUtilities.cs (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index e335990b..cdd141c3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Internal changes to prepare for upcoming game updates. * For modders: + * Added `PathUtilities` to simplify working with file/asset names. * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). * For the web UI: diff --git a/src/SMAPI/Utilities/PathUtilities.cs b/src/SMAPI/Utilities/PathUtilities.cs new file mode 100644 index 00000000..ea134468 --- /dev/null +++ b/src/SMAPI/Utilities/PathUtilities.cs @@ -0,0 +1,45 @@ +using System.Diagnostics.Contracts; +using ToolkitPathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; + +namespace StardewModdingAPI.Utilities +{ + /// Provides utilities for normalizing file paths. + public static class PathUtilities + { + /********* + ** Public methods + *********/ + /// Get the segments from a path (e.g. /usr/bin/example => usr, bin, and example). + /// The path to split. + /// The number of segments to match. Any additional segments will be merged into the last returned part. + [Pure] + public static string[] GetSegments(string path, int? limit = null) + { + return ToolkitPathUtilities.GetSegments(path, limit); + } + + /// Normalize path separators in a file path. + /// The file path to normalize. + [Pure] + public static string NormalizePathSeparators(string path) + { + return ToolkitPathUtilities.NormalizePathSeparators(path); + } + + /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../). + /// The path to check. + [Pure] + public static bool IsSafeRelativePath(string path) + { + return ToolkitPathUtilities.IsSafeRelativePath(path); + } + + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// The string to check. + [Pure] + public static bool IsSlug(string str) + { + return ToolkitPathUtilities.IsSlug(str); + } + } +} -- cgit From 0b21357e37c900774668fdaf3e83e1e7f7df0c38 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 Sep 2020 16:40:32 -0400 Subject: fix asset propagation for title menu buttons --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 6 ++++++ 2 files changed, 7 insertions(+) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index cdd141c3..b43489d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,7 @@ * For modders: * Added `PathUtilities` to simplify working with file/asset names. * You can now read/write `SDate` values to JSON (e.g. for `config.json`, network mod messages, etc). + * Fixed asset propagation not updating title menu buttons immediately on Linux/Mac. * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.16 and 1.17. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index d7dd9fb9..71199d59 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -563,6 +563,7 @@ namespace StardewModdingAPI.Metadata /// The content manager through which to reload the asset. /// The asset key to reload. /// Returns whether any textures were reloaded. + /// Derived from the constructor and . private bool ReloadTitleButtons(LocalizedContentManager content, string key) { if (Game1.activeClickableMenu is TitleMenu titleMenu) @@ -570,6 +571,11 @@ namespace StardewModdingAPI.Metadata Texture2D texture = content.Load(key); titleMenu.titleButtonsTexture = texture; + titleMenu.backButton.texture = texture; + titleMenu.aboutButton.texture = texture; + titleMenu.languageButton.texture = texture; + foreach (ClickableTextureComponent button in titleMenu.buttons) + button.texture = titleMenu.titleButtonsTexture; foreach (TemporaryAnimatedSprite bird in titleMenu.birds) bird.texture = texture; -- cgit From 2022836b819241842acae1d20c61895025bf364b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 6 Sep 2020 20:59:19 -0400 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 8 +++++--- docs/technical/mod-package.md | 4 +++- src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 8 ++++---- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 8 files changed, 19 insertions(+), 15 deletions(-) (limited to 'docs/release-notes.md') diff --git a/build/common.targets b/build/common.targets index 43bbcba5..021e1fe6 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.6.2 + 3.7.0 SMAPI latest diff --git a/docs/release-notes.md b/docs/release-notes.md index b43489d3..3b8b0f8b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,11 +7,13 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> -## Upcoming release +## 3.7 +Released 07 September 2020 for Stardew Valley 1.4.1 or later. + * For players: * Added heuristic compatibility rewrites. (This improves mod compatibility with Android and future game updates.) * Tweaked the rules for showing update alerts (see _for SMAPI developers_ below for details). - * Simplified error shown for duplicate mods. + * Simplified the error shown for duplicate mods. * Fixed crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute (thanks to spacechase0!). * Fixed map tile rotation broken when you return to the title screen and reload a save. * Fixed broken URL in update alerts for unofficial versions. @@ -31,7 +33,7 @@ * For SMAPI developers: * The web API now returns an update alert in two new cases: any newer unofficial update (previously only shown if the mod was incompatible), and a newer prerelease version if the installed non-prerelease version is broken (previously only shown if the installed version was prerelease). - * Reorganised SMAPI core to reduce coupling to `Game1`, make it easier to navigate, and simplify future game updates. + * Reorganised the SMAPI core to reduce coupling to game types like `Game1`, make it easier to navigate, and simplify future game updates. * SMAPI now automatically fixes code broken by these changes in game code, so manual rewriters are no longer needed: * reference to a method with new optional parameters; * reference to a field replaced by a property; diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 0547b3ff..26ef019f 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -356,7 +356,9 @@ That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in which can be uploaded to NuGet or referenced directly. ## Release notes -### Upcoming release +### 3.2 +Released 07 September 2020. + * Added option to change `Mods` folder path. * Rewrote documentation to make it easier to read. diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index 5061b01b..5e35b7e9 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -1,7 +1,7 @@  StardewModdingAPI.ModBuildConfig - 3.1.0 + 3.2.0 net45 x86 false diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index afb03cec..c0b0799a 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.1.0 + 3.2.0 Build package for SMAPI mods Pathoschild Pathoschild @@ -14,9 +14,9 @@ https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later. - 3.1.0: - - Added support for semantic versioning 2.0. - - 0Harmony.dll is now ignored if the mod references it directly (it's bundled with SMAPI). + 3.2.0: + - Added option to change `Mods` folder path. + - Rewrote documentation to make it easier to read. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 1be55776..368f470c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.6.2", + "Version": "3.7.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.6.2" + "MinimumApiVersion": "3.7.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index c57ac162..8a95a78f 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.6.2", + "Version": "3.7.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.6.2" + "MinimumApiVersion": "3.7.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 485e35fa..858e832f 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -51,7 +51,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.6.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.7.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); -- cgit From f9fac11028354f15d786d5b854608edb10716f79 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 7 Sep 2020 13:05:34 -0400 Subject: Add 'release highlights' links to release notes --- docs/release-notes.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'docs/release-notes.md') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3b8b0f8b..4671ba95 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,7 +8,7 @@ --> ## 3.7 -Released 07 September 2020 for Stardew Valley 1.4.1 or later. +Released 07 September 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/41341767). * For players: * Added heuristic compatibility rewrites. (This improves mod compatibility with Android and future game updates.) @@ -64,7 +64,7 @@ Released 21 June 2020 for Stardew Valley 1.4.1 or later. * Fixed event priority sorting. ## 3.6 -Released 20 June 2020 for Stardew Valley 1.4.1 or later. +Released 20 June 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/38441800). * For players: * Added crossplatform compatibility for mods which use the `[HarmonyPatch(type)]` attribute. @@ -106,7 +106,7 @@ Released 20 June 2020 for Stardew Valley 1.4.1 or later. * Changed SMAPI's Harmony ID from `io.smapi` to `SMAPI` for readability in Harmony summaries. ## 3.5 -Released 27 April 2020 for Stardew Valley 1.4.1 or later. +Released 27 April 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/36471055). * For players: * SMAPI now prevents more game errors due to broken items, so you no longer need save editing to remove them. @@ -140,7 +140,7 @@ Released 24 March 2020 for Stardew Valley 1.4.1 or later. * Fixed mouse input suppression not working in SMAPI 3.4. ## 3.4 -Released 22 March 2020 for Stardew Valley 1.4.1 or later. +Released 22 March 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/35161371). * For players: * Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). @@ -173,7 +173,7 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. * Fixed errors with custom spouse room mods in SMAPI 3.3. ## 3.3 -Released 22 February 2020 for Stardew Valley 1.4.1 or later. +Released 22 February 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/34248719). * For players: * Improved performance for mods which load many images. @@ -207,7 +207,7 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. * The SMAPI log now prefixes the OS name with `Android` on Android. ## 3.2 -Released 01 February 2020 for Stardew Valley 1.4.1 or later. +Released 01 February 2020 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/33659728). * For players: * SMAPI now prevents crashes due to invalid schedule data. @@ -248,7 +248,7 @@ Released 01 February 2020 for Stardew Valley 1.4.1 or later. * Dropped API support for the pre-3.0 update-check format. ## 3.1 -Released 05 January 2019 for Stardew Valley 1.4.1 or later. +Released 05 January 2019 for Stardew Valley 1.4.1 or later. See [release highlights](https://www.patreon.com/posts/32904041). * For players: * Added separate group in 'skipped mods' list for broken dependencies, so it's easier to see what to fix first. -- cgit