From d8d8cac2d89c27c6d28be444cebaabf0e2077a53 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 10:43:05 -0400 Subject: simplify logged paranoid warnings --- src/SMAPI/Framework/SCore.cs | 133 ++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 41 deletions(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..8c9424c1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1129,67 +1129,118 @@ namespace StardewModdingAPI.Framework // log warnings if (modsWithWarnings.Any()) { - // issue block format logic - void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) - { - IModMetadata[] matches = modsWithWarnings - .Where(mod => mod.HasUnsuppressWarning(warning)) - .ToArray(); - if (!matches.Any()) - return; - - this.Monitor.Log(" " + heading, logLevel); - this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel); - foreach (string line in blurb) - this.Monitor.Log(" " + line, logLevel); - this.Monitor.Newline(); - foreach (IModMetadata match in matches) - this.Monitor.Log($" - {match.DisplayName}", logLevel); - this.Monitor.Newline(); - } - - // supported issues - LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", + // broken code + this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", "errors, or crashes in-game." ); - LogWarningGroup(ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", + + // changes serializer + this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", "These mods change the save serializer. They may corrupt your save files, or make them unusable if", "you uninstall these mods." ); - if (this.Settings.ParanoidWarnings) - { - LogWarningGroup(ModWarning.AccessesConsole, LogLevel.Warn, "Accesses the console directly", - "These mods directly access the SMAPI console, and you enabled paranoid warnings. (Note that this may be", - "legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", - "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", - "legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly", - "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that", - "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - } - LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code", + + // patched game code + this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code", "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", "your game has issues, try removing these first. Otherwise you can ignore this warning." ); - LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", + + // unvalidated update tick + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", "corruption. If your game has issues, try removing these first." ); - LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", + + // paranoid warnings + if (this.Settings.ParanoidWarnings) + { + this.LogModWarningGroup( + modsWithWarnings, + match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), + level: LogLevel.Debug, + heading: "Direct system access", + blurb: new[] + { + "You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or", + "SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for", + "further investigation.)" + }, + modLabel: mod => + { + List labels = new List(); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole)) + labels.Add("console"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem)) + labels.Add("files"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell)) + labels.Add("shells/processes"); + + return $"{mod.DisplayName} ({string.Join(", ", labels)})"; + } + ); + } + + // no update keys + this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", "mods. Consider notifying the mod authors about this problem." ); - LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", + + // not crossplatform + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." ); } } + /// Write a mod warning group to the console and log. + /// The mods to search. + /// Matches mods to include in the warning group. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + /// Formats the mod label, or null to use the . + private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) + { + // get matching mods + IModMetadata[] matches = mods + .Where(match) + .ToArray(); + if (!matches.Any()) + return; + + // log header/blurb + this.Monitor.Log(" " + heading, level); + this.Monitor.Log(" " + "".PadRight(50, '-'), level); + foreach (string line in blurb) + this.Monitor.Log(" " + line, level); + this.Monitor.Newline(); + + // log mod list + foreach (IModMetadata modMatch in matches) + { + string label = modLabel != null + ? modLabel(modMatch) + : modMatch.DisplayName; + this.Monitor.Log($" - {label}", level); + } + + this.Monitor.Newline(); + } + + /// Write a mod warning group to the console and log. + /// The mods to search. + /// The mod warning to match. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) + { + this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); + } + /// Load a mod's entry class. /// The mod assembly. /// The loaded instance. -- cgit From 719831c15a74a4987496dc77a3caa6999940bf90 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 10:58:10 -0400 Subject: sort mod warning lists --- docs/release-notes.md | 3 +++ src/SMAPI/Framework/SCore.cs | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index ee8bd468..f12455bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming released +* For players: + * Mod warnings are now listed alphabetically. + * For modders: * Added `Multiplayer.PeerConnected` event. * Simplified paranoid warnings in the log and reduced their log level. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 8c9424c1..cd292bfc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1205,10 +1205,12 @@ namespace StardewModdingAPI.Framework private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) { // get matching mods - IModMetadata[] matches = mods + string[] modLabels = mods .Where(match) + .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName) + .OrderBy(p => p) .ToArray(); - if (!matches.Any()) + if (!modLabels.Any()) return; // log header/blurb @@ -1219,13 +1221,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Newline(); // log mod list - foreach (IModMetadata modMatch in matches) - { - string label = modLabel != null - ? modLabel(modMatch) - : modMatch.DisplayName; + foreach (string label in modLabels) this.Monitor.Log($" - {label}", level); - } this.Monitor.Newline(); } -- cgit From 163eebd92e21075698986a843850f0850514e778 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 May 2020 19:57:22 -0400 Subject: move internal commands out of SCore --- src/SMAPI/Framework/CommandManager.cs | 13 ++++- src/SMAPI/Framework/Commands/HelpCommand.cs | 64 +++++++++++++++++++++++ src/SMAPI/Framework/Commands/IInternalCommand.cs | 24 +++++++++ src/SMAPI/Framework/Commands/ReloadI18nCommand.cs | 44 ++++++++++++++++ src/SMAPI/Framework/SCore.cs | 54 ++++--------------- 5 files changed, 154 insertions(+), 45 deletions(-) create mode 100644 src/SMAPI/Framework/Commands/HelpCommand.cs create mode 100644 src/SMAPI/Framework/Commands/IInternalCommand.cs create mode 100644 src/SMAPI/Framework/Commands/ReloadI18nCommand.cs (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index ceeb6f93..eaa91c86 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using StardewModdingAPI.Framework.Commands; namespace StardewModdingAPI.Framework { @@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework /// The or is null or empty. /// The is not a valid format. /// There's already a command with that name. - public void Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) + public CommandManager Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) { name = this.GetNormalizedName(name); @@ -45,6 +46,16 @@ namespace StardewModdingAPI.Framework // add command this.Commands.Add(name, new Command(mod, name, documentation, callback)); + return this; + } + + /// Add a console command. + /// the SMAPI console command to add. + /// Writes messages to the console. + /// There's already a command with that name. + public CommandManager Add(IInternalCommand command, IMonitor monitor) + { + return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor)); } /// Get a command by its unique name. diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs new file mode 100644 index 00000000..b8730a00 --- /dev/null +++ b/src/SMAPI/Framework/Commands/HelpCommand.cs @@ -0,0 +1,64 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'help' SMAPI console command. + internal class HelpCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// Manages console commands. + private readonly CommandManager CommandManager; + + + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "help"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display."; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Manages console commands. + public HelpCommand(CommandManager commandManager) + { + this.CommandManager = commandManager; + } + + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + if (args.Any()) + { + Command result = this.CommandManager.Get(args[0]); + if (result == null) + monitor.Log("There's no command with that name.", LogLevel.Error); + else + monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); + } + else + { + string message = "The following commands are registered:\n"; + IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); + foreach (var group in groups) + { + string modName = group.Key ?? "SMAPI"; + string[] commandNames = group.ToArray(); + message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; + } + message += "For more information about a command, type 'help command_name'."; + + monitor.Log(message, LogLevel.Info); + } + } + } +} diff --git a/src/SMAPI/Framework/Commands/IInternalCommand.cs b/src/SMAPI/Framework/Commands/IInternalCommand.cs new file mode 100644 index 00000000..abf105b6 --- /dev/null +++ b/src/SMAPI/Framework/Commands/IInternalCommand.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.Commands +{ + /// A core SMAPI console command. + interface IInternalCommand + { + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + string Name { get; } + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + string Description { get; } + + + /********* + ** Methods + *********/ + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + void HandleCommand(string[] args, IMonitor monitor); + } +} diff --git a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs new file mode 100644 index 00000000..12328bb6 --- /dev/null +++ b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs @@ -0,0 +1,44 @@ +using System; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'reload_i18n' SMAPI console command. + internal class ReloadI18nCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// Reload translations for all mods. + private readonly Action ReloadTranslations; + + + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "reload_i18n"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Reloads translation files for all mods.\n\nUsage: reload_i18n"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Reload translations for all mods.. + public ReloadI18nCommand(Action reloadTranslations) + { + this.ReloadTranslations = reloadTranslations; + } + + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + this.ReloadTranslations(); + monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index cd292bfc..a89616a3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -16,6 +16,7 @@ using System.Windows.Forms; #endif using Newtonsoft.Json; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Commands; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; @@ -508,8 +509,9 @@ namespace StardewModdingAPI.Framework { // prepare console this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + this.GameInstance.CommandManager + .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) + .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input Thread inputThread = new Thread(() => @@ -1273,6 +1275,12 @@ namespace StardewModdingAPI.Framework } /// Reload translations for all mods. + private void ReloadTranslations() + { + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + } + + /// Reload translations for the given mods. /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { @@ -1357,48 +1365,6 @@ namespace StardewModdingAPI.Framework return translations; } - /// The method called when the user submits a core SMAPI command in the console. - /// The command name. - /// The command arguments. - private void HandleCommand(string name, string[] arguments) - { - switch (name) - { - case "help": - if (arguments.Any()) - { - Command result = this.GameInstance.CommandManager.Get(arguments[0]); - if (result == null) - this.Monitor.Log("There's no command with that name.", LogLevel.Error); - else - this.Monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); - } - else - { - string message = "The following commands are registered:\n"; - IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); - foreach (var group in groups) - { - string modName = group.Key ?? "SMAPI"; - string[] commandNames = group.ToArray(); - message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; - } - message += "For more information about a command, type 'help command_name'."; - - this.Monitor.Log(message, LogLevel.Info); - } - break; - - case "reload_i18n": - this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); - this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); - break; - - default: - throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); - } - } - /// Redirect messages logged directly to the console to the given monitor. /// The monitor with which to log messages as the game. /// The message to log. -- cgit From b074eb279a649030e71ed2192bb6a528a9801fce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 May 2020 20:00:33 -0400 Subject: add harmony_summary command --- docs/release-notes.md | 3 +- .../Framework/Commands/HarmonySummaryCommand.cs | 167 +++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 1 + 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index f95a6192..4596a525 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,8 +11,9 @@ * Internal changes to improve performance and reliability. * For modders: - * Added `Multiplayer.PeerConnected` event. * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added `Multiplayer.PeerConnected` event. + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs new file mode 100644 index 00000000..bc8f4aa2 --- /dev/null +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'harmony_summary' SMAPI console command. + internal class HarmonySummaryCommand : IInternalCommand + { + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "harmony_summary"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary \n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method."; + + + /********* + ** Public methods + *********/ + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.Method).ToArray(); + + StringBuilder result = new StringBuilder(); + + if (!matches.Any()) + result.AppendLine("No current patches match your search."); + else + { + result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:"); + result.AppendLine(); + foreach (var match in matches) + { + result.AppendLine($" {match.Method}"); + foreach (var ownerGroup in match.PatchTypesByOwner) + { + var sortedTypes = ownerGroup.Value + .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); + + result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); + } + } + } + + monitor.Log(result.ToString(), LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// Get all current Harmony patches matching any of the given search terms. + /// The search terms to match. + private IEnumerable FilterPatches(string[] searchTerms) + { + bool hasSearch = searchTerms.Any(); + bool IsMatch(string target) => searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); + + foreach (var patch in this.GetAllPatches()) + { + if (!hasSearch) + yield return patch; + + // matches entire patch + if (IsMatch(patch.Method)) + { + yield return patch; + continue; + } + + // matches individual patchers + foreach (var pair in patch.PatchTypesByOwner.ToArray()) + { + if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString()))) + patch.PatchTypesByOwner.Remove(pair.Key); + } + + if (patch.PatchTypesByOwner.Any()) + yield return patch; + } + } + + /// Get all current Harmony patches. + private IEnumerable GetAllPatches() + { + foreach (MethodBase method in Harmony.GetAllPatchedMethods()) + { + // get metadata for method + string methodLabel = method.FullDescription(); + HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); + IDictionary> patchGroups = new Dictionary> + { + [PatchType.Prefix] = patchInfo.Prefixes, + [PatchType.Postfix] = patchInfo.Postfixes, + [PatchType.Finalizer] = patchInfo.Finalizers, + [PatchType.Transpiler] = patchInfo.Transpilers + }; + + // get patch types by owner + var typesByOwner = new Dictionary>(); + foreach (var group in patchGroups) + { + foreach (var patch in group.Value) + { + if (!typesByOwner.TryGetValue(patch.owner, out ISet patchTypes)) + typesByOwner[patch.owner] = patchTypes = new HashSet(); + patchTypes.Add(group.Key); + } + } + + // create search result + yield return new SearchResult(methodLabel, typesByOwner); + } + } + + /// A Harmony patch type. + private enum PatchType + { + /// A prefix patch. + Prefix, + + /// A postfix patch. + Postfix, + + /// A finalizer patch. + Finalizer, + + /// A transpiler patch. + Transpiler + } + + /// A patch search result for a method. + private class SearchResult + { + /********* + ** Accessors + *********/ + /// A detailed human-readable label for the patched method. + public string Method { get; } + + /// The patch types by the Harmony instance ID that added them. + public IDictionary> PatchTypesByOwner { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A detailed human-readable label for the patched method. + /// The patch types by the Harmony instance ID that added them. + public SearchResult(string method, IDictionary> patchTypesByOwner) + { + this.Method = method; + this.PatchTypesByOwner = patchTypesByOwner; + } + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a89616a3..9d96bad1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,6 +511,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) + .Add(new HarmonySummaryCommand(), this.Monitor) .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input -- cgit From fc29fe918a89623544b011c76217aa1ea1975d00 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 18:58:05 -0400 Subject: refactor & optimize event code a bit, drop old support for unknown event handlers --- src/SMAPI/Events/EventPriority.cs | 24 +--- src/SMAPI/Events/EventPriorityAttribute.cs | 31 +++-- src/SMAPI/Framework/Events/EventManager.cs | 5 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 138 ++++++--------------- src/SMAPI/Framework/Events/ManagedEventHandler.cs | 62 +++++++++ src/SMAPI/Framework/Events/ModDisplayEvents.cs | 20 +-- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 28 ++--- src/SMAPI/Framework/Events/ModInputEvents.cs | 8 +- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 6 +- src/SMAPI/Framework/Events/ModPlayerEvents.cs | 6 +- src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 6 +- src/SMAPI/Framework/Events/ModWorldEvents.cs | 8 +- src/SMAPI/Framework/SCore.cs | 2 +- 13 files changed, 166 insertions(+), 178 deletions(-) create mode 100644 src/SMAPI/Framework/Events/ManagedEventHandler.cs (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/src/SMAPI/Events/EventPriority.cs b/src/SMAPI/Events/EventPriority.cs index 17f5fbb7..e1fb00ac 100644 --- a/src/SMAPI/Events/EventPriority.cs +++ b/src/SMAPI/Events/EventPriority.cs @@ -1,29 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace StardewModdingAPI.Events { - /// - /// Event priority for method handlers. - /// + /// The event priorities for method handlers. public enum EventPriority { - /// - /// Low priority. - /// + /// Low priority. Low = 3, - /// - /// Normal priority. This is the default. - /// + /// The default priority. Normal = 2, - /// - /// High priority. - /// - High = 1, + /// High priority. + High = 1 } } diff --git a/src/SMAPI/Events/EventPriorityAttribute.cs b/src/SMAPI/Events/EventPriorityAttribute.cs index c5683931..207e7862 100644 --- a/src/SMAPI/Events/EventPriorityAttribute.cs +++ b/src/SMAPI/Events/EventPriorityAttribute.cs @@ -1,27 +1,24 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace StardewModdingAPI.Events { - /// - /// An attribute for controlling event priority of an event handler. - /// + /// An attribute which specifies the priority for an event handler. [AttributeUsage(AttributeTargets.Method)] - public class EventPriorityAttribute : System.Attribute + public class EventPriorityAttribute : Attribute { - /// - /// The priority for the method marked by this attribute. - /// - public EventPriority Priority { get; } + /********* + ** Accessors + *********/ + /// The event handler priority, relative to other handlers across all mods registered for this event. + internal EventPriority Priority { get; } - /// - /// Constructor. - /// - /// The priority for method marked by this attribute. - public EventPriorityAttribute( EventPriority priority ) + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The event handler priority, relative to other handlers across all mods registered for this event. Higher-priority handlers are notified before lower-priority handlers. + public EventPriorityAttribute(EventPriority priority) { this.Priority = priority; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index a9dfda97..538fde59 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -174,15 +174,14 @@ namespace StardewModdingAPI.Framework.Events ** Public methods *********/ /// Construct an instance. - /// Writes messages to the log. /// The mod registry with which to identify mods. /// Tracks performance metrics. - public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor) + public EventManager(ModRegistry modRegistry, PerformanceMonitor performanceMonitor) { // create shortcut initializers ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) { - return new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical); + return new ManagedEvent($"{typeName}.{eventName}", modRegistry, performanceMonitor, isPerformanceCritical); } // init events (new) diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 172b25c0..b0f0ae71 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -15,24 +14,24 @@ namespace StardewModdingAPI.Framework.Events /********* ** Fields *********/ - /// The underlying event. - private IList> EventHandlers = new List>(); - - /// Writes messages to the log. - private readonly IMonitor Monitor; + /// The underlying event handlers. + private readonly List> EventHandlers = new List>(); /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; - /// The display names for the mods which added each delegate. - private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); - - /// The cached invocation list. - private EventHandler[] CachedInvocationList; - /// Tracks performance metrics. private readonly PerformanceMonitor PerformanceMonitor; + /// The total number of event handlers registered for this events, regardless of whether they're still registered. + private int RegistrationIndex; + + /// Whether any registered event handlers have a custom priority value. + private bool HasCustomPriorities; + + /// Whether event handlers should be sorted before the next invocation. + private bool NeedsSort; + /********* ** Accessors @@ -49,14 +48,12 @@ namespace StardewModdingAPI.Framework.Events *********/ /// Construct an instance. /// A human-readable name for the event. - /// Writes messages to the log. /// The mod registry with which to identify mods. /// Tracks performance metrics. /// Whether the event is typically called at least once per second. - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; - this.Monitor = monitor; this.ModRegistry = modRegistry; this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; @@ -65,14 +62,7 @@ namespace StardewModdingAPI.Framework.Events /// Get whether anything is listening to the event. public bool HasListeners() { - return this.CachedInvocationList?.Length > 0; - } - - /// Add an event handler. - /// The event handler. - public void Add(EventHandler handler) - { - this.Add(handler, this.ModRegistry.GetFromStack()); + return this.EventHandlers.Count > 0; } /// Add an event handler. @@ -80,33 +70,46 @@ namespace StardewModdingAPI.Framework.Events /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { - this.EventHandlers.Add(handler); - this.AddTracking(mod, handler, this.EventHandlers); + EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; + var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); + + this.EventHandlers.Add(managedHandler); + this.HasCustomPriorities = this.HasCustomPriorities || managedHandler.HasCustomPriority(); + + if (this.HasCustomPriorities) + this.NeedsSort = true; } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - this.EventHandlers.Remove(handler); - this.RemoveTracking(handler, this.EventHandlers); + this.EventHandlers.RemoveAll(p => p.Handler == handler); + this.HasCustomPriorities = this.HasCustomPriorities && this.EventHandlers.Any(p => p.HasCustomPriority()); } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { + // sort event handlers by priority + // (This is done here to avoid repeatedly sorting when handlers are added/removed.) + if (this.NeedsSort) + { + this.NeedsSort = false; + this.EventHandlers.Sort(); + } + + // raise if (this.EventHandlers.Count == 0) return; - - this.PerformanceMonitor.Track(this.EventName, () => { - foreach (EventHandler handler in this.CachedInvocationList) + foreach (ManagedEventHandler handler in this.EventHandlers) { try { - this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Invoke(null, args)); + this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); } catch (Exception ex) { @@ -124,13 +127,13 @@ namespace StardewModdingAPI.Framework.Events if (this.EventHandlers.Count == 0) return; - foreach (EventHandler handler in this.CachedInvocationList) + foreach (ManagedEventHandler handler in this.EventHandlers) { - if (match(this.GetSourceMod(handler))) + if (match(handler.SourceMod)) { try { - handler.Invoke(null, args); + handler.Handler.Invoke(null, args); } catch (Exception ex) { @@ -146,80 +149,21 @@ namespace StardewModdingAPI.Framework.Events *********/ /// Get the mod name for a given event handler to display in performance monitoring reports. /// The event handler. - private string GetModNameForPerformanceCounters(EventHandler handler) + private string GetModNameForPerformanceCounters(ManagedEventHandler handler) { - IModMetadata mod = this.GetSourceMod(handler); - if (mod == null) - return Constants.GamePerformanceCounterName; + IModMetadata mod = handler.SourceMod; return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; } - /// - /// Get the event priority of an event handler. - /// - /// The event handler to get the priority of. - /// The event priority of the event handler. - private EventPriority GetPriorityOfHandler(EventHandler handler) - { - CustomAttributeData attr = handler.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(EventPriorityAttribute)); - if (attr == null) - return EventPriority.Normal; - return (EventPriority) attr.ConstructorArguments[0].Value; - } - - /// - /// Sort an invocation list by its priority. - /// - /// The invocation list. - /// An array of the event handlers sorted by their priority. - private EventHandler[] GetCachedInvocationList(IEnumerable> invocationList ) - { - EventHandler[] handlers = invocationList?.ToArray() ?? new EventHandler[0]; - return handlers.OrderBy((h1) => this.GetPriorityOfHandler(h1)).ToArray(); - } - - /// Track an event handler. - /// The mod which added the handler. - /// The event handler. - /// The updated event invocation list. - protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) - { - this.SourceMods[handler] = mod; - this.CachedInvocationList = this.GetCachedInvocationList(invocationList); - } - - /// Remove tracking for an event handler. - /// The event handler. - /// The updated event invocation list. - protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) - { - this.CachedInvocationList = this.GetCachedInvocationList(invocationList); - if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) - this.SourceMods.Remove(handler); - } - - /// Get the mod which registered the given event handler, if available. - /// The event handler. - protected IModMetadata GetSourceMod(EventHandler handler) - { - return this.SourceMods.TryGetValue(handler, out IModMetadata mod) - ? mod - : null; - } - /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. - protected void LogError(EventHandler handler, Exception ex) + protected void LogError(ManagedEventHandler handler, Exception ex) { - IModMetadata mod = this.GetSourceMod(handler); - if (mod != null) - mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } } diff --git a/src/SMAPI/Framework/Events/ManagedEventHandler.cs b/src/SMAPI/Framework/Events/ManagedEventHandler.cs new file mode 100644 index 00000000..87591f63 --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEventHandler.cs @@ -0,0 +1,62 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// An event handler wrapper which tracks metadata about an event handler. + /// The event arguments type. + internal class ManagedEventHandler : IComparable + { + /********* + ** Accessors + *********/ + /// The event handler method. + public EventHandler Handler { get; } + + /// The order in which the event handler was registered, relative to other handlers for this event. + public int RegistrationOrder { get; } + + /// The event handler priority, relative to other handlers for this event. + public EventPriority Priority { get; } + + /// The mod which registered the handler. + public IModMetadata SourceMod { get; set; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The event handler method. + /// The order in which the event handler was registered, relative to other handlers for this event. + /// The event handler priority, relative to other handlers for this event. + /// The mod which registered the handler. + public ManagedEventHandler(EventHandler handler, int registrationOrder, EventPriority priority, IModMetadata sourceMod) + { + this.Handler = handler; + this.RegistrationOrder = registrationOrder; + this.Priority = priority; + this.SourceMod = sourceMod; + } + + /// Get whether the event handler has a custom priority value. + public bool HasCustomPriority() + { + return this.Priority != EventPriority.Normal; + } + + /// Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + /// An object to compare with this instance. + /// is not the same type as this instance. + public int CompareTo(object obj) + { + if (!(obj is ManagedEventHandler other)) + throw new ArgumentException("Can't compare to an unrelated object type."); + + int priorityCompare = this.Priority.CompareTo(other.Priority); + return priorityCompare != 0 + ? priorityCompare + : this.RegistrationOrder.CompareTo(other.RegistrationOrder); + } + } +} diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs index e383eec6..54d40dee 100644 --- a/src/SMAPI/Framework/Events/ModDisplayEvents.cs +++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs @@ -13,70 +13,70 @@ namespace StardewModdingAPI.Framework.Events /// Raised after a game menu is opened, closed, or replaced. public event EventHandler MenuChanged { - add => this.EventManager.MenuChanged.Add(value); + add => this.EventManager.MenuChanged.Add(value, this.Mod); remove => this.EventManager.MenuChanged.Remove(value); } /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. public event EventHandler Rendering { - add => this.EventManager.Rendering.Add(value); + add => this.EventManager.Rendering.Add(value, this.Mod); remove => this.EventManager.Rendering.Remove(value); } /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). public event EventHandler Rendered { - add => this.EventManager.Rendered.Add(value); + add => this.EventManager.Rendered.Add(value, this.Mod); remove => this.EventManager.Rendered.Remove(value); } /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. public event EventHandler RenderingWorld { - add => this.EventManager.RenderingWorld.Add(value); + add => this.EventManager.RenderingWorld.Add(value, this.Mod); remove => this.EventManager.RenderingWorld.Remove(value); } /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. public event EventHandler RenderedWorld { - add => this.EventManager.RenderedWorld.Add(value); + add => this.EventManager.RenderedWorld.Add(value, this.Mod); remove => this.EventManager.RenderedWorld.Remove(value); } /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. public event EventHandler RenderingActiveMenu { - add => this.EventManager.RenderingActiveMenu.Add(value); + add => this.EventManager.RenderingActiveMenu.Add(value, this.Mod); remove => this.EventManager.RenderingActiveMenu.Remove(value); } /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. public event EventHandler RenderedActiveMenu { - add => this.EventManager.RenderedActiveMenu.Add(value); + add => this.EventManager.RenderedActiveMenu.Add(value, this.Mod); remove => this.EventManager.RenderedActiveMenu.Remove(value); } /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. public event EventHandler RenderingHud { - add => this.EventManager.RenderingHud.Add(value); + add => this.EventManager.RenderingHud.Add(value, this.Mod); remove => this.EventManager.RenderingHud.Remove(value); } /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. public event EventHandler RenderedHud { - add => this.EventManager.RenderedHud.Add(value); + add => this.EventManager.RenderedHud.Add(value, this.Mod); remove => this.EventManager.RenderedHud.Remove(value); } /// Raised after the game window is resized. public event EventHandler WindowResized { - add => this.EventManager.WindowResized.Add(value); + add => this.EventManager.WindowResized.Add(value, this.Mod); remove => this.EventManager.WindowResized.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index c15460fa..a0119bf8 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -12,84 +12,84 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game is launched, right before the first update tick. public event EventHandler GameLaunched { - add => this.EventManager.GameLaunched.Add(value); + add => this.EventManager.GameLaunched.Add(value, this.Mod); remove => this.EventManager.GameLaunched.Remove(value); } /// Raised before the game performs its overall update tick (≈60 times per second). public event EventHandler UpdateTicking { - add => this.EventManager.UpdateTicking.Add(value); + add => this.EventManager.UpdateTicking.Add(value, this.Mod); remove => this.EventManager.UpdateTicking.Remove(value); } /// Raised after the game performs its overall update tick (≈60 times per second). public event EventHandler UpdateTicked { - add => this.EventManager.UpdateTicked.Add(value); + add => this.EventManager.UpdateTicked.Add(value, this.Mod); remove => this.EventManager.UpdateTicked.Remove(value); } /// Raised once per second before the game state is updated. public event EventHandler OneSecondUpdateTicking { - add => this.EventManager.OneSecondUpdateTicking.Add(value); + add => this.EventManager.OneSecondUpdateTicking.Add(value, this.Mod); remove => this.EventManager.OneSecondUpdateTicking.Remove(value); } /// Raised once per second after the game state is updated. public event EventHandler OneSecondUpdateTicked { - add => this.EventManager.OneSecondUpdateTicked.Add(value); + add => this.EventManager.OneSecondUpdateTicked.Add(value, this.Mod); remove => this.EventManager.OneSecondUpdateTicked.Remove(value); } /// Raised before the game creates a new save file. public event EventHandler SaveCreating { - add => this.EventManager.SaveCreating.Add(value); + add => this.EventManager.SaveCreating.Add(value, this.Mod); remove => this.EventManager.SaveCreating.Remove(value); } /// Raised after the game finishes creating the save file. public event EventHandler SaveCreated { - add => this.EventManager.SaveCreated.Add(value); + add => this.EventManager.SaveCreated.Add(value, this.Mod); remove => this.EventManager.SaveCreated.Remove(value); } /// Raised before the game begins writes data to the save file. public event EventHandler Saving { - add => this.EventManager.Saving.Add(value); + add => this.EventManager.Saving.Add(value, this.Mod); remove => this.EventManager.Saving.Remove(value); } /// Raised after the game finishes writing data to the save file. public event EventHandler Saved { - add => this.EventManager.Saved.Add(value); + add => this.EventManager.Saved.Add(value, this.Mod); remove => this.EventManager.Saved.Remove(value); } /// Raised after the player loads a save slot and the world is initialized. public event EventHandler SaveLoaded { - add => this.EventManager.SaveLoaded.Add(value); + add => this.EventManager.SaveLoaded.Add(value, this.Mod); remove => this.EventManager.SaveLoaded.Remove(value); } /// Raised after the game begins a new day (including when the player loads a save). public event EventHandler DayStarted { - add => this.EventManager.DayStarted.Add(value); + add => this.EventManager.DayStarted.Add(value, this.Mod); remove => this.EventManager.DayStarted.Remove(value); } /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . public event EventHandler DayEnding { - add => this.EventManager.DayEnding.Add(value); + add => this.EventManager.DayEnding.Add(value, this.Mod); remove => this.EventManager.DayEnding.Remove(value); } @@ -97,14 +97,14 @@ namespace StardewModdingAPI.Framework.Events public event EventHandler TimeChanged { - add => this.EventManager.TimeChanged.Add(value); + add => this.EventManager.TimeChanged.Add(value, this.Mod); remove => this.EventManager.TimeChanged.Remove(value); } /// Raised after the game returns to the title screen. public event EventHandler ReturnedToTitle { - add => this.EventManager.ReturnedToTitle.Add(value); + add => this.EventManager.ReturnedToTitle.Add(value, this.Mod); remove => this.EventManager.ReturnedToTitle.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 6a4298b4..ab26ab3e 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -12,28 +12,28 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player presses a button on the keyboard, controller, or mouse. public event EventHandler ButtonPressed { - add => this.EventManager.ButtonPressed.Add(value); + add => this.EventManager.ButtonPressed.Add(value, this.Mod); remove => this.EventManager.ButtonPressed.Remove(value); } /// Raised after the player releases a button on the keyboard, controller, or mouse. public event EventHandler ButtonReleased { - add => this.EventManager.ButtonReleased.Add(value); + add => this.EventManager.ButtonReleased.Add(value, this.Mod); remove => this.EventManager.ButtonReleased.Remove(value); } /// Raised after the player moves the in-game cursor. public event EventHandler CursorMoved { - add => this.EventManager.CursorMoved.Add(value); + add => this.EventManager.CursorMoved.Add(value, this.Mod); remove => this.EventManager.CursorMoved.Remove(value); } /// Raised after the player scrolls the mouse wheel. public event EventHandler MouseWheelScrolled { - add => this.EventManager.MouseWheelScrolled.Add(value); + add => this.EventManager.MouseWheelScrolled.Add(value, this.Mod); remove => this.EventManager.MouseWheelScrolled.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 152c4e0c..2006b2b5 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. public event EventHandler PeerContextReceived { - add => this.EventManager.PeerContextReceived.Add(value); + add => this.EventManager.PeerContextReceived.Add(value, this.Mod); remove => this.EventManager.PeerContextReceived.Remove(value); } /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { - add => this.EventManager.ModMessageReceived.Add(value); + add => this.EventManager.ModMessageReceived.Add(value, this.Mod); remove => this.EventManager.ModMessageReceived.Remove(value); } /// Raised after the connection with a peer is severed. public event EventHandler PeerDisconnected { - add => this.EventManager.PeerDisconnected.Add(value); + add => this.EventManager.PeerDisconnected.Add(value, this.Mod); remove => this.EventManager.PeerDisconnected.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs index ca7cfd96..240beb8d 100644 --- a/src/SMAPI/Framework/Events/ModPlayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. public event EventHandler InventoryChanged { - add => this.EventManager.InventoryChanged.Add(value); + add => this.EventManager.InventoryChanged.Add(value, this.Mod); remove => this.EventManager.InventoryChanged.Remove(value); } /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the local player. public event EventHandler LevelChanged { - add => this.EventManager.LevelChanged.Add(value); + add => this.EventManager.LevelChanged.Add(value, this.Mod); remove => this.EventManager.LevelChanged.Remove(value); } /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the local player. public event EventHandler Warped { - add => this.EventManager.Warped.Add(value); + add => this.EventManager.Warped.Add(value, this.Mod); remove => this.EventManager.Warped.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs index 9388bdb2..1d6788e1 100644 --- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. public event EventHandler LoadStageChanged { - add => this.EventManager.LoadStageChanged.Add(value); + add => this.EventManager.LoadStageChanged.Add(value, this.Mod); remove => this.EventManager.LoadStageChanged.Remove(value); } /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. public event EventHandler UnvalidatedUpdateTicking { - add => this.EventManager.UnvalidatedUpdateTicking.Add(value); + add => this.EventManager.UnvalidatedUpdateTicking.Add(value, this.Mod); remove => this.EventManager.UnvalidatedUpdateTicking.Remove(value); } /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. public event EventHandler UnvalidatedUpdateTicked { - add => this.EventManager.UnvalidatedUpdateTicked.Add(value); + add => this.EventManager.UnvalidatedUpdateTicked.Add(value, this.Mod); remove => this.EventManager.UnvalidatedUpdateTicked.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 2ae69669..21b1b664 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -40,28 +40,28 @@ namespace StardewModdingAPI.Framework.Events /// Raised after NPCs are added or removed in a location. public event EventHandler NpcListChanged { - add => this.EventManager.NpcListChanged.Add(value); + add => this.EventManager.NpcListChanged.Add(value, this.Mod); remove => this.EventManager.NpcListChanged.Remove(value); } /// Raised after objects are added or removed in a location. public event EventHandler ObjectListChanged { - add => this.EventManager.ObjectListChanged.Add(value); + add => this.EventManager.ObjectListChanged.Add(value, this.Mod); remove => this.EventManager.ObjectListChanged.Remove(value); } /// Raised after items are added or removed from a chest. public event EventHandler ChestInventoryChanged { - add => this.EventManager.ChestInventoryChanged.Add(value); + add => this.EventManager.ChestInventoryChanged.Add(value, this.Mod); remove => this.EventManager.ChestInventoryChanged.Remove(value); } /// Raised after terrain features (like floors and trees) are added or removed in a location. public event EventHandler TerrainFeatureListChanged { - add => this.EventManager.TerrainFeatureListChanged.Add(value); + add => this.EventManager.TerrainFeatureListChanged.Add(value, this.Mod); remove => this.EventManager.TerrainFeatureListChanged.Remove(value); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..c6e69d4e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI.Framework this.MonitorForGame = this.GetSecondaryMonitor("game"); SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceMonitor); + this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor); SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); -- cgit From dcd2c647a2abd836e8ee20f8ddad6568c9b4fbf2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 22:17:32 -0400 Subject: temporarily restore Harmony 1.x support with compile flag (#711) --- docs/release-notes.md | 10 ++- docs/technical/smapi.md | 1 + .../Framework/Commands/HarmonySummaryCommand.cs | 2 + .../ModLoading/RewriteFacades/AccessToolsFacade.cs | 2 + .../RewriteFacades/HarmonyInstanceFacade.cs | 2 + .../RewriteFacades/HarmonyMethodFacade.cs | 2 + .../Rewriters/Harmony1AssemblyRewriter.cs | 2 + src/SMAPI/Framework/Patching/GamePatcher.cs | 8 ++ src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 8 ++ src/SMAPI/Framework/Patching/PatchHelper.cs | 36 +++++++++ src/SMAPI/Framework/SCore.cs | 2 + src/SMAPI/Metadata/InstructionMetadata.cs | 6 ++ src/SMAPI/Patches/DialogueErrorPatch.cs | 94 +++++++++++++++++++++- src/SMAPI/Patches/EventErrorPatch.cs | 48 ++++++++++- src/SMAPI/Patches/LoadContextPatch.cs | 8 ++ src/SMAPI/Patches/LoadErrorPatch.cs | 8 ++ src/SMAPI/Patches/ObjectErrorPatch.cs | 50 +++++++++++- src/SMAPI/Patches/ScheduleErrorPatch.cs | 50 +++++++++++- src/SMAPI/SMAPI.csproj | 2 +- 19 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 src/SMAPI/Framework/Patching/PatchHelper.cs (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index dd87c1fc..c47ee835 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,12 @@ ← [README](README.md) # Release notes -## Upcoming released +## Upcoming release + 1 +* For modders: + * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. + +## Upcoming release * For players: * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. @@ -17,12 +22,10 @@ * Internal changes to improve performance and reliability. * For modders: - * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. * Added ability to override update keys from the compatibility list. - * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. @@ -33,6 +36,7 @@ * Fixed `ModMessageReceived` event handlers not tracked for performance monitoring. * For SMAPI developers: + * Added support for bundling a custom Harmony build for upcoming use. * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. * Overhauled update checks to simplify individual clients, centralize common logic, and enable upcoming features. * Merged the separate legacy redirects app on AWS into the main app on Azure. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index ca8a9c70..3b2d6e56 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -58,6 +58,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. +`HARMONY_2` | Whether to enable experimental Harmony 2.0 support. Existing Harmony 1._x_ mods will be rewritten automatically for compatibility. ## For SMAPI developers ### Compiling from source diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 08233feb..8c20fbdd 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Linq; @@ -166,3 +167,4 @@ namespace StardewModdingAPI.Framework.Commands } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs index 8e4320b3..102f3364 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -40,3 +41,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs index 54b91679..ad6d5e4f 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -80,3 +81,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs index 44c97401..f3975558 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -43,3 +44,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index 8fed170a..b30d686e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using HarmonyLib; using Mono.Cecil; @@ -125,3 +126,4 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } } } +#endif diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index dcad285a..82d7b9c8 100644 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -1,5 +1,9 @@ using System; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Patching { @@ -27,7 +31,11 @@ namespace StardewModdingAPI.Framework.Patching /// The patches to apply. public void Apply(params IHarmonyPatch[] patches) { +#if HARMONY_2 Harmony harmony = new Harmony("SMAPI"); +#else + HarmonyInstance harmony = HarmonyInstance.Create("SMAPI"); +#endif foreach (IHarmonyPatch patch in patches) { try diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs index 7d5eb3d4..922243fa 100644 --- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs +++ b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs @@ -1,4 +1,8 @@ +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Patching { @@ -10,6 +14,10 @@ namespace StardewModdingAPI.Framework.Patching /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 void Apply(Harmony harmony); +#else + void Apply(HarmonyInstance harmony); +#endif } } diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs new file mode 100644 index 00000000..d1aa0185 --- /dev/null +++ b/src/SMAPI/Framework/Patching/PatchHelper.cs @@ -0,0 +1,36 @@ +#if !HARMONY_2 +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Patching +{ + /// Provides generic methods for implementing Harmony patches. + internal class PatchHelper + { + /********* + ** Fields + *********/ + /// The interception keys currently being intercepted. + private static readonly HashSet InterceptingKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Track a method that will be intercepted. + /// The intercept key. + /// Returns false if the method was already marked for interception, else true. + public static bool StartIntercept(string key) + { + return PatchHelper.InterceptingKeys.Add(key); + } + + /// Track a method as no longer being intercepted. + /// The intercept key. + public static void StopIntercept(string key) + { + PatchHelper.InterceptingKeys.Remove(key); + } + } +} +#endif diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 530b6754..1a2c97f4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,7 +511,9 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) +#if HARMONY_2 .Add(new HarmonySummaryCommand(), this.Monitor) +#endif .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 89430a11..79d7a7a8 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,8 +38,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); +#if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); +#endif /**** ** detect mod issues @@ -51,7 +53,11 @@ namespace StardewModdingAPI.Metadata /**** ** detect code which may impact game stability ****/ +#if HARMONY_2 yield return new TypeFinder(typeof(HarmonyLib.Harmony).FullName, InstructionHandleResult.DetectedGamePatch); +#else + yield return new TypeFinder(typeof(Harmony.HarmonyInstance).FullName, InstructionHandleResult.DetectedGamePatch); +#endif yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer); diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index cddf29d6..8043eda3 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewValley; +#if HARMONY_2 +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -47,6 +52,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) { harmony.Patch( @@ -58,11 +64,24 @@ namespace StardewModdingAPI.Patches finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_NPC_CurrentDialogue)) ); } - +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_Dialogue_Constructor)) + ); + harmony.Patch( + original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue)) + ); + } +#endif /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call after the Dialogue constructor. /// The instance being patched. /// The dialogue being parsed. @@ -102,5 +121,74 @@ namespace StardewModdingAPI.Patches return null; } +#else + + /// The method to call instead of the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// Returns whether to execute the original method. + private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) + { + // get private members + bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); + IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + IReflectedField> dialogues = DialogueErrorPatch.Reflection.GetField>(__instance, "dialogues"); + + // replicate base constructor + if (dialogues.GetValue() == null) + dialogues.SetValue(new List()); + + // duplicate code with try..catch + try + { + if (!nameArraysTranslated) + translateArraysOfStrings.Invoke(); + __instance.speaker = speaker; + parseDialogueString.Invoke(masterDialogue); + checkForSpecialDialogueAttributes.Invoke(); + } + catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + { + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return false; + } + + /// The method to call instead of . + /// The instance being patched. + /// The return value of the original method. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_NPC_CurrentDialogue); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Stack)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) + { + DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Stack(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index de9dea29..4dbb25f3 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -1,6 +1,11 @@ -using System; using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 +using System; using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -38,6 +43,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) { harmony.Patch( @@ -45,11 +51,21 @@ namespace StardewModdingAPI.Patches finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) ); } +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + ); + } +#endif /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call instead of the GameLocation.CheckEventPrecondition. /// The return value of the original method. /// The precondition to be parsed. @@ -65,5 +81,35 @@ namespace StardewModdingAPI.Patches return null; } +#else + /// The method to call instead of the GameLocation.CheckEventPrecondition. + /// The instance being patched. + /// The return value of the original method. + /// The precondition to be parsed. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) + { + const string key = nameof(Before_GameLocation_CheckEventPrecondition); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 9c707676..768ddd6b 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,6 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; @@ -47,7 +51,11 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { // detect CreatedBasicInfo harmony.Patch( diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index f8ad6693..5e67b169 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -2,7 +2,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -49,7 +53,11 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { harmony.Patch( original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 189a14a0..4edcc64e 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,11 +1,16 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; using StardewValley.Menus; using SObject = StardewValley.Object; +#if HARMONY_2 +using System; +using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -27,7 +32,11 @@ namespace StardewModdingAPI.Patches *********/ /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { // object.getDescription harmony.Patch( @@ -38,7 +47,11 @@ namespace StardewModdingAPI.Patches // object.getDisplayName harmony.Patch( original: AccessTools.Method(typeof(SObject), "loadDisplayName"), +#if HARMONY_2 finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Finalize_Object_loadDisplayName)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName)) +#endif ); // IClickableMenu.drawToolTip @@ -68,6 +81,7 @@ namespace StardewModdingAPI.Patches return true; } +#if HARMONY_2 /// The method to call after . /// The patched method's return value. /// The exception thrown by the wrapped method, if any. @@ -82,6 +96,38 @@ namespace StardewModdingAPI.Patches return __exception; } +#else + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_Object_loadDisplayName); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (string)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException) + { + __result = "???"; + return false; + } + catch + { + return true; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif /// The method to call instead of . /// The item for which to draw a tooltip. diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index df6ffab3..cc2238b0 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -1,10 +1,15 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewValley; +#if HARMONY_2 +using System; +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -40,11 +45,19 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { harmony.Patch( original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), +#if HARMONY_2 finalizer: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Finalize_NPC_parseMasterSchedule)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) +#endif ); } @@ -52,6 +65,7 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call instead of . /// The raw schedule data to parse. /// The instance being patched. @@ -68,5 +82,35 @@ namespace StardewModdingAPI.Patches return null; } +#else + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_NPC_parseMasterSchedule); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); + return false; + } + catch (TargetInvocationException ex) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Dictionary(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 603b6fb5..c17de6d0 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,7 +14,7 @@ - + -- cgit From f63f14c70369541311bb5034894409a5170d56e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 Jun 2020 18:53:29 -0400 Subject: fix typo --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1a2c97f4..2794002c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -994,7 +994,7 @@ namespace StardewModdingAPI.Framework } catch (SAssemblyLoadFailedException ex) { - errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}"; + errorReasonPhrase = $"its DLL couldn't be loaded: {ex.Message}"; return false; } catch (Exception ex) -- cgit From aeab19f4aca366be3446b9f1ee097d51d21b5fde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jun 2020 21:28:44 -0400 Subject: backport harmony_summary command to Harmony 1.x (#711) --- docs/release-notes.md | 4 ++- .../Framework/Commands/HarmonySummaryCommand.cs | 38 ++++++++++++++++++++-- src/SMAPI/Framework/SCore.cs | 2 -- 3 files changed, 38 insertions(+), 6 deletions(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index c47ee835..9ea3e445 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,10 +1,11 @@ ← [README](README.md) # Release notes + ## Upcoming release * For players: @@ -25,6 +26,7 @@ * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Added ability to override update keys from the compatibility list. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 8c20fbdd..8fdd4282 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -1,16 +1,27 @@ -#if HARMONY_2 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Commands { /// The 'harmony_summary' SMAPI console command. internal class HarmonySummaryCommand : IInternalCommand { +#if !HARMONY_2 + /********* + ** Fields + *********/ + /// The Harmony instance through which to fetch patch info. + private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}"); +#endif + /********* ** Accessors *********/ @@ -45,7 +56,16 @@ namespace StardewModdingAPI.Framework.Commands foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key)) { var sortedTypes = ownerGroup.Value - .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); + .OrderBy(p => p switch + { + PatchType.Prefix => 0, + PatchType.Postfix => 1, +#if HARMONY_2 + PatchType.Finalizer => 2, +#endif + PatchType.Transpiler => 3, + _ => 4 + }); result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); } @@ -91,15 +111,26 @@ namespace StardewModdingAPI.Framework.Commands /// Get all current Harmony patches. private IEnumerable GetAllPatches() { +#if HARMONY_2 foreach (MethodBase method in Harmony.GetAllPatchedMethods()) +#else + foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods()) +#endif { // get metadata for method +#if HARMONY_2 HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); +#else + Harmony.Patches patchInfo = this.HarmonyInstance.GetPatchInfo(method); +#endif + IDictionary> patchGroups = new Dictionary> { [PatchType.Prefix] = patchInfo.Prefixes, [PatchType.Postfix] = patchInfo.Postfixes, +#if HARMONY_2 [PatchType.Finalizer] = patchInfo.Finalizers, +#endif [PatchType.Transpiler] = patchInfo.Transpilers }; @@ -129,8 +160,10 @@ namespace StardewModdingAPI.Framework.Commands /// A postfix patch. Postfix, +#if HARMONY_2 /// A finalizer patch. Finalizer, +#endif /// A transpiler patch. Transpiler @@ -167,4 +200,3 @@ namespace StardewModdingAPI.Framework.Commands } } } -#endif diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2794002c..e1db563c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,9 +511,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) -#if HARMONY_2 .Add(new HarmonySummaryCommand(), this.Monitor) -#endif .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input -- cgit From 067163da02c5a5993d88d80f04d379c22bc32cba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:50:23 -0400 Subject: make parallel rewriting optional --- docs/release-notes.md | 2 +- src/SMAPI/Constants.cs | 8 ++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 +- .../ModLoading/Framework/RecursiveRewriter.cs | 143 +++++++++++++-------- src/SMAPI/Framework/Models/SConfig.cs | 11 +- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/SMAPI.config.json | 20 ++- 7 files changed, 128 insertions(+), 70 deletions(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index ed86f73f..3c3f0796 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). + * Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it. * Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!). * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..9d510d2d 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -52,6 +52,14 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// Whether SMAPI was compiled in debug mode. + internal const bool IsDebugBuild = +#if DEBUG + true; +#else + false; +#endif + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index dbb5f696..f8c901e0 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,9 +76,10 @@ 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) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -108,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -262,9 +263,10 @@ 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) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix, bool rewriteInParallel) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -313,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(); + bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); // 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 9dc3680f..34c78c7d 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -56,15 +57,20 @@ 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() + public bool RewriteModule(bool rewriteInParallel) { - int typesChanged = 0; - Exception exception = null; + IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - Parallel.ForEach( - source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like - body: type => + // 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; @@ -72,50 +78,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework bool changed = false; try { - changed |= this.RewriteCustomAttributes(type.CustomAttributes); - changed |= this.RewriteGenericParameters(type.GenericParameters); - - foreach (InterfaceImplementation @interface in type.Interfaces) - changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); - - if (type.BaseType.FullName != "System.Object") - changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); - - foreach (MethodDefinition method in type.Methods) - { - changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); - changed |= this.RewriteGenericParameters(method.GenericParameters); - changed |= this.RewriteCustomAttributes(method.CustomAttributes); - - foreach (ParameterDefinition parameter in method.Parameters) - changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); - - foreach (var methodOverride in method.Overrides) - changed |= this.RewriteMethodReference(methodOverride); - - if (method.HasBody) - { - foreach (VariableDefinition variable in method.Body.Variables) - changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - Collection instructions = cil.Body.Instructions; - for (int i = 0; i < instructions.Count; i++) - { - var instruction = instructions[i]; - if (instruction.OpCode.Code == Code.Nop) - continue; - - changed |= this.RewriteInstruction(instruction, cil, newInstruction => - { - changed = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); - } - } - } + changed = this.RewriteTypeDefinition(type); } catch (Exception ex) { @@ -124,18 +87,90 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework if (changed) Interlocked.Increment(ref typesChanged); + }); + + return exception == null + ? typesChanged > 0 + : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + } + + // non-parallel rewriting + { + 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 exception == null - ? typesChanged > 0 - : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + return changed; + } } /********* ** Private methods *********/ + /// Rewrite a loaded type definition. + /// The type definition to rewrite. + /// Returns whether the type was modified. + private bool RewriteTypeDefinition(TypeDefinition type) + { + bool changed = false; + + changed |= this.RewriteCustomAttributes(type.CustomAttributes); + changed |= this.RewriteGenericParameters(type.GenericParameters); + + foreach (InterfaceImplementation @interface in type.Interfaces) + changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); + + if (type.BaseType.FullName != "System.Object") + changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); + + foreach (MethodDefinition method in type.Methods) + { + changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); + changed |= this.RewriteGenericParameters(method.GenericParameters); + changed |= this.RewriteCustomAttributes(method.CustomAttributes); + + foreach (ParameterDefinition parameter in method.Parameters) + changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + + foreach (var methodOverride in method.Overrides) + changed |= this.RewriteMethodReference(methodOverride); + + if (method.HasBody) + { + foreach (VariableDefinition variable in method.Body.Variables) + changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + Collection instructions = cil.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + if (instruction.OpCode.Code == Code.Nop) + continue; + + changed |= this.RewriteInstruction(instruction, cil, newInstruction => + { + changed = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + } + } + } + + return changed; + } + /// Rewrite a CIL instruction if needed. /// The current CIL instruction. /// The CIL instruction processor. diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b1612aa4..a98d8c54 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -15,12 +15,8 @@ namespace StardewModdingAPI.Framework.Models private static readonly IDictionary DefaultValues = new Dictionary { [nameof(CheckForUpdates)] = true, - [nameof(ParanoidWarnings)] = -#if DEBUG - true, -#else - false, -#endif + [nameof(ParanoidWarnings)] = Constants.IsDebugBuild, + [nameof(RewriteInParallel)] = Constants.IsDebugBuild, [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", @@ -45,6 +41,9 @@ 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 e1db563c..90435f54 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -337,6 +337,8 @@ 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) @@ -981,7 +983,7 @@ namespace StardewModdingAPI.Framework Assembly modAssembly; try { - modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible, rewriteInParallel: this.Settings.RewriteInParallel); 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 a426b0ef..0a6d8372 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -33,18 +33,30 @@ 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 * part of their normal functionality, so these warnings are meaningless without further - * investigation. When this is commented out, it'll be true for local debug builds and false - * otherwise. + * investigation. + * + * When this is commented out, it'll be true for local debug builds and false otherwise. */ //"ParanoidWarnings": true, /** - * Whether SMAPI should show newer beta versions as an available update. When this is commented - * out, it'll be true if the current SMAPI version is beta, and false otherwise. + * Whether SMAPI should show newer beta versions as an available update. + * + * When this is commented out, it'll be true if the current SMAPI version is beta, and false + * otherwise. */ //"UseBetaChannel": true, -- cgit From e9b8df46181bc9996cb2690acbb98971454b5b2a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 12:29:08 -0400 Subject: fix reload_i18n not reloading content pack translations --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src/SMAPI/Framework/SCore.cs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2bb4dc84..b25defa8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ * Mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Fixed `BadImageFormatException` error detection. * Fixed black maps on Android for mods which use `.tmx` files. + * Fixed `reload_i18n` command not reloading content pack translations. * For the web UI: * Added GitHub licenses to mod compatibility list. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 90435f54..2b04b1dc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1280,7 +1280,7 @@ namespace StardewModdingAPI.Framework /// Reload translations for all mods. private void ReloadTranslations() { - this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + this.ReloadTranslations(this.ModRegistry.GetAll()); } /// Reload translations for the given mods. -- cgit