diff options
Diffstat (limited to 'src/SMAPI/Framework')
55 files changed, 1648 insertions, 782 deletions
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 /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception> /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception> /// <exception cref="ArgumentException">There's already a command with that name.</exception> - public void Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false) + public CommandManager Add(IModMetadata mod, string name, string documentation, Action<string, string[]> 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; + } + + /// <summary>Add a console command.</summary> + /// <param name="command">the SMAPI console command to add.</param> + /// <param name="monitor">Writes messages to the console.</param> + /// <exception cref="ArgumentException">There's already a command with that name.</exception> + public CommandManager Add(IInternalCommand command, IMonitor monitor) + { + return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor)); } /// <summary>Get a command by its unique name.</summary> diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs new file mode 100644 index 00000000..08233feb --- /dev/null +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.Commands +{ + /// <summary>The 'harmony_summary' SMAPI console command.</summary> + internal class HarmonySummaryCommand : IInternalCommand + { + /********* + ** Accessors + *********/ + /// <summary>The command name, which the user must type to trigger it.</summary> + public string Name { get; } = "harmony_summary"; + + /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary> + 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 <search>\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 + *********/ + /// <summary>Handle the console command when it's entered by the user.</summary> + /// <param name="args">The command arguments.</param> + /// <param name="monitor">Writes messages to the console.</param> + public void HandleCommand(string[] args, IMonitor monitor) + { + SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).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.MethodName}"); + 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 }); + + result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); + } + result.AppendLine(); + } + } + + monitor.Log(result.ToString().TrimEnd(), LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get all current Harmony patches matching any of the given search terms.</summary> + /// <param name="searchTerms">The search terms to match.</param> + private IEnumerable<SearchResult> FilterPatches(string[] searchTerms) + { + bool hasSearch = searchTerms.Any(); + bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); + + foreach (var patch in this.GetAllPatches()) + { + // matches entire patch + if (IsMatch(patch.MethodDescription)) + { + 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; + } + } + + /// <summary>Get all current Harmony patches.</summary> + private IEnumerable<SearchResult> GetAllPatches() + { + foreach (MethodBase method in Harmony.GetAllPatchedMethods()) + { + // get metadata for method + HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); + IDictionary<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>> + { + [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<string, ISet<PatchType>>(); + foreach (var group in patchGroups) + { + foreach (var patch in group.Value) + { + if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes)) + typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>(); + patchTypes.Add(group.Key); + } + } + + // create search result + yield return new SearchResult(method, typesByOwner); + } + } + + /// <summary>A Harmony patch type.</summary> + private enum PatchType + { + /// <summary>A prefix patch.</summary> + Prefix, + + /// <summary>A postfix patch.</summary> + Postfix, + + /// <summary>A finalizer patch.</summary> + Finalizer, + + /// <summary>A transpiler patch.</summary> + Transpiler + } + + /// <summary>A patch search result for a method.</summary> + private class SearchResult + { + /********* + ** Accessors + *********/ + /// <summary>A simple human-readable name for the patched method.</summary> + public string MethodName { get; } + + /// <summary>A detailed description for the patched method.</summary> + public string MethodDescription { get; } + + /// <summary>The patch types by the Harmony instance ID that added them.</summary> + public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="method">The patched method.</param> + /// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param> + public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner) + { + this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}"; + this.MethodDescription = method.FullDescription(); + this.PatchTypesByOwner = patchTypesByOwner; + } + } + } +} 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 +{ + /// <summary>The 'help' SMAPI console command.</summary> + internal class HelpCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// <summary>Manages console commands.</summary> + private readonly CommandManager CommandManager; + + + /********* + ** Accessors + *********/ + /// <summary>The command name, which the user must type to trigger it.</summary> + public string Name { get; } = "help"; + + /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary> + public string Description { get; } = "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help <cmd>\n- cmd: The name of a command whose documentation to display."; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="commandManager">Manages console commands.</param> + public HelpCommand(CommandManager commandManager) + { + this.CommandManager = commandManager; + } + + /// <summary>Handle the console command when it's entered by the user.</summary> + /// <param name="args">The command arguments.</param> + /// <param name="monitor">Writes messages to the console.</param> + 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<string, string>[] 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 +{ + /// <summary>A core SMAPI console command.</summary> + interface IInternalCommand + { + /********* + ** Accessors + *********/ + /// <summary>The command name, which the user must type to trigger it.</summary> + string Name { get; } + + /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary> + string Description { get; } + + + /********* + ** Methods + *********/ + /// <summary>Handle the console command when it's entered by the user.</summary> + /// <param name="args">The command arguments.</param> + /// <param name="monitor">Writes messages to the console.</param> + 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 +{ + /// <summary>The 'reload_i18n' SMAPI console command.</summary> + internal class ReloadI18nCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// <summary>Reload translations for all mods.</summary> + private readonly Action ReloadTranslations; + + + /********* + ** Accessors + *********/ + /// <summary>The command name, which the user must type to trigger it.</summary> + public string Name { get; } = "reload_i18n"; + + /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary> + public string Description { get; } = "Reloads translation files for all mods.\n\nUsage: reload_i18n"; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="reloadTranslations">Reload translations for all mods..</param> + public ReloadI18nCommand(Action reloadTranslations) + { + this.ReloadTranslations = reloadTranslations; + } + + /// <summary>Handle the console command when it's entered by the user.</summary> + /// <param name="args">The command arguments.</param> + /// <param name="monitor">Writes messages to the console.</param> + 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/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index fda80a83..cfda55b9 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -246,10 +246,11 @@ namespace StardewModdingAPI.Framework.ContentManagers texture.GetData(data); for (int i = 0; i < data.Length; i++) { - if (data[i].A == byte.MinValue || data[i].A == byte.MaxValue) + var pixel = data[i]; + if (pixel.A == byte.MinValue || pixel.A == byte.MaxValue) continue; // no need to change fully transparent/opaque pixels - data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); + data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) } texture.SetData(data); diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 538fde59..b5a12a6e 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -109,9 +109,12 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ - /// <summary>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.</summary> + /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection (<see cref="IMultiplayerEvents.PeerConnected"/>), 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.</summary> public readonly ManagedEvent<PeerContextReceivedEventArgs> PeerContextReceived; + /// <summary>Raised after a peer connection is approved by the game.</summary> + public readonly ManagedEvent<PeerConnectedEventArgs> PeerConnected; + /// <summary>Raised after a mod message is received over the network.</summary> public readonly ManagedEvent<ModMessageReceivedEventArgs> ModMessageReceived; @@ -217,6 +220,7 @@ namespace StardewModdingAPI.Framework.Events this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); + this.PeerConnected = ManageEventOf<PeerConnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerConnected)); this.ModMessageReceived = ManageEventOf<ModMessageReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.PeerDisconnected = ManageEventOf<PeerDisconnectedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 2006b2b5..64cf0355 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -9,13 +9,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ - /// <summary>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.</summary> + /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection (<see cref="IMultiplayerEvents.PeerConnected"/>), 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.</summary> public event EventHandler<PeerContextReceivedEventArgs> PeerContextReceived { add => this.EventManager.PeerContextReceived.Add(value, this.Mod); remove => this.EventManager.PeerContextReceived.Remove(value); } + /// <summary>Raised after a peer connection is approved by the game.</summary> + public event EventHandler<PeerConnectedEventArgs> PeerConnected + { + add => this.EventManager.PeerConnected.Add(value); + remove => this.EventManager.PeerConnected.Remove(value); + } + /// <summary>Raised after a mod message is received over the network.</summary> public event EventHandler<ModMessageReceivedEventArgs> ModMessageReceived { diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs index 37927482..1231b494 100644 --- a/src/SMAPI/Framework/IModMetadata.cs +++ b/src/SMAPI/Framework/IModMetadata.cs @@ -112,9 +112,9 @@ namespace StardewModdingAPI.Framework /// <summary>Whether the mod has at least one valid update key set.</summary> bool HasValidUpdateKeys(); - /// <summary>Get whether the mod has a given warning and it hasn't been suppressed in the <see cref="DataRecord"/>.</summary> - /// <param name="warning">The warning to check.</param> - bool HasUnsuppressWarning(ModWarning warning); + /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="DataRecord"/>.</summary> + /// <param name="warnings">The warnings to check.</param> + bool HasUnsuppressedWarnings(params ModWarning[] warnings); /// <summary>Get a relative path which includes the root folder name.</summary> string GetRelativePathWithRoot(); diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index 36622066..2657fd12 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -205,16 +205,13 @@ namespace StardewModdingAPI.Framework.Input /// <summary>Get the equivalent state.</summary> public GamePadState GetState() { - if (this.State == null) - { - this.State = new GamePadState( - leftThumbStick: this.LeftStickPos, - rightThumbStick: this.RightStickPos, - leftTrigger: this.LeftTrigger, - rightTrigger: this.RightTrigger, - buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values - ); - } + this.State ??= new GamePadState( + leftThumbStick: this.LeftStickPos, + rightThumbStick: this.RightStickPos, + leftTrigger: this.LeftTrigger, + rightTrigger: this.RightTrigger, + buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values + ); return this.State.Value; } diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs index 59956feb..1cc16ca9 100644 --- a/src/SMAPI/Framework/Input/MouseStateBuilder.cs +++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs @@ -89,19 +89,16 @@ namespace StardewModdingAPI.Framework.Input /// <summary>Get the equivalent state.</summary> public MouseState GetState() { - if (this.State == null) - { - this.State = new MouseState( - x: this.X, - y: this.Y, - scrollWheel: this.ScrollWheelValue, - leftButton: this.ButtonStates[SButton.MouseLeft], - middleButton: this.ButtonStates[SButton.MouseMiddle], - rightButton: this.ButtonStates[SButton.MouseRight], - xButton1: this.ButtonStates[SButton.MouseX1], - xButton2: this.ButtonStates[SButton.MouseX2] - ); - } + this.State ??= new MouseState( + x: this.X, + y: this.Y, + scrollWheel: this.ScrollWheelValue, + leftButton: this.ButtonStates[SButton.MouseLeft], + middleButton: this.ButtonStates[SButton.MouseMiddle], + rightButton: this.ButtonStates[SButton.MouseRight], + xButton1: this.ButtonStates[SButton.MouseX1], + xButton2: this.ButtonStates[SButton.MouseX2] + ); return this.State.Value; } diff --git a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs index 86c327ed..916c215d 100644 --- a/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs @@ -122,7 +122,8 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <returns>Returns the same property instance for convenience.</returns> private IReflectedProperty<T> AssertAccessAllowed<T>(IReflectedProperty<T> property) { - this.AssertAccessAllowed(property?.PropertyInfo); + this.AssertAccessAllowed(property?.PropertyInfo.GetMethod?.GetBaseDefinition()); + this.AssertAccessAllowed(property?.PropertyInfo.SetMethod?.GetBaseDefinition()); return property; } @@ -131,7 +132,7 @@ namespace StardewModdingAPI.Framework.ModHelpers /// <returns>Returns the same method instance for convenience.</returns> private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) { - this.AssertAccessAllowed(method?.MethodInfo); + this.AssertAccessAllowed(method?.MethodInfo.GetBaseDefinition()); return method; } diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b5533335..dbb5f696 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -6,6 +6,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; +using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Utilities; @@ -49,6 +50,8 @@ namespace StardewModdingAPI.Framework.ModLoading this.Monitor = monitor; this.ParanoidMode = paranoidMode; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + + // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); @@ -124,13 +127,22 @@ namespace StardewModdingAPI.Framework.ModLoading if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) + this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); + + // load PDB file if present + byte[] symbols; { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); + string symbolsPath = Path.Combine(Path.GetDirectoryName(assemblyPath), Path.GetFileNameWithoutExtension(assemblyPath)) + ".pdb"; + symbols = File.Exists(symbolsPath) + ? File.ReadAllBytes(symbolsPath) + : null; } + + // load assembly + using MemoryStream outStream = new MemoryStream(); + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes, symbols); } else { @@ -282,35 +294,32 @@ namespace StardewModdingAPI.Framework.ModLoading this.ChangeTypeScope(type); } - // find (and optionally rewrite) incompatible instructions - bool anyRewritten = false; - IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) - { - // check method definition - foreach (IInstructionHandler handler in handlers) + // find or rewrite code + IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode, platformChanged).ToArray(); + RecursiveRewriter rewriter = new RecursiveRewriter( + module: module, + rewriteType: (type, replaceWith) => { - InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; - } - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - var instructions = cil.Body.Instructions; - // ReSharper disable once ForCanBeConvertedToForeach -- deliberate access by index so each handler sees replacements from previous handlers - for (int offset = 0; offset < instructions.Count; offset++) + bool rewritten = false; + foreach (IInstructionHandler handler in handlers) + rewritten |= handler.Handle(module, type, replaceWith); + return rewritten; + }, + rewriteInstruction: (ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith) => { + bool rewritten = false; foreach (IInstructionHandler handler in handlers) - { - Instruction instruction = instructions[offset]; - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; - } + rewritten |= handler.Handle(module, cil, instruction, replaceWith); + return rewritten; } + ); + bool anyRewritten = rewriter.RewriteModule(); + + // handle rewrite flags + foreach (IInstructionHandler handler in handlers) + { + foreach (var flag in handler.Flags) + this.ProcessInstructionHandleResult(mod, handler, flag, loggedMessages, logPrefix, filename); } return platformChanged || anyRewritten; @@ -325,49 +334,52 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="filename">The assembly filename for log messages.</param> private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet<string> loggedMessages, string logPrefix, string filename) { + // get message template + // ($phrase is replaced with the noun phrase or messages) + string template = null; switch (result) { case InstructionHandleResult.Rewritten: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Rewrote {filename} to fix {handler.NounPhrase}..."); + template = $"{logPrefix}Rewrote {filename} to fix $phrase..."; break; case InstructionHandleResult.NotCompatible: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Broken code in {filename}: {handler.NounPhrase}."); + template = $"{logPrefix}Broken code in {filename}: $phrase."; mod.SetWarning(ModWarning.BrokenCodeLoaded); break; case InstructionHandleResult.DetectedGamePatch: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected game patcher ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected game patcher ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.PatchesGame); break; case InstructionHandleResult.DetectedSaveSerializer: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected possible save serializer change ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected possible save serializer change ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.ChangesSaveSerializer); break; case InstructionHandleResult.DetectedUnvalidatedUpdateTick: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected reference to {handler.NounPhrase} in assembly {filename}."); + template = $"{logPrefix}Detected reference to $phrase in assembly {filename}."; mod.SetWarning(ModWarning.UsesUnvalidatedUpdateTick); break; case InstructionHandleResult.DetectedDynamic: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected 'dynamic' keyword ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected 'dynamic' keyword ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.UsesDynamic); break; case InstructionHandleResult.DetectedConsoleAccess: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected direct console access ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected direct console access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesConsole); break; case InstructionHandleResult.DetectedFilesystemAccess: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected filesystem access ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected filesystem access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesFilesystem); break; case InstructionHandleResult.DetectedShellAccess: - this.Monitor.LogOnce(loggedMessages, $"{logPrefix}Detected shell or process access ({handler.NounPhrase}) in assembly {filename}."); + template = $"{logPrefix}Detected shell or process access ($phrase) in assembly {filename}."; mod.SetWarning(ModWarning.AccessesShell); break; @@ -377,6 +389,17 @@ namespace StardewModdingAPI.Framework.ModLoading default: throw new NotSupportedException($"Unrecognized instruction handler result '{result}'."); } + if (template == null) + return; + + // format messages + if (handler.Phrases.Any()) + { + foreach (string message in handler.Phrases) + this.Monitor.LogOnce(template.Replace("$phrase", message)); + } + else + this.Monitor.LogOnce(template.Replace("$phrase", handler.DefaultPhrase ?? handler.GetType().Name)); } /// <summary>Get the correct reference to use for compatibility with the current platform.</summary> @@ -395,18 +418,5 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; type.Scope = assemblyRef; } - - /// <summary>Get all methods in a module.</summary> - /// <param name="module">The module to search.</param> - private IEnumerable<MethodDefinition> GetMethods(ModuleDefinition module) - { - return ( - from type in module.GetTypes() - where type.HasMethods - from method in type.Methods - where method.HasBody - select method - ); - } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index 898bafb4..e1476b73 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,10 +1,12 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds incompatible CIL instructions that reference a given event.</summary> - internal class EventFinder : IInstructionHandler + internal class EventFinder : BaseInstructionHandler { /********* ** Fields @@ -20,13 +22,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -34,34 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <param name="eventName">The event name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) + : base(defaultPhrase: $"{fullTypeName}.{eventName} event") { this.FullTypeName = fullTypeName; this.EventName = eventName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{eventName} event"; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; + if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) + this.MarkFlag(this.Result); + + return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index 606ca8b7..c157ed9b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,10 +1,12 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds incompatible CIL instructions that reference a given field.</summary> - internal class FieldFinder : IInstructionHandler + internal class FieldFinder : BaseInstructionHandler { /********* ** Fields @@ -20,13 +22,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -34,49 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <param name="fieldName">The field name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) + : base(defaultPhrase: $"{fullTypeName}.{fieldName} field") { this.FullTypeName = fullTypeName; this.FieldName = fieldName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{fieldName} field"; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - + if (!this.Flags.Contains(this.Result) && RewriteHelper.IsFieldReferenceTo(instruction, this.FullTypeName, this.FieldName)) + this.MarkFlag(this.Result); - /********* - ** Protected methods - *********/ - /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="instruction">The IL instruction.</param> - protected bool IsMatch(Instruction instruction) - { - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - return - fieldRef != null - && fieldRef.DeclaringType.FullName == this.FullTypeName - && fieldRef.Name == this.FieldName; + return false; } } } diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 9ca246ff..82c93a7c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,10 +1,12 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds incompatible CIL instructions that reference a given method.</summary> - internal class MethodFinder : IInstructionHandler + internal class MethodFinder : BaseInstructionHandler { /********* ** Fields @@ -20,13 +22,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -34,34 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <param name="methodName">The method name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) + : base(defaultPhrase: $"{fullTypeName}.{methodName} method") { this.FullTypeName = fullTypeName; this.MethodName = methodName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{methodName} method"; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; + if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) + this.MarkFlag(this.Result); + + return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index 0677aa88..c96d61a2 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,10 +1,12 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds incompatible CIL instructions that reference a given property.</summary> - internal class PropertyFinder : IInstructionHandler + internal class PropertyFinder : BaseInstructionHandler { /********* ** Fields @@ -20,13 +22,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -34,34 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <param name="propertyName">The property name for which to find references.</param> /// <param name="result">The result to return for matching instructions.</param> public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) + : base(defaultPhrase: $"{fullTypeName}.{propertyName} property") { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{propertyName} property"; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; + if (!this.Flags.Contains(this.Result) && this.IsMatch(instruction)) + this.MarkFlag(this.Result); + + return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 459e3210..a67cfa4f 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,13 +1,15 @@ +using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds references to a field, property, or method which returns a different type than the code expects.</summary> /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks> - internal class ReferenceToMemberWithUnexpectedTypeFinder : IInstructionHandler + internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandler { /********* ** Fields @@ -17,39 +19,23 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; private set; } = ""; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) + : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -58,13 +44,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders // get target field FieldDefinition targetField = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); if (targetField == null) - return InstructionHandleResult.None; + return false; // validate return type if (!RewriteHelper.LooksLikeSameType(fieldRef.FieldType, targetField.FieldType)) { - this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"; - return InstructionHandleResult.NotCompatible; + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (field returns {this.GetFriendlyTypeName(targetField.FieldType)}, not {this.GetFriendlyTypeName(fieldRef.FieldType)})"); + return false; } } @@ -75,21 +61,21 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders // get potential targets MethodDefinition[] candidateMethods = methodReference.DeclaringType.Resolve()?.Methods.Where(found => found.Name == methodReference.Name).ToArray(); if (candidateMethods == null || !candidateMethods.Any()) - return InstructionHandleResult.None; + return false; // compare return types MethodDefinition methodDef = methodReference.Resolve(); if (methodDef == null) - return InstructionHandleResult.None; // validated by ReferenceToMissingMemberFinder + return false; // validated by ReferenceToMissingMemberFinder if (candidateMethods.All(method => !RewriteHelper.LooksLikeSameType(method.ReturnType, methodDef.ReturnType))) { - this.NounPhrase = $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"; - return InstructionHandleResult.NotCompatible; + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {methodDef.DeclaringType.FullName}.{methodDef.Name} (no such method returns {this.GetFriendlyTypeName(methodDef.ReturnType)})"); + return false; } } - return InstructionHandleResult.None; + return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index 44b531a5..ebb62948 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,13 +1,15 @@ +using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds references to a field, property, or method which no longer exists.</summary> /// <remarks>This implementation is purely heuristic. It should never return a false positive, but won't detect all cases.</remarks> - internal class ReferenceToMissingMemberFinder : IInstructionHandler + internal class ReferenceToMissingMemberFinder : BaseInstructionHandler { /********* ** Fields @@ -17,39 +19,23 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; private set; } = ""; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="validateReferencesToAssemblies">The assembly names to which to heuristically detect broken references.</param> public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) + : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet<string>(validateReferencesToAssemblies); } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -58,8 +44,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders FieldDefinition target = fieldRef.DeclaringType.Resolve()?.Fields.FirstOrDefault(p => p.Name == fieldRef.Name); if (target == null) { - this.NounPhrase = $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"; - return InstructionHandleResult.NotCompatible; + this.MarkFlag(InstructionHandleResult.NotCompatible, $"reference to {fieldRef.DeclaringType.FullName}.{fieldRef.Name} (no such field)"); + return false; } } @@ -70,17 +56,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders MethodDefinition target = methodRef.Resolve(); if (target == null) { + string phrase = null; if (this.IsProperty(methodRef)) - this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name.Substring(4)} (no such property)"; else if (methodRef.Name == ".ctor") - this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no matching constructor)"; else - this.NounPhrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; - return InstructionHandleResult.NotCompatible; + phrase = $"reference to {methodRef.DeclaringType.FullName}.{methodRef.Name} (no such method)"; + + this.MarkFlag(InstructionHandleResult.NotCompatible, phrase); + return false; } } - return InstructionHandleResult.None; + return false; } diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs new file mode 100644 index 00000000..a1ade536 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs @@ -0,0 +1,51 @@ +using System; +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// <summary>Finds incompatible CIL instructions that reference types in a given assembly.</summary> + internal class TypeAssemblyFinder : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// <summary>The full assembly name to which to find references.</summary> + private readonly string AssemblyName; + + /// <summary>The result to return for matching instructions.</summary> + private readonly InstructionHandleResult Result; + + /// <summary>Get whether a matched type should be ignored.</summary> + private readonly Func<TypeReference, bool> ShouldIgnore; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="assemblyName">The full assembly name to which to find references.</param> + /// <param name="result">The result to return for matching instructions.</param> + /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> + public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) + : base(defaultPhrase: $"{assemblyName} assembly") + { + this.AssemblyName = assemblyName; + this.Result = result; + this.ShouldIgnore = shouldIgnore; + } + + /// <summary>Rewrite a type reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) + { + if (type.Scope.Name == this.AssemblyName && this.ShouldIgnore?.Invoke(type) != true) + this.MarkFlag(this.Result); + + return false; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs index 701b15f2..c285414a 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -1,31 +1,23 @@ using System; -using System.Linq; using Mono.Cecil; -using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// <summary>Finds incompatible CIL instructions that reference a given type.</summary> - internal class TypeFinder : IInstructionHandler + internal class TypeFinder : BaseInstructionHandler { /********* - ** Accessors + ** Fields *********/ - /// <summary>The full type name for which to find references.</summary> + /// <summary>The full type name to match.</summary> private readonly string FullTypeName; /// <summary>The result to return for matching instructions.</summary> private readonly InstructionHandleResult Result; - /// <summary>A lambda which overrides a matched type.</summary> - protected readonly Func<TypeReference, bool> ShouldIgnore; - - - /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } + /// <summary>Get whether a matched type should be ignored.</summary> + private readonly Func<TypeReference, bool> ShouldIgnore; /********* @@ -34,104 +26,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// <summary>Construct an instance.</summary> /// <param name="fullTypeName">The full type name to match.</param> /// <param name="result">The result to return for matching instructions.</param> - /// <param name="shouldIgnore">A lambda which overrides a matched type.</param> + /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeFinder(string fullTypeName, InstructionHandleResult result, Func<TypeReference, bool> shouldIgnore = null) + : base(defaultPhrase: $"{fullTypeName} type") { this.FullTypeName = fullTypeName; this.Result = result; - this.NounPhrase = $"{fullTypeName} type"; - this.ShouldIgnore = shouldIgnore ?? (p => false); - } - - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(method) - ? this.Result - : InstructionHandleResult.None; + this.ShouldIgnore = shouldIgnore; } - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a type reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="method">The method definition.</param> - protected bool IsMatch(MethodDefinition method) + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { - if (this.IsMatch(method.ReturnType)) - return true; - - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - return true; - } - - return false; - } - - /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="instruction">The IL instruction.</param> - protected bool IsMatch(Instruction instruction) - { - // field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) - { - return - this.IsMatch(fieldRef.DeclaringType) // field on target class - || this.IsMatch(fieldRef.FieldType); // field value is target class - } - - // method reference - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null) - { - return - this.IsMatch(methodRef.DeclaringType) // method on target class - || this.IsMatch(methodRef.ReturnType) // method returns target class - || methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType)); // method parameters - } - - return false; - } - - /// <summary>Get whether a type reference matches the expected type.</summary> - /// <param name="type">The type to check.</param> - protected bool IsMatch(TypeReference type) - { - // root type - if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type)) - return true; - - // generic arguments - if (type is GenericInstanceType genericType) - { - if (genericType.GenericArguments.Any(this.IsMatch)) - return true; - } - - // generic parameters (e.g. constraints) - if (type.GenericParameters.Any(this.IsMatch)) - return true; + if (type.FullName == this.FullTypeName && this.ShouldIgnore?.Invoke(type) != true) + this.MarkFlag(this.Result); return false; } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs new file mode 100644 index 00000000..79fb45b8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// <summary>The base implementation for a CIL instruction handler or rewriter.</summary> + internal abstract class BaseInstructionHandler : IInstructionHandler + { + /********* + ** Accessors + *********/ + /// <summary>A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</summary> + public string DefaultPhrase { get; } + + /// <summary>The rewrite flags raised for the current module.</summary> + public ISet<InstructionHandleResult> Flags { get; } = new HashSet<InstructionHandleResult>(); + + /// <summary>The brief noun phrases indicating what the handler matched for the current module.</summary> + public ISet<string> Phrases { get; } = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// <summary>Rewrite a type reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + public virtual bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) + { + return false; + } + + /// <summary>Rewrite a CIL instruction reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) + { + return false; + } + + + /********* + ** Protected methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="defaultPhrase">A brief noun phrase indicating what the handler matches.</param> + protected BaseInstructionHandler(string defaultPhrase) + { + this.DefaultPhrase = defaultPhrase; + } + + /// <summary>Raise a result flag.</summary> + /// <param name="flag">The result flag to set.</param> + /// <param name="resultMessage">The result message to add.</param> + /// <returns>Returns true for convenience.</returns> + protected bool MarkFlag(InstructionHandleResult flag, string resultMessage = null) + { + this.Flags.Add(flag); + if (resultMessage != null) + this.Phrases.Add(resultMessage); + return true; + } + + /// <summary>Raise a generic flag indicating that the code was rewritten.</summary> + public bool MarkRewritten() + { + return this.MarkFlag(InstructionHandleResult.Rewritten); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs new file mode 100644 index 00000000..9dc3680f --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -0,0 +1,309 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Collections.Generic; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// <summary>Handles recursively rewriting loaded assembly code.</summary> + internal class RecursiveRewriter + { + /********* + ** Delegates + *********/ + /// <summary>Rewrite a type reference in the assembly code.</summary> + /// <param name="type">The current type reference.</param> + /// <param name="replaceWith">Replaces the type reference with the given type.</param> + /// <returns>Returns whether the type was changed.</returns> + public delegate bool RewriteTypeDelegate(TypeReference type, Action<TypeReference> replaceWith); + + /// <summary>Rewrite a CIL instruction in the assembly code.</summary> + /// <param name="instruction">The current CIL instruction.</param> + /// <param name="cil">The CIL instruction processor.</param> + /// <param name="replaceWith">Replaces the CIL instruction with the given instruction.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith); + + + /********* + ** Accessors + *********/ + /// <summary>The module to rewrite.</summary> + public ModuleDefinition Module { get; } + + /// <summary>Handle or rewrite a type reference if needed.</summary> + public RewriteTypeDelegate RewriteTypeImpl { get; } + + /// <summary>Handle or rewrite a CIL instruction if needed.</summary> + public RewriteInstructionDelegate RewriteInstructionImpl { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="module">The module to rewrite.</param> + /// <param name="rewriteType">Handle or rewrite a type reference if needed.</param> + /// <param name="rewriteInstruction">Handle or rewrite a CIL instruction if needed.</param> + public RecursiveRewriter(ModuleDefinition module, RewriteTypeDelegate rewriteType, RewriteInstructionDelegate rewriteInstruction) + { + this.Module = module; + this.RewriteTypeImpl = rewriteType; + this.RewriteInstructionImpl = rewriteInstruction; + } + + /// <summary>Rewrite the loaded module code.</summary> + /// <returns>Returns whether the module was modified.</returns> + public bool RewriteModule() + { + int typesChanged = 0; + Exception exception = null; + + Parallel.ForEach( + source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like <Module> + body: type => + { + if (exception != null) + return; + + 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<Instruction> 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; + }); + } + } + } + } + catch (Exception ex) + { + exception ??= ex; + } + + if (changed) + Interlocked.Increment(ref typesChanged); + } + ); + + return exception == null + ? typesChanged > 0 + : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + } + + + /********* + ** Private methods + *********/ + /// <summary>Rewrite a CIL instruction if needed.</summary> + /// <param name="instruction">The current CIL instruction.</param> + /// <param name="cil">The CIL instruction processor.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + private bool RewriteInstruction(Instruction instruction, ILProcessor cil, Action<Instruction> replaceWith) + { + bool rewritten = false; + + // field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + rewritten |= this.RewriteTypeReference(fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); + rewritten |= this.RewriteTypeReference(fieldRef.FieldType, newType => fieldRef.FieldType = newType); + } + + // method reference + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (methodRef != null) + this.RewriteMethodReference(methodRef); + + // type reference + if (instruction.Operand is TypeReference typeRef) + rewritten |= this.RewriteTypeReference(typeRef, newType => replaceWith(cil.Create(instruction.OpCode, newType))); + + // instruction itself + // (should be done after the above type rewrites to ensure valid types) + rewritten |= this.RewriteInstructionImpl(ref instruction, cil, newInstruction => + { + rewritten = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + + return rewritten; + } + + /// <summary>Rewrite a method reference if needed.</summary> + /// <param name="methodRef">The current method reference.</param> + private bool RewriteMethodReference(MethodReference methodRef) + { + bool rewritten = false; + + rewritten |= this.RewriteTypeReference(methodRef.DeclaringType, newType => + { + // note: generic methods are wrapped into a MethodSpecification which doesn't allow changing the + // declaring type directly. For our purposes we want to change all generic versions of a matched + // method anyway, so we can use GetElementMethod to get the underlying method here. + methodRef.GetElementMethod().DeclaringType = newType; + }); + rewritten |= this.RewriteTypeReference(methodRef.ReturnType, newType => methodRef.ReturnType = newType); + + foreach (var parameter in methodRef.Parameters) + rewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + + if (methodRef is GenericInstanceMethod genericRef) + { + for (int i = 0; i < genericRef.GenericArguments.Count; i++) + rewritten |= this.RewriteTypeReference(genericRef.GenericArguments[i], newType => genericRef.GenericArguments[i] = newType); + } + + return rewritten; + } + + /// <summary>Rewrite a type reference if needed.</summary> + /// <param name="type">The current type reference.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + private bool RewriteTypeReference(TypeReference type, Action<TypeReference> replaceWith) + { + bool rewritten = false; + + // type + rewritten |= this.RewriteTypeImpl(type, newType => + { + type = newType; + replaceWith(newType); + rewritten = true; + }); + + // generic arguments + if (type is GenericInstanceType genericType) + { + for (int i = 0; i < genericType.GenericArguments.Count; i++) + rewritten |= this.RewriteTypeReference(genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); + } + + // generic parameters (e.g. constraints) + rewritten |= this.RewriteGenericParameters(type.GenericParameters); + + return rewritten; + } + + /// <summary>Rewrite custom attributes if needed.</summary> + /// <param name="attributes">The current custom attributes.</param> + private bool RewriteCustomAttributes(Collection<CustomAttribute> attributes) + { + bool rewritten = false; + + for (int attrIndex = 0; attrIndex < attributes.Count; attrIndex++) + { + CustomAttribute attribute = attributes[attrIndex]; + bool curChanged = false; + + // attribute type + TypeReference newAttrType = null; + rewritten |= this.RewriteTypeReference(attribute.AttributeType, newType => + { + newAttrType = newType; + curChanged = true; + }); + + // constructor arguments + TypeReference[] argTypes = new TypeReference[attribute.ConstructorArguments.Count]; + for (int i = 0; i < argTypes.Length; i++) + { + var arg = attribute.ConstructorArguments[i]; + + argTypes[i] = arg.Type; + rewritten |= this.RewriteTypeReference(arg.Type, newType => + { + argTypes[i] = newType; + curChanged = true; + }); + } + + // swap attribute + if (curChanged) + { + // get constructor + MethodDefinition constructor = (newAttrType ?? attribute.AttributeType) + .Resolve() + .Methods + .Where(method => method.IsConstructor) + .FirstOrDefault(ctor => RewriteHelper.HasMatchingSignature(ctor, attribute.Constructor)); + if (constructor == null) + throw new InvalidOperationException($"Can't rewrite attribute type '{attribute.AttributeType.FullName}' to '{newAttrType?.FullName}', no equivalent constructor found."); + + // create new attribute + var newAttr = new CustomAttribute(this.Module.ImportReference(constructor)); + for (int i = 0; i < argTypes.Length; i++) + newAttr.ConstructorArguments.Add(new CustomAttributeArgument(argTypes[i], attribute.ConstructorArguments[i].Value)); + foreach (var prop in attribute.Properties) + newAttr.Properties.Add(new CustomAttributeNamedArgument(prop.Name, prop.Argument)); + foreach (var field in attribute.Fields) + newAttr.Fields.Add(new CustomAttributeNamedArgument(field.Name, field.Argument)); + + // swap attribute + attributes[attrIndex] = newAttr; + rewritten = true; + } + } + + return rewritten; + } + + /// <summary>Rewrites generic type parameters if needed.</summary> + /// <param name="parameters">The current generic type parameters.</param> + private bool RewriteGenericParameters(Collection<GenericParameter> parameters) + { + bool anyChanged = false; + + for (int i = 0; i < parameters.Count; i++) + { + TypeReference parameter = parameters[i]; + anyChanged |= this.RewriteTypeReference(parameter, newType => parameters[i] = new GenericParameter(parameter.Name, newType)); + } + + return anyChanged; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index f8f10dc4..36058b86 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -4,7 +4,7 @@ using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -namespace StardewModdingAPI.Framework.ModLoading +namespace StardewModdingAPI.Framework.ModLoading.Framework { /// <summary>Provides helper methods for field rewriters.</summary> internal static class RewriteHelper @@ -28,6 +28,28 @@ namespace StardewModdingAPI.Framework.ModLoading : null; } + /// <summary>Get whether the field is a reference to the expected type and field.</summary> + /// <param name="instruction">The IL instruction.</param> + /// <param name="fullTypeName">The full type name containing the expected field.</param> + /// <param name="fieldName">The name of the expected field.</param> + public static bool IsFieldReferenceTo(Instruction instruction, string fullTypeName, string fieldName) + { + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + return RewriteHelper.IsFieldReferenceTo(fieldRef, fullTypeName, fieldName); + } + + /// <summary>Get whether the field is a reference to the expected type and field.</summary> + /// <param name="fieldRef">The field reference to check.</param> + /// <param name="fullTypeName">The full type name containing the expected field.</param> + /// <param name="fieldName">The name of the expected field.</param> + public static bool IsFieldReferenceTo(FieldReference fieldRef, string fullTypeName, string fieldName) + { + return + fieldRef != null + && fieldRef.DeclaringType.FullName == fullTypeName + && fieldRef.Name == fieldName; + } + /// <summary>Get the method reference from an instruction if it matches.</summary> /// <param name="instruction">The IL instruction.</param> public static MethodReference AsMethodReference(Instruction instruction) @@ -42,6 +64,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// <param name="reference">The type reference.</param> public static bool IsSameType(Type type, TypeReference reference) { + // + // duplicated by IsSameType(TypeReference, TypeReference) below + // + // same namespace & name if (type.Namespace != reference.Namespace || type.Name != reference.Name) return false; @@ -66,6 +92,39 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// <summary>Get whether a type matches a type reference.</summary> + /// <param name="type">The defined type.</param> + /// <param name="reference">The type reference.</param> + public static bool IsSameType(TypeReference type, TypeReference reference) + { + // + // duplicated by IsSameType(Type, TypeReference) above + // + + // same namespace & name + if (type.Namespace != reference.Namespace || type.Name != reference.Name) + return false; + + // same generic parameters + if (type.IsGenericInstance) + { + if (!reference.IsGenericInstance) + return false; + + TypeReference[] defGenerics = ((GenericInstanceType)type).GenericArguments.ToArray(); + TypeReference[] refGenerics = ((GenericInstanceType)reference).GenericArguments.ToArray(); + if (defGenerics.Length != refGenerics.Length) + return false; + for (int i = 0; i < defGenerics.Length; i++) + { + if (!RewriteHelper.IsSameType(defGenerics[i], refGenerics[i])) + return false; + } + } + + return true; + } + /// <summary>Determine whether two type IDs look like the same type, accounting for placeholder values such as !0.</summary> /// <param name="typeA">The type ID to compare.</param> /// <param name="typeB">The other type ID to compare.</param> @@ -78,8 +137,12 @@ namespace StardewModdingAPI.Framework.ModLoading /// <summary>Get whether a method definition matches the signature expected by a method reference.</summary> /// <param name="definition">The method definition.</param> /// <param name="reference">The method reference.</param> - public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) + public static bool HasMatchingSignature(MethodBase definition, MethodReference reference) { + // + // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below + // + // same name if (definition.Name != reference.Name) return false; @@ -97,13 +160,46 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// <summary>Get whether a method definition matches the signature expected by a method reference.</summary> + /// <param name="definition">The method definition.</param> + /// <param name="reference">The method reference.</param> + public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodBase, MethodReference) above + // + + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterDefinition[] definitionParameters = definition.Parameters.ToArray(); + ParameterDefinition[] referenceParameters = reference.Parameters.ToArray(); + if (referenceParameters.Length != definitionParameters.Length) + return false; + for (int i = 0; i < referenceParameters.Length; i++) + { + if (!RewriteHelper.IsSameType(definitionParameters[i].ParameterType, referenceParameters[i].ParameterType)) + return false; + } + return true; + } + /// <summary>Get whether a type has a method whose signature matches the one expected by a method reference.</summary> /// <param name="type">The type to check.</param> /// <param name="reference">The method reference.</param> public static bool HasMatchingSignature(Type type, MethodReference reference) { + if (reference.Name == ".ctor") + { + return type + .GetConstructors(BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) + .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); + } + return type - .GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public) + .GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); } } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 8830cc74..e6de6785 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Mono.Cecil; using Mono.Cecil.Cil; @@ -9,26 +11,32 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Accessors *********/ - /// <summary>A brief noun phrase indicating what the handler matches.</summary> - string NounPhrase { get; } + /// <summary>A brief noun phrase indicating what the handler matches, used if <see cref="Phrases"/> is empty.</summary> + string DefaultPhrase { get; } + + /// <summary>The rewrite flags raised for the current module.</summary> + ISet<InstructionHandleResult> Flags { get; } + + /// <summary>The brief noun phrases indicating what the handler matched for the current module.</summary> + ISet<string> Phrases { get; } /********* ** Methods *********/ - /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <summary>Rewrite a type reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith); - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith); } } diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs index 0e90362e..30701552 100644 --- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs +++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs @@ -215,13 +215,14 @@ namespace StardewModdingAPI.Framework.ModLoading return this.GetUpdateKeys(validOnly: true).Any(); } - /// <summary>Get whether the mod has a given warning and it hasn't been suppressed in the <see cref="DataRecord"/>.</summary> - /// <param name="warning">The warning to check.</param> - public bool HasUnsuppressWarning(ModWarning warning) + /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="IModMetadata.DataRecord"/>.</summary> + /// <param name="warnings">The warnings to check.</param> + public bool HasUnsuppressedWarnings(params ModWarning[] warnings) { - return + return warnings.Any(warning => this.Warnings.HasFlag(warning) - && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning)); + && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning)) + ); } /// <summary>Get a relative path which includes the root folder name.</summary> diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs new file mode 100644 index 00000000..8e4320b3 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// <summary>Maps Harmony 1.x <see cref="AccessTools"/> methods to Harmony 2.x to avoid breaking older mods.</summary> + /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks> + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class AccessToolsFacade + { + /********* + ** Public methods + *********/ + public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) + { + // Harmony 1.x matched both static and instance constructors + return + AccessTools.DeclaredConstructor(type, parameters, searchForStatic: false) + ?? AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true); + } + + public static ConstructorInfo Constructor(Type type, Type[] parameters = null) + { + // Harmony 1.x matched both static and instance constructors + return + AccessTools.Constructor(type, parameters, searchForStatic: false) + ?? AccessTools.Constructor(type, parameters, searchForStatic: true); + } + + public static List<ConstructorInfo> GetDeclaredConstructors(Type type) + { + // Harmony 1.x matched both static and instance constructors + return + AccessTools.GetDeclaredConstructors(type, searchForStatic: false) + ?? AccessTools.GetDeclaredConstructors(type, searchForStatic: true); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs new file mode 100644 index 00000000..54b91679 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// <summary>Maps Harmony 1.x <code>HarmonyInstance</code> methods to Harmony 2.x's <see cref="Harmony"/> to avoid breaking older mods.</summary> + /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks> + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class HarmonyInstanceFacade : Harmony + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="id">The unique patch identifier.</param> + public HarmonyInstanceFacade(string id) + : base(id) { } + + public static Harmony Create(string id) + { + return new Harmony(id); + } + + public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + // In Harmony 1.x you could target a virtual method that's not implemented by the + // target type, but in Harmony 2.0 you need to target the concrete implementation. + // This just resolves the method to the concrete implementation if needed. + if (original != null) + original = original.GetDeclaredMember(); + + // call Harmony 2.0 and show a detailed exception if it fails + try + { + MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); + return (DynamicMethod)method; + } + catch (Exception ex) + { + string patchTypes = this.GetPatchTypesLabel(prefix, postfix, transpiler); + string methodLabel = this.GetMethodLabel(original); + throw new Exception($"Harmony instance {this.Id} failed applying {patchTypes} to {methodLabel}.", ex); + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Get a human-readable label for the patch types being applies.</summary> + /// <param name="prefix">The prefix method, if any.</param> + /// <param name="postfix">The postfix method, if any.</param> + /// <param name="transpiler">The transpiler method, if any.</param> + private string GetPatchTypesLabel(HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + var patchTypes = new List<string>(); + + if (prefix != null) + patchTypes.Add("prefix"); + if (postfix != null) + patchTypes.Add("postfix"); + if (transpiler != null) + patchTypes.Add("transpiler"); + + return string.Join("/", patchTypes); + } + + /// <summary>Get a human-readable label for the method being patched.</summary> + /// <param name="method">The method being patched.</param> + private string GetMethodLabel(MethodBase method) + { + return method != null + ? $"method {method.DeclaringType?.FullName}.{method.Name}" + : "null method"; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs new file mode 100644 index 00000000..44c97401 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs @@ -0,0 +1,45 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// <summary>Maps Harmony 1.x <see cref="HarmonyMethod"/> methods to Harmony 2.x to avoid breaking older mods.</summary> + /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks> + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class HarmonyMethodFacade : HarmonyMethod + { + /********* + ** Public methods + *********/ + public HarmonyMethodFacade(MethodInfo method) + { + this.ImportMethodImpl(method); + } + + public HarmonyMethodFacade(Type type, string name, Type[] parameters = null) + { + this.ImportMethodImpl(AccessTools.Method(type, name, parameters)); + } + + + /********* + ** Private methods + *********/ + /// <summary>Import a method directly using the internal HarmonyMethod code.</summary> + /// <param name="methodInfo">The method to import.</param> + private void ImportMethodImpl(MethodInfo methodInfo) + { + // A null method is no longer allowed in the constructor with Harmony 2.0, but the + // internal code still handles null fine. For backwards compatibility, this bypasses + // the new restriction when the mod hasn't been updated for Harmony 2.0 yet. + + MethodInfo importMethod = typeof(HarmonyMethod).GetMethod("ImportMethod", BindingFlags.Instance | BindingFlags.NonPublic); + if (importMethod == null) + throw new InvalidOperationException("Can't find 'HarmonyMethod.ImportMethod' method"); + importMethod.Invoke(this, new object[] { methodInfo }); + } + } +} diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs index 26b22315..cf71af77 100644 --- a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs @@ -2,24 +2,25 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -#pragma warning disable 1591 // missing documentation -namespace StardewModdingAPI.Framework.RewriteFacades +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades { /// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows.</summary> /// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks> - public class SpriteBatchMethods : SpriteBatch + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] + [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class SpriteBatchFacade : SpriteBatch { /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + public SpriteBatchFacade(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } /**** ** MonoGame signatures ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/Mac.")] public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix? matrix) { base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, matrix ?? Matrix.Identity); @@ -28,31 +29,26 @@ namespace StardewModdingAPI.Framework.RewriteFacades /**** ** XNA signatures ****/ - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] public new void Begin() { base.Begin(); } - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] public new void Begin(SpriteSortMode sortMode, BlendState blendState) { base.Begin(sortMode, blendState); } - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) { base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); } - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) { base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); } - [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Windows.")] public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect, Matrix transformMatrix) { base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect, transformMatrix); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs index ff86c6e2..8043b13a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs @@ -2,16 +2,22 @@ using System; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites references to one field with another.</summary> - internal class FieldReplaceRewriter : FieldFinder + internal class FieldReplaceRewriter : BaseInstructionHandler { /********* ** Fields *********/ + /// <summary>The type containing the field to which references should be rewritten.</summary> + private readonly Type Type; + + /// <summary>The field name to which references should be rewritten.</summary> + private readonly string FromFieldName; + /// <summary>The new field to reference.</summary> private readonly FieldInfo ToField; @@ -20,31 +26,36 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="type">The type whose field to which references should be rewritten.</param> + /// <param name="type">The type whose field to rewrite.</param> /// <param name="fromFieldName">The field name to rewrite.</param> /// <param name="toFieldName">The new field name to reference.</param> public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : base(type.FullName, fromFieldName, InstructionHandleResult.None) + : base(defaultPhrase: $"{type.FullName}.{fromFieldName} field") { + this.Type = type; + this.FromFieldName = fromFieldName; this.ToField = type.GetField(toFieldName); if (this.ToField == null) throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); } - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; + // get field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) + return false; + // replace with new field FieldReference newRef = module.ImportReference(this.ToField); - cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); - return InstructionHandleResult.Rewritten; + replaceWith(cil.Create(instruction.OpCode, newRef)); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs index a43c5e9a..c3b5854e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldToPropertyRewriter.cs @@ -1,21 +1,24 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites field references into property references.</summary> - internal class FieldToPropertyRewriter : FieldFinder + internal class FieldToPropertyRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// <summary>The type whose field to which references should be rewritten.</summary> + /// <summary>The type containing the field to which references should be rewritten.</summary> private readonly Type Type; - /// <summary>The property name.</summary> - private readonly string PropertyName; + /// <summary>The field name to which references should be rewritten.</summary> + private readonly string FromFieldName; + + /// <summary>The new property name.</summary> + private readonly string ToPropertyName; /********* @@ -26,10 +29,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <param name="fieldName">The field name to rewrite.</param> /// <param name="propertyName">The property name (if different).</param> public FieldToPropertyRewriter(Type type, string fieldName, string propertyName) - : base(type.FullName, fieldName, InstructionHandleResult.None) + : base(defaultPhrase: $"{type.FullName}.{fieldName} field") { this.Type = type; - this.PropertyName = propertyName; + this.FromFieldName = fieldName; + this.ToPropertyName = propertyName; } /// <summary>Construct an instance.</summary> @@ -38,22 +42,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public FieldToPropertyRewriter(Type type, string fieldName) : this(type, fieldName, fieldName) { } - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; + // get field ref + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) + return false; + // replace with property string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; - MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.PropertyName}")); - cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); - - return InstructionHandleResult.Rewritten; + MethodReference propertyRef = module.ImportReference(this.Type.GetMethod($"{methodPrefix}_{this.ToPropertyName}")); + replaceWith(cil.Create(OpCodes.Call, propertyRef)); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs new file mode 100644 index 00000000..8fed170a --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -0,0 +1,127 @@ +using System; +using HarmonyLib; +using Mono.Cecil; +using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.RewriteFacades; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// <summary>Rewrites Harmony 1.x assembly references to work with Harmony 2.x.</summary> + internal class Harmony1AssemblyRewriter : BaseInstructionHandler + { + /********* + ** Fields + *********/ + /// <summary>Whether any Harmony 1.x types were replaced.</summary> + private bool ReplacedTypes; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + public Harmony1AssemblyRewriter() + : base(defaultPhrase: "Harmony 1.x") { } + + /// <summary>Rewrite a type reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) + { + // rewrite Harmony 1.x type to Harmony 2.0 type + if (type.Scope is AssemblyNameReference scope && scope.Name == "0Harmony" && scope.Version.Major == 1) + { + Type targetType = this.GetMappedType(type); + replaceWith(module.ImportReference(targetType)); + this.MarkRewritten(); + this.ReplacedTypes = true; + return true; + } + + return false; + } + + /// <summary>Rewrite a CIL instruction reference if needed.</summary> + /// <param name="module">The assembly module containing the instruction.</param> + /// <param name="cil">The CIL processor.</param> + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) + { + // rewrite Harmony 1.x methods to Harmony 2.0 + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (this.TryRewriteMethodsToFacade(module, methodRef)) + return true; + + // rewrite renamed fields + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (fieldRef != null) + { + if (fieldRef.DeclaringType.FullName == "HarmonyLib.HarmonyMethod" && fieldRef.Name == "prioritiy") + fieldRef.Name = nameof(HarmonyMethod.priority); + } + + return false; + } + + + /********* + ** Private methods + *********/ + /// <summary>Rewrite methods to use Harmony facades if needed.</summary> + /// <param name="module">The assembly module containing the method reference.</param> + /// <param name="methodRef">The method reference to map.</param> + private bool TryRewriteMethodsToFacade(ModuleDefinition module, MethodReference methodRef) + { + if (!this.ReplacedTypes) + return false; // not Harmony (or already using Harmony 2.0) + + // get facade type + Type toType; + switch (methodRef?.DeclaringType.FullName) + { + case "HarmonyLib.Harmony": + toType = typeof(HarmonyInstanceFacade); + break; + + case "HarmonyLib.AccessTools": + toType = typeof(AccessToolsFacade); + break; + + case "HarmonyLib.HarmonyMethod": + toType = typeof(HarmonyMethodFacade); + break; + + default: + return false; + } + + // map if there's a matching method + if (RewriteHelper.HasMatchingSignature(toType, methodRef)) + { + methodRef.DeclaringType = module.ImportReference(toType); + return true; + } + + return false; + } + + /// <summary>Get an equivalent Harmony 2.x type.</summary> + /// <param name="type">The Harmony 1.x method.</param> + private Type GetMappedType(TypeReference type) + { + // main Harmony object + if (type.FullName == "Harmony.HarmonyInstance") + return typeof(Harmony); + + // other objects + string fullName = type.FullName.Replace("Harmony.", "HarmonyLib."); + string targetName = typeof(Harmony).AssemblyQualifiedName.Replace(typeof(Harmony).FullName, fullName); + return Type.GetType(targetName, throwOnError: true); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 6b8c2de1..b8e53f40 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -1,31 +1,23 @@ using System; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites method references from one parent type to another if the signatures match.</summary> - internal class MethodParentRewriter : IInstructionHandler + internal class MethodParentRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// <summary>The type whose methods to remap.</summary> - private readonly Type FromType; + /// <summary>The full name of the type whose methods to remap.</summary> + private readonly string FromType; /// <summary>The type with methods to map to.</summary> private readonly Type ToType; - /// <summary>Whether to only rewrite references if loading the assembly on a different platform than it was compiled on.</summary> - private readonly bool OnlyIfPlatformChanged; - - - /********* - ** Accessors - *********/ - /// <summary>A brief noun phrase indicating what the instruction finder matches.</summary> - public string NounPhrase { get; } - /********* ** Public methods @@ -33,55 +25,50 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>Construct an instance.</summary> /// <param name="fromType">The type whose methods to remap.</param> /// <param name="toType">The type with methods to map to.</param> - /// <param name="onlyIfPlatformChanged">Whether to only rewrite references if loading the assembly on a different platform than it was compiled on.</param> - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) + /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> + public MethodParentRewriter(string fromType, Type toType, string nounPhrase = null) + : base(nounPhrase ?? $"{fromType.Split('.').Last()} methods") { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = $"{fromType.Name} methods"; - this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } + /// <summary>Construct an instance.</summary> + /// <param name="fromType">The type whose methods to remap.</param> + /// <param name="toType">The type with methods to map to.</param> + /// <param name="nounPhrase">A brief noun phrase indicating what the instruction finder matches (or <c>null</c> to generate one).</param> + public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null) + : this(fromType.FullName, toType, nounPhrase) { } - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - if (!this.IsMatch(instruction, platformChanged)) - return InstructionHandleResult.None; + // get method ref + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (!this.IsMatch(methodRef)) + return false; - MethodReference methodRef = (MethodReference)instruction.Operand; + // rewrite methodRef.DeclaringType = module.ImportReference(this.ToType); - return InstructionHandleResult.Rewritten; + return this.MarkRewritten(); } /********* - ** Protected methods + ** Private methods *********/ /// <summary>Get whether a CIL instruction matches.</summary> - /// <param name="instruction">The IL instruction.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - protected bool IsMatch(Instruction instruction, bool platformChanged) + /// <param name="methodRef">The method reference.</param> + private bool IsMatch(MethodReference methodRef) { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); return methodRef != null - && (platformChanged || !this.OnlyIfPlatformChanged) - && methodRef.DeclaringType.FullName == this.FromType.FullName + && methodRef.DeclaringType.FullName == this.FromType && RewriteHelper.HasMatchingSignature(this.ToType, methodRef); } } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs index 7e7c0efa..6ef18b26 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/StaticFieldToConstantRewriter.cs @@ -1,17 +1,23 @@ using System; using Mono.Cecil; using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites static field references into constant values.</summary> /// <typeparam name="TValue">The constant value type.</typeparam> - internal class StaticFieldToConstantRewriter<TValue> : FieldFinder + internal class StaticFieldToConstantRewriter<TValue> : BaseInstructionHandler { /********* ** Fields *********/ + /// <summary>The type containing the field to which references should be rewritten.</summary> + private readonly Type Type; + + /// <summary>The field name to which references should be rewritten.</summary> + private readonly string FromFieldName; + /// <summary>The constant value to replace with.</summary> private readonly TValue Value; @@ -24,24 +30,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <param name="fieldName">The field name to rewrite.</param> /// <param name="value">The constant value to replace with.</param> public StaticFieldToConstantRewriter(Type type, string fieldName, TValue value) - : base(type.FullName, fieldName, InstructionHandleResult.None) + : base(defaultPhrase: $"{type.FullName}.{fieldName} field") { + this.Type = type; + this.FromFieldName = fieldName; this.Value = value; } - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> + /// <summary>Rewrite a CIL instruction reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="instruction">The CIL instruction to handle.</param> + /// <param name="replaceWith">Replaces the CIL instruction with a new one.</param> + /// <returns>Returns whether the instruction was changed.</returns> + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action<Instruction> replaceWith) { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; + // get field reference + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName)) + return false; - cil.Replace(instruction, this.CreateConstantInstruction(cil, this.Value)); - return InstructionHandleResult.Rewritten; + // rewrite to constant + replaceWith(this.CreateConstantInstruction(cil, this.Value)); + return this.MarkRewritten(); } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs index fade082b..c2120444 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -1,12 +1,11 @@ using System; using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// <summary>Rewrites all references to a type.</summary> - internal class TypeReferenceRewriter : TypeFinder + internal class TypeReferenceRewriter : BaseInstructionHandler { /********* ** Fields @@ -17,6 +16,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>The new type to reference.</summary> private readonly Type ToType; + /// <summary>Get whether a matched type should be ignored.</summary> + private readonly Func<TypeReference, bool> ShouldIgnore; + /********* ** Public methods @@ -24,129 +26,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// <summary>Construct an instance.</summary> /// <param name="fromTypeFullName">The full type name to which to find references.</param> /// <param name="toType">The new type to reference.</param> - /// <param name="shouldIgnore">A lambda which overrides a matched type.</param> + /// <param name="shouldIgnore">Get whether a matched type should be ignored.</param> public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func<TypeReference, bool> shouldIgnore = null) - : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) + : base($"{fromTypeFullName} type") { this.FromTypeName = fromTypeFullName; this.ToType = toType; + this.ShouldIgnore = shouldIgnore; } - /// <summary>Perform the predefined logic for a method if applicable.</summary> + /// <summary>Rewrite a type reference if needed.</summary> /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="method">The method definition containing the instruction.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// <param name="type">The type definition to handle.</param> + /// <param name="replaceWith">Replaces the type reference with a new one.</param> + /// <returns>Returns whether the type was changed.</returns> + public override bool Handle(ModuleDefinition module, TypeReference type, Action<TypeReference> replaceWith) { - bool rewritten = false; - - // return type - if (this.IsMatch(method.ReturnType)) - { - this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - rewritten = true; - } - - // parameters - foreach (ParameterDefinition parameter in method.Parameters) - { - if (this.IsMatch(parameter.ParameterType)) - { - this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); - rewritten = true; - } - } - - // generic parameters - for (int i = 0; i < method.GenericParameters.Count; i++) - { - var parameter = method.GenericParameters[i]; - if (this.IsMatch(parameter)) - { - this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); - rewritten = true; - } - } - - // local variables - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - { - this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType); - rewritten = true; - } - } - - return rewritten - ? InstructionHandleResult.Rewritten - : InstructionHandleResult.None; - } - - /// <summary>Perform the predefined logic for an instruction if applicable.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="cil">The CIL processor.</param> - /// <param name="instruction">The instruction to handle.</param> - /// <param name="assemblyMap">Metadata for mapping assemblies to the current platform.</param> - /// <param name="platformChanged">Whether the mod was compiled on a different platform.</param> - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.IsMatch(instruction)) - return InstructionHandleResult.None; - - // field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) - { - this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); - this.RewriteIfNeeded(module, fieldRef.FieldType, newType => fieldRef.FieldType = newType); - } - - // method reference - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null) - { - this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); - this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType); - foreach (var parameter in methodRef.Parameters) - this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); - } - - // type reference - if (instruction.Operand is TypeReference typeRef) - this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType))); - - return InstructionHandleResult.Rewritten; - } - - /********* - ** Private methods - *********/ - /// <summary>Change a type reference if needed.</summary> - /// <param name="module">The assembly module containing the instruction.</param> - /// <param name="type">The type to replace if it matches.</param> - /// <param name="set">Assign the new type reference.</param> - private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action<TypeReference> set) - { - // current type - if (type.FullName == this.FromTypeName) - { - if (!this.ShouldIgnore(type)) - set(module.ImportReference(this.ToType)); - return; - } - - // recurse into generic arguments - if (type is GenericInstanceType genericType) - { - for (int i = 0; i < genericType.GenericArguments.Count; i++) - this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); - } + // check type reference + if (type.FullName != this.FromTypeName || this.ShouldIgnore?.Invoke(type) == true) + return false; - // recurse into generic parameters (e.g. constraints) - for (int i = 0; i < type.GenericParameters.Count; i++) - this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); + // rewrite to new type + replaceWith(module.ImportReference(this.ToType)); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs index 7dbfa767..ac9cf313 100644 --- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs +++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs @@ -45,23 +45,22 @@ namespace StardewModdingAPI.Framework.Networking [SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")] protected override void onReceiveMessage(GalaxyID peer, Stream messageStream) { - using (IncomingMessage message = new IncomingMessage()) - using (BinaryReader reader = new BinaryReader(messageStream)) + using IncomingMessage message = new IncomingMessage(); + using BinaryReader reader = new BinaryReader(messageStream); + + message.Read(reader); + ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead + this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () => { - message.Read(reader); - ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead - this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () => + if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) { - if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID) - this.gameServer.processIncomingMessage(message); - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); - GalaxyID capturedPeer = new GalaxyID(peerID); - this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), this.getConnectionId(peer), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); - } - }); - } + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + GalaxyID capturedPeer = new GalaxyID(peerID); + this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), this.getConnectionId(peer), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64()); + } + }); } /// <summary>Send a message to a remote peer.</summary> diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs index f2c61917..05c8b872 100644 --- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs +++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs @@ -44,25 +44,24 @@ namespace StardewModdingAPI.Framework.Networking { // add hook to call multiplayer core NetConnection peer = rawMessage.SenderConnection; - using (IncomingMessage message = new IncomingMessage()) - using (Stream readStream = new NetBufferReadStream(rawMessage)) - using (BinaryReader reader = new BinaryReader(readStream)) + using IncomingMessage message = new IncomingMessage(); + using Stream readStream = new NetBufferReadStream(rawMessage); + using BinaryReader reader = new BinaryReader(readStream); + + while (rawMessage.LengthBits - rawMessage.Position >= 8) { - while (rawMessage.LengthBits - rawMessage.Position >= 8) + message.Read(reader); + NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused + this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () => { - message.Read(reader); - NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused - this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () => + if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) + this.gameServer.processIncomingMessage(message); + else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) { - if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer) - this.gameServer.processIncomingMessage(message); - else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction) - { - NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); - this.gameServer.checkFarmhandRequest("", this.getConnectionId(rawMessage.SenderConnection), farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); - } - }); - } + NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader); + this.gameServer.checkFarmhandRequest("", this.getConnectionId(rawMessage.SenderConnection), farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer); + } + }); } } } diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index f82159d0..dcad285a 100644 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -1,5 +1,5 @@ using System; -using Harmony; +using HarmonyLib; namespace StardewModdingAPI.Framework.Patching { @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework.Patching /// <param name="patches">The patches to apply.</param> public void Apply(params IHarmonyPatch[] patches) { - HarmonyInstance harmony = HarmonyInstance.Create("io.smapi"); + Harmony harmony = new Harmony("SMAPI"); foreach (IHarmonyPatch patch in patches) { try diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs index cb42f40e..7d5eb3d4 100644 --- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs +++ b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs @@ -1,4 +1,4 @@ -using Harmony; +using HarmonyLib; namespace StardewModdingAPI.Framework.Patching { @@ -10,6 +10,6 @@ namespace StardewModdingAPI.Framework.Patching /// <summary>Apply the Harmony patch.</summary> /// <param name="harmony">The Harmony instance.</param> - void Apply(HarmonyInstance harmony); + void Apply(Harmony harmony); } } diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs deleted file mode 100644 index 4cb436f0..00000000 --- a/src/SMAPI/Framework/Patching/PatchHelper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.Patching -{ - /// <summary>Provides generic methods for implementing Harmony patches.</summary> - internal class PatchHelper - { - /********* - ** Fields - *********/ - /// <summary>The interception keys currently being intercepted.</summary> - private static readonly HashSet<string> InterceptingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - - - /********* - ** Public methods - *********/ - /// <summary>Track a method that will be intercepted.</summary> - /// <param name="key">The intercept key.</param> - /// <returns>Returns false if the method was already marked for interception, else true.</returns> - public static bool StartIntercept(string key) - { - return PatchHelper.InterceptingKeys.Add(key); - } - - /// <summary>Track a method as no longer being intercepted.</summary> - /// <param name="key">The intercept key.</param> - public static void StopIntercept(string key) - { - PatchHelper.InterceptingKeys.Remove(key); - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs index 01197f74..af630055 100644 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Framework.PerformanceMonitoring { /// <summary>The context for an alert.</summary> - internal struct AlertContext + internal readonly struct AlertContext { /********* ** Accessors diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs index f5b80189..d5a0b343 100644 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs @@ -1,7 +1,7 @@ namespace StardewModdingAPI.Framework.PerformanceMonitoring { /// <summary>A single alert entry.</summary> - internal struct AlertEntry + internal readonly struct AlertEntry { /********* ** Accessors diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs index cff502ad..1746e358 100644 --- a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs +++ b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Framework.PerformanceMonitoring { /// <summary>A peak invocation time.</summary> - internal struct PeakEntry + internal readonly struct PeakEntry { /********* ** Accessors diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs index 3cf668ee..42825999 100644 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Harmony; namespace StardewModdingAPI.Framework.PerformanceMonitoring { @@ -57,7 +56,7 @@ namespace StardewModdingAPI.Framework.PerformanceMonitoring // add entry if (this.Entries.Count > this.MaxEntries) this.Entries.Pop(); - this.Entries.Add(entry); + this.Entries.Push(entry); // update metrics if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs index 8adbd88d..18cca628 100644 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs @@ -3,7 +3,7 @@ using System; namespace StardewModdingAPI.Framework.PerformanceMonitoring { /// <summary>A single performance counter entry.</summary> - internal struct PerformanceCounterEntry + internal readonly struct PerformanceCounterEntry { /********* ** Accessors diff --git a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs index 382949bf..85e69ae6 100644 --- a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs +++ b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs @@ -2,7 +2,6 @@ using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; -using StardewValley; using xTile.Dimensions; using xTile.Layers; using xTile.ObjectModel; @@ -14,23 +13,13 @@ namespace StardewModdingAPI.Framework.Rendering internal class SDisplayDevice : SXnaDisplayDevice { /********* - ** Fields - *********/ - /// <summary>The origin to use when rotating tiles.</summary> - private readonly Vector2 RotationOrigin; - - - /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="contentManager">The content manager through which to load tiles.</param> /// <param name="graphicsDevice">The graphics device with which to render tiles.</param> public SDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice) - : base(contentManager, graphicsDevice) - { - this.RotationOrigin = new Vector2((Game1.tileSize * Game1.pixelZoom) / 2f); - } + : base(contentManager, graphicsDevice) { } /// <summary>Draw a tile to the screen.</summary> /// <param name="tile">The tile to draw.</param> diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs index d4f62b4f..121e53bc 100644 --- a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs +++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs @@ -10,7 +10,7 @@ using xTile.Layers; using xTile.Tiles; using Rectangle = xTile.Dimensions.Rectangle; -namespace StardewModdingAPI.Framework +namespace StardewModdingAPI.Framework.Rendering { /// <summary>A map display device which reimplements the default logic.</summary> /// <remarks>This is an exact copy of <see cref="XnaDisplayDevice"/>, except that private fields are protected and all methods are virtual.</remarks> diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c6e69d4e..530b6754 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,10 @@ namespace StardewModdingAPI.Framework { // prepare console this.Monitor.Log("Type 'help' for help, or 'help <cmd>' 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 <cmd>\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 HarmonySummaryCommand(), this.Monitor) + .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input Thread inputThread = new Thread(() => @@ -1129,67 +1132,115 @@ 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<string> labels = new List<string>(); + 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." ); } } + /// <summary>Write a mod warning group to the console and log.</summary> + /// <param name="mods">The mods to search.</param> + /// <param name="match">Matches mods to include in the warning group.</param> + /// <param name="level">The log level for the logged messages.</param> + /// <param name="heading">A brief heading label for the group.</param> + /// <param name="blurb">A detailed explanation of the warning, split into lines.</param> + /// <param name="modLabel">Formats the mod label, or <c>null</c> to use the <see cref="IModMetadata.DisplayName"/>.</param> + private void LogModWarningGroup(IModMetadata[] mods, Func<IModMetadata, bool> match, LogLevel level, string heading, string[] blurb, Func<IModMetadata, string> modLabel = null) + { + // get matching mods + string[] modLabels = mods + .Where(match) + .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName) + .OrderBy(p => p) + .ToArray(); + if (!modLabels.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 (string label in modLabels) + this.Monitor.Log($" - {label}", level); + + this.Monitor.Newline(); + } + + /// <summary>Write a mod warning group to the console and log.</summary> + /// <param name="mods">The mods to search.</param> + /// <param name="warning">The mod warning to match.</param> + /// <param name="level">The log level for the logged messages.</param> + /// <param name="heading">A brief heading label for the group.</param> + /// <param name="blurb">A detailed explanation of the warning, split into lines.</param> + void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) + { + this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); + } + /// <summary>Load a mod's entry class.</summary> /// <param name="modAssembly">The mod assembly.</param> /// <param name="mod">The loaded instance.</param> @@ -1225,6 +1276,12 @@ namespace StardewModdingAPI.Framework } /// <summary>Reload translations for all mods.</summary> + private void ReloadTranslations() + { + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + } + + /// <summary>Reload translations for the given mods.</summary> /// <param name="mods">The mods for which to reload translations.</param> private void ReloadTranslations(IEnumerable<IModMetadata> mods) { @@ -1309,48 +1366,6 @@ namespace StardewModdingAPI.Framework return translations; } - /// <summary>The method called when the user submits a core SMAPI command in the console.</summary> - /// <param name="name">The command name.</param> - /// <param name="arguments">The command arguments.</param> - 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<string, string>[] 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}'."); - } - } - /// <summary>Redirect messages logged directly to the console to the given monitor.</summary> /// <param name="gameMonitor">The monitor with which to log messages as the game.</param> /// <param name="message">The message to log.</param> diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 23358afb..4d310185 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -1310,7 +1310,7 @@ namespace StardewModdingAPI.Framework } Game1.drawPlayerHeldObject(Game1.player); } - label_139: + label_139: if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) Game1.drawTool(Game1.player); if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs index 821c343f..8c444e45 100644 --- a/src/SMAPI/Framework/SMultiplayer.cs +++ b/src/SMAPI/Framework/SMultiplayer.cs @@ -231,7 +231,11 @@ namespace StardewModdingAPI.Framework this.AddPeer(peer, canBeHost: false); } + // let game handle connection resume(); + + // raise event + this.EventManager.PeerConnected.Raise(new PeerConnectedEventArgs(this.Peers[message.FarmerID])); break; // handle mod message diff --git a/src/SMAPI/Framework/Serialization/ColorConverter.cs b/src/SMAPI/Framework/Serialization/ColorConverter.cs index 19979981..7315f1a5 100644 --- a/src/SMAPI/Framework/Serialization/ColorConverter.cs +++ b/src/SMAPI/Framework/Serialization/ColorConverter.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI.Framework.Serialization { string[] parts = str.Split(','); if (parts.Length != 4) - throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path})."); + throw new SParseException($"Can't parse {nameof(Color)} from invalid value '{str}' (path: {path})."); int r = Convert.ToInt32(parts[0]); int g = Convert.ToInt32(parts[1]); diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs index 3481c9b2..6cf795dc 100644 --- a/src/SMAPI/Framework/Serialization/PointConverter.cs +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.Serialization { string[] parts = str.Split(','); if (parts.Length != 2) - throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path})."); + throw new SParseException($"Can't parse {nameof(Point)} from invalid value '{str}' (path: {path})."); int x = Convert.ToInt32(parts[0]); int y = Convert.ToInt32(parts[1]); diff --git a/src/SMAPI/Framework/Serialization/RectangleConverter.cs b/src/SMAPI/Framework/Serialization/RectangleConverter.cs index fbb2e253..a5780d8a 100644 --- a/src/SMAPI/Framework/Serialization/RectangleConverter.cs +++ b/src/SMAPI/Framework/Serialization/RectangleConverter.cs @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Framework.Serialization var match = Regex.Match(str, @"^\{X:(?<x>\d+) Y:(?<y>\d+) Width:(?<width>\d+) Height:(?<height>\d+)\}$", RegexOptions.IgnoreCase); if (!match.Success) - throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path})."); + throw new SParseException($"Can't parse {nameof(Rectangle)} from invalid value '{str}' (path: {path})."); int x = Convert.ToInt32(match.Groups["x"].Value); int y = Convert.ToInt32(match.Groups["y"].Value); diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs index 1d9b08e0..3e2ab776 100644 --- a/src/SMAPI/Framework/Serialization/Vector2Converter.cs +++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.Serialization { string[] parts = str.Split(','); if (parts.Length != 2) - throw new SParseException($"Can't parse {typeof(Vector2).Name} from invalid value '{str}' (path: {path})."); + throw new SParseException($"Can't parse {nameof(Vector2)} from invalid value '{str}' (path: {path})."); float x = Convert.ToSingle(parts[0]); float y = Convert.ToSingle(parts[1]); |