From 4eb68e96ed2b986cf2db621b24f4ebbdd0cf83f1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 4 May 2020 17:41:45 -0400 Subject: fix asset propagation for Gil's portraits --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 0a14086b..fa6541cb 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -816,9 +816,18 @@ namespace StardewModdingAPI.Metadata where key != null && lookup.Contains(key) select new { Npc = npc, Key = key } ) - .ToArray(); - if (!characters.Any()) - return; + .ToList(); + + // special case: Gil is a private NPC field on the AdventureGuild class (only used for the portrait) + { + string gilKey = this.NormalizeAssetNameIgnoringEmpty("Portraits/Gil"); + if (lookup.Contains(gilKey)) + { + GameLocation adventureGuild = Game1.getLocationFromName("AdventureGuild"); + if (adventureGuild != null) + characters.Add(new { Npc = this.Reflection.GetField(adventureGuild, "Gil").GetValue(), Key = gilKey }); + } + } // update portrait foreach (var target in characters) -- cgit From 9728fe3f347328323ff79c6c93df2ab390f6070e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 4 May 2020 17:53:48 -0400 Subject: add Multiplayer.PeerConnected event --- docs/release-notes.md | 1 + src/SMAPI/Events/IMultiplayerEvents.cs | 5 ++++- src/SMAPI/Events/PeerConnectedEventArgs.cs | 25 ++++++++++++++++++++++ src/SMAPI/Framework/Events/EventManager.cs | 6 +++++- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 9 +++++++- src/SMAPI/Framework/SMultiplayer.cs | 4 ++++ 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI/Events/PeerConnectedEventArgs.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index cb177ca0..8037c10c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming released * For modders: + * Added `Multiplayer.PeerConnected` event. * Fixed asset propagation for Gil's portraits. ## 3.5 diff --git a/src/SMAPI/Events/IMultiplayerEvents.cs b/src/SMAPI/Events/IMultiplayerEvents.cs index 4a31f48e..af9b5f17 100644 --- a/src/SMAPI/Events/IMultiplayerEvents.cs +++ b/src/SMAPI/Events/IMultiplayerEvents.cs @@ -5,9 +5,12 @@ namespace StardewModdingAPI.Events /// Events raised for multiplayer messages and connections. public interface IMultiplayerEvents { - /// 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. + /// 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. event EventHandler PeerContextReceived; + /// Raised after a peer connection is approved by the game. + event EventHandler PeerConnected; + /// Raised after a mod message is received over the network. event EventHandler ModMessageReceived; diff --git a/src/SMAPI/Events/PeerConnectedEventArgs.cs b/src/SMAPI/Events/PeerConnectedEventArgs.cs new file mode 100644 index 00000000..bfaa2bd3 --- /dev/null +++ b/src/SMAPI/Events/PeerConnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class PeerConnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The peer whose metadata was received. + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The peer whose metadata was received. + internal PeerConnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index a9dfda97..cc718d2c 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -109,9 +109,12 @@ namespace StardewModdingAPI.Framework.Events /**** ** Multiplayer ****/ - /// 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. + /// Raised after the mod context for a peer is received. This happens before the game approves the connection (), so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. public readonly ManagedEvent PeerContextReceived; + /// Raised after a peer connection is approved by the game. + public readonly ManagedEvent PeerConnected; + /// Raised after a mod message is received over the network. public readonly ManagedEvent ModMessageReceived; @@ -218,6 +221,7 @@ namespace StardewModdingAPI.Framework.Events this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); + this.PeerConnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerConnected)); this.ModMessageReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.ModMessageReceived)); this.PeerDisconnected = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerDisconnected)); diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 152c4e0c..9f76511e 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -9,13 +9,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Accessors *********/ - /// 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. + /// Raised after the mod context for a peer is received. This happens before the game approves the connection (), so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. public event EventHandler PeerContextReceived { add => this.EventManager.PeerContextReceived.Add(value); remove => this.EventManager.PeerContextReceived.Remove(value); } + /// Raised after a peer connection is approved by the game. + public event EventHandler PeerConnected + { + add => this.EventManager.PeerConnected.Add(value); + remove => this.EventManager.PeerConnected.Remove(value); + } + /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { 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 -- cgit From e08979acd301792435661d2c0008469a800f8dbb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 20:49:05 -0400 Subject: migrate to Harmony 2.0 (#711) --- src/SMAPI/Framework/Patching/GamePatcher.cs | 4 ++-- src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 4 ++-- src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs | 3 +-- src/SMAPI/Patches/DialogueErrorPatch.cs | 4 ++-- src/SMAPI/Patches/EventErrorPatch.cs | 4 ++-- src/SMAPI/Patches/LoadContextPatch.cs | 4 ++-- src/SMAPI/Patches/LoadErrorPatch.cs | 4 ++-- src/SMAPI/Patches/ObjectErrorPatch.cs | 4 ++-- src/SMAPI/Patches/ScheduleErrorPatch.cs | 4 ++-- src/SMAPI/SMAPI.csproj | 2 +- 10 files changed, 18 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index f82159d0..cdb54453 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 /// The patches to apply. public void Apply(params IHarmonyPatch[] patches) { - HarmonyInstance harmony = HarmonyInstance.Create("io.smapi"); + Harmony harmony = new Harmony("io.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 /// Apply the Harmony patch. /// The Harmony instance. - void Apply(HarmonyInstance harmony); + void Apply(Harmony harmony); } } 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/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index 1e49826d..80029540 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { harmony.Patch( original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index 504d1d2e..a2b94e8b 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { harmony.Patch( original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 0cc8c8eb..9c707676 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { // detect CreatedBasicInfo harmony.Patch( diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index 77415ff2..f8ad6693 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { harmony.Patch( original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index d3b8800a..b9655043 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; using StardewValley.Menus; @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Patches *********/ /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { // object.getDescription harmony.Patch( diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index 799fcb40..386230a6 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Harmony; +using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -39,7 +39,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) + public void Apply(Harmony harmony) { harmony.Patch( original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 5f41387b..1755b9e7 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -16,7 +16,7 @@ - + -- cgit From 2d37fe6819dd15a6e995ea55d625179106c22cd7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 20:54:25 -0400 Subject: rename files for upcoming change (#711) --- .../Framework/ModLoading/Finders/TypeFinder.cs | 139 ------------------- .../ModLoading/Framework/BaseTypeFinder.cs | 139 +++++++++++++++++++ .../Framework/BaseTypeReferenceRewriter.cs | 151 ++++++++++++++++++++ .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 152 --------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 1 + 5 files changed, 291 insertions(+), 291 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs deleted file mode 100644 index 701b15f2..00000000 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Finders -{ - /// Finds incompatible CIL instructions that reference a given type. - internal class TypeFinder : IInstructionHandler - { - /********* - ** Accessors - *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - /// A lambda which overrides a matched type. - protected readonly Func ShouldIgnore; - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name to match. - /// The result to return for matching instructions. - /// A lambda which overrides a matched type. - public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) - { - this.FullTypeName = fullTypeName; - this.Result = result; - this.NounPhrase = $"{fullTypeName} type"; - this.ShouldIgnore = shouldIgnore ?? (p => false); - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(method) - ? this.Result - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Get whether a CIL instruction matches. - /// The method definition. - protected bool IsMatch(MethodDefinition method) - { - if (this.IsMatch(method.ReturnType)) - return true; - - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - return true; - } - - return false; - } - - /// Get whether a CIL instruction matches. - /// The IL instruction. - 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; - } - - /// Get whether a type reference matches the expected type. - /// The type to check. - 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; - - return false; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs new file mode 100644 index 00000000..170bbb48 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : IInstructionHandler + { + /********* + ** Accessors + *********/ + /// The full type name for which to find references. + private readonly string FullTypeName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + /// A lambda which overrides a matched type. + protected readonly Func ShouldIgnore; + + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the instruction finder matches. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to match. + /// The result to return for matching instructions. + /// A lambda which overrides a matched type. + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) + { + this.FullTypeName = fullTypeName; + this.Result = result; + this.NounPhrase = $"{fullTypeName} type"; + this.ShouldIgnore = shouldIgnore ?? (p => false); + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(method) + ? this.Result + : InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return this.IsMatch(instruction) + ? this.Result + : InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Get whether a CIL instruction matches. + /// The method definition. + protected bool IsMatch(MethodDefinition method) + { + if (this.IsMatch(method.ReturnType)) + return true; + + foreach (VariableDefinition variable in method.Body.Variables) + { + if (this.IsMatch(variable.VariableType)) + return true; + } + + return false; + } + + /// Get whether a CIL instruction matches. + /// The IL instruction. + 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; + } + + /// Get whether a type reference matches the expected type. + /// The type to check. + 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; + + return false; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs new file mode 100644 index 00000000..8c2d11c8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -0,0 +1,151 @@ +using System; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// Rewrites all references to a type. + internal class TypeReferenceRewriter : TypeFinder + { + /********* + ** Fields + *********/ + /// The full type name to which to find references. + private readonly string FromTypeName; + + /// The new type to reference. + private readonly Type ToType; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to which to find references. + /// The new type to reference. + /// A lambda which overrides a matched type. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) + : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) + { + this.FromTypeName = fromTypeFullName; + this.ToType = toType; + } + + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition containing the instruction. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + 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; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + 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 + *********/ + /// Change a type reference if needed. + /// The assembly module containing the instruction. + /// The type to replace if it matches. + /// Assign the new type reference. + private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action 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); + } + + // 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)); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs deleted file mode 100644 index fade082b..00000000 --- a/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using Mono.Cecil; -using Mono.Cecil.Cil; -using StardewModdingAPI.Framework.ModLoading.Finders; - -namespace StardewModdingAPI.Framework.ModLoading.Rewriters -{ - /// Rewrites all references to a type. - internal class TypeReferenceRewriter : TypeFinder - { - /********* - ** Fields - *********/ - /// The full type name to which to find references. - private readonly string FromTypeName; - - /// The new type to reference. - private readonly Type ToType; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The full type name to which to find references. - /// The new type to reference. - /// A lambda which overrides a matched type. - public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) - : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) - { - this.FromTypeName = fromTypeFullName; - this.ToType = toType; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - 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; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - 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 - *********/ - /// Change a type reference if needed. - /// The assembly module containing the instruction. - /// The type to replace if it matches. - /// Assign the new type reference. - private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action 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); - } - - // 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)); - } - } -} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index eee5c235..f3d6e6db 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -3,6 +3,7 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.Rewriters; using StardewModdingAPI.Framework.RewriteFacades; using StardewValley; -- cgit From f4192663d78c7a45418f07f0bf4acb67b11291fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 20:53:02 -0400 Subject: add Harmony 2.0 rewriters (#711) --- .../ModLoading/Finders/TypeAssemblyFinder.cs | 25 ++++++ .../Framework/ModLoading/Finders/TypeFinder.cs | 25 ++++++ .../ModLoading/Framework/BaseTypeFinder.cs | 34 +++----- .../Framework/BaseTypeReferenceRewriter.cs | 97 ++++++++-------------- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 2 +- .../Rewriters/Harmony1AssemblyRewriter.cs | 77 +++++++++++++++++ .../ModLoading/Rewriters/MethodParentRewriter.cs | 18 ++-- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 68 +++++++++++++++ .../RewriteFacades/HarmonyInstanceMethods.cs | 33 ++++++++ src/SMAPI/Metadata/InstructionMetadata.cs | 5 +- 10 files changed, 295 insertions(+), 89 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs create mode 100644 src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs new file mode 100644 index 00000000..5301186b --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs @@ -0,0 +1,25 @@ +using System; +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference types in a given assembly. + internal class TypeAssemblyFinder : BaseTypeFinder + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full assembly name to which to find references. + /// The result to return for matching instructions. + /// A lambda which overrides a matched type. + public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func shouldIgnore = null) + : base( + isMatch: type => type.Scope.Name == assemblyName && (shouldIgnore == null || !shouldIgnore(type)), + result: result, + nounPhrase: $"{assemblyName} assembly" + ) + { } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs new file mode 100644 index 00000000..3adc31c7 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -0,0 +1,25 @@ +using System; +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Finders +{ + /// Finds incompatible CIL instructions that reference a given type. + internal class TypeFinder : BaseTypeFinder + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to match. + /// The result to return for matching instructions. + /// A lambda which overrides a matched type. + public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) + : base( + isMatch: type => type.FullName == fullTypeName && (shouldIgnore == null || !shouldIgnore(type)), + result: result, + nounPhrase: $"{fullTypeName} type" + ) + { } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index 170bbb48..b1547334 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -5,21 +5,18 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Framework { - /// Finds incompatible CIL instructions that reference a given type. - internal class TypeFinder : IInstructionHandler + /// Finds incompatible CIL type reference instructions. + internal abstract class BaseTypeFinder : IInstructionHandler { /********* ** Accessors *********/ - /// The full type name for which to find references. - private readonly string FullTypeName; + /// Matches the type references to handle. + private readonly Func IsMatchImpl; /// The result to return for matching instructions. private readonly InstructionHandleResult Result; - /// A lambda which overrides a matched type. - protected readonly Func ShouldIgnore; - /********* ** Accessors @@ -32,15 +29,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Public methods *********/ /// Construct an instance. - /// The full type name to match. + /// Matches the type references to handle. /// The result to return for matching instructions. - /// A lambda which overrides a matched type. - public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) + /// A brief noun phrase indicating what the instruction finder matches. + public BaseTypeFinder(Func isMatch, InstructionHandleResult result, string nounPhrase) { - this.FullTypeName = fullTypeName; + this.IsMatchImpl = isMatch; this.Result = result; - this.NounPhrase = $"{fullTypeName} type"; - this.ShouldIgnore = shouldIgnore ?? (p => false); + this.NounPhrase = nounPhrase; } /// Perform the predefined logic for a method if applicable. @@ -68,13 +64,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework : InstructionHandleResult.None; } - - /********* - ** Protected methods - *********/ /// Get whether a CIL instruction matches. /// The method definition. - protected bool IsMatch(MethodDefinition method) + public bool IsMatch(MethodDefinition method) { if (this.IsMatch(method.ReturnType)) return true; @@ -90,7 +82,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Get whether a CIL instruction matches. /// The IL instruction. - protected bool IsMatch(Instruction instruction) + public bool IsMatch(Instruction instruction) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -116,10 +108,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Get whether a type reference matches the expected type. /// The type to check. - protected bool IsMatch(TypeReference type) + public bool IsMatch(TypeReference type) { // root type - if (type.FullName == this.FullTypeName && !this.ShouldIgnore(type)) + if (this.IsMatchImpl(type)) return true; // generic arguments diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs index 8c2d11c8..55ce6b5a 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -5,30 +5,32 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Framework { /// Rewrites all references to a type. - internal class TypeReferenceRewriter : TypeFinder + internal abstract class BaseTypeReferenceRewriter : IInstructionHandler { /********* ** Fields *********/ - /// The full type name to which to find references. - private readonly string FromTypeName; + /// The type finder which matches types to rewrite. + private readonly BaseTypeFinder Finder; - /// The new type to reference. - private readonly Type ToType; + + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the handler matches. + public string NounPhrase { get; } /********* ** Public methods *********/ /// Construct an instance. - /// The full type name to which to find references. - /// The new type to reference. - /// A lambda which overrides a matched type. - public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) - : base(fromTypeFullName, InstructionHandleResult.None, shouldIgnore) + /// The type finder which matches types to rewrite. + /// A brief noun phrase indicating what the instruction finder matches. + public BaseTypeReferenceRewriter(BaseTypeFinder finder, string nounPhrase) { - this.FromTypeName = fromTypeFullName; - this.ToType = toType; + this.Finder = finder; + this.NounPhrase = nounPhrase; } /// Perform the predefined logic for a method if applicable. @@ -36,46 +38,36 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method definition containing the instruction. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; // return type - if (this.IsMatch(method.ReturnType)) + if (this.Finder.IsMatch(method.ReturnType)) { - this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - rewritten = true; + rewritten |= this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); } // parameters foreach (ParameterDefinition parameter in method.Parameters) { - if (this.IsMatch(parameter.ParameterType)) - { - this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); - rewritten = true; - } + if (this.Finder.IsMatch(parameter.ParameterType)) + rewritten |= this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); } // 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; - } + if (this.Finder.IsMatch(parameter)) + rewritten |= this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); } // 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; - } + if (this.Finder.IsMatch(variable.VariableType)) + rewritten |= this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType); } return rewritten @@ -89,34 +81,37 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - if (!this.IsMatch(instruction)) + if (!this.Finder.IsMatch(instruction)) return InstructionHandleResult.None; + bool rewritten = false; // 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); + rewritten |= this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); + rewritten |= 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); + rewritten |= this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); + rewritten |= this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType); foreach (var parameter in methodRef.Parameters) - this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); + rewritten |= 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))); + rewritten |= this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType))); - return InstructionHandleResult.Rewritten; + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; } /********* @@ -126,26 +121,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The assembly module containing the instruction. /// The type to replace if it matches. /// Assign the new type reference. - private void RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action 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); - } - - // 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)); - } + protected abstract bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set); } } diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index f8f10dc4..d9a49cfa 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -103,7 +103,7 @@ namespace StardewModdingAPI.Framework.ModLoading public static bool HasMatchingSignature(Type type, MethodReference 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/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs new file mode 100644 index 00000000..29e44bfe --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -0,0 +1,77 @@ +using System; +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites Harmony 1.x assembly references to work with Harmony 2.x. + internal class Harmony1AssemblyRewriter : BaseTypeReferenceRewriter + { + /********* + ** Fields + *********/ + /// The full assembly name to which to find references. + private const string FromAssemblyName = "0Harmony"; + + /// The main Harmony type. + private readonly Type HarmonyType = typeof(HarmonyLib.Harmony); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Harmony1AssemblyRewriter() + : base(new TypeAssemblyFinder(Harmony1AssemblyRewriter.FromAssemblyName, InstructionHandleResult.None), "Harmony 1.x types") + { } + + + /********* + ** Private methods + *********/ + /// Change a type reference if needed. + /// The assembly module containing the instruction. + /// The type to replace if it matches. + /// Assign the new type reference. + protected override bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) + { + bool rewritten = false; + + // current type + if (type.Scope.Name == Harmony1AssemblyRewriter.FromAssemblyName && type.Scope is AssemblyNameReference assemblyScope && assemblyScope.Version.Major == 1) + { + Type targetType = this.GetMappedType(type); + set(module.ImportReference(targetType)); + return true; + } + + // recurse into generic arguments + if (type is GenericInstanceType genericType) + { + for (int i = 0; i < genericType.GenericArguments.Count; i++) + rewritten |= this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); + } + + // recurse into generic parameters (e.g. constraints) + for (int i = 0; i < type.GenericParameters.Count; i++) + rewritten |= this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); + + return rewritten; + } + + /// Get an equivalent Harmony 2.x type. + /// The Harmony 1.x method. + private Type GetMappedType(TypeReference type) + { + // main Harmony object + if (type.FullName == "Harmony.HarmonyInstance") + return this.HarmonyType; + + // other objects + string fullName = type.FullName.Replace("Harmony.", "HarmonyLib."); + string targetName = this.HarmonyType.AssemblyQualifiedName.Replace(this.HarmonyType.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..0984dc44 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,8 +11,8 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /********* ** Fields *********/ - /// The type whose methods to remap. - private readonly Type FromType; + /// The full name of the type whose methods to remap. + private readonly string FromType; /// The type with methods to map to. private readonly Type ToType; @@ -34,14 +35,21 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type whose methods to remap. /// The type with methods to map to. /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) + public MethodParentRewriter(string fromType, Type toType, bool onlyIfPlatformChanged = false) { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = $"{fromType.Name} methods"; + this.NounPhrase = $"{fromType.Split('.').Last()} methods"; this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } + /// Construct an instance. + /// The type whose methods to remap. + /// The type with methods to map to. + /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) + : this(fromType.FullName, toType, onlyIfPlatformChanged) { } + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition containing the instruction. @@ -81,7 +89,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters 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/TypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs new file mode 100644 index 00000000..d95e5ac9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Rewriters/TypeReferenceRewriter.cs @@ -0,0 +1,68 @@ +using System; +using Mono.Cecil; +using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.Framework; + +namespace StardewModdingAPI.Framework.ModLoading.Rewriters +{ + /// Rewrites all references to a type. + internal class TypeReferenceRewriter : BaseTypeReferenceRewriter + { + /********* + ** Fields + *********/ + /// The full type name to which to find references. + private readonly string FromTypeName; + + /// The new type to reference. + private readonly Type ToType; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The full type name to which to find references. + /// The new type to reference. + /// A lambda which overrides a matched type. + public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) + : base(new TypeFinder(fromTypeFullName, InstructionHandleResult.None, shouldIgnore), $"{fromTypeFullName} type") + { + this.FromTypeName = fromTypeFullName; + this.ToType = toType; + } + + + /********* + ** Protected methods + *********/ + /// Change a type reference if needed. + /// The assembly module containing the instruction. + /// The type to replace if it matches. + /// Assign the new type reference. + protected override bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) + { + bool rewritten = false; + + // current type + if (type.FullName == this.FromTypeName) + { + set(module.ImportReference(this.ToType)); + return true; + } + + // recurse into generic arguments + if (type is GenericInstanceType genericType) + { + for (int i = 0; i < genericType.GenericArguments.Count; i++) + rewritten |= this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); + } + + // recurse into generic parameters (e.g. constraints) + for (int i = 0; i < type.GenericParameters.Count; i++) + rewritten |= this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); + + return rewritten; + } + } +} diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs new file mode 100644 index 00000000..0f906f51 --- /dev/null +++ b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs @@ -0,0 +1,33 @@ +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + public class HarmonyInstanceMethods : Harmony + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique patch identifier. + public HarmonyInstanceMethods(string id) + : base(id) { } + + /// Creates a new Harmony instance. + /// A unique identifier for the instance. + public static Harmony Create(string id) + { + return new Harmony(id); + } + + public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); + return new DynamicMethod(method.Name, method.Attributes, method.CallingConvention, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray(), method.Module, true); + } + } +} diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index f3d6e6db..fb7141e7 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -3,7 +3,6 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; -using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Framework.ModLoading.Rewriters; using StardewModdingAPI.Framework.RewriteFacades; using StardewValley; @@ -37,6 +36,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); + // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) + yield return new Harmony1AssemblyRewriter(); + yield return new MethodParentRewriter("HarmonyLib.Harmony", typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false); + /**** ** detect mod issues ****/ -- cgit From 499cd8ab317080096c373c6ed6649bd51fb01c7d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 21:45:53 -0400 Subject: combine Harmony 1.x rewrite logs (#711) --- .../Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs | 9 ++++++++- src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs | 5 +++-- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index 29e44bfe..9faca235 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -18,12 +18,19 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters private readonly Type HarmonyType = typeof(HarmonyLib.Harmony); + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the rewriter matches. + public const string DefaultNounPhrase = "Harmony 1.x"; + + /********* ** Public methods *********/ /// Construct an instance. public Harmony1AssemblyRewriter() - : base(new TypeAssemblyFinder(Harmony1AssemblyRewriter.FromAssemblyName, InstructionHandleResult.None), "Harmony 1.x types") + : base(new TypeAssemblyFinder(Harmony1AssemblyRewriter.FromAssemblyName, InstructionHandleResult.None), Harmony1AssemblyRewriter.DefaultNounPhrase) { } diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index 0984dc44..c4c740b3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -35,11 +35,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type whose methods to remap. /// The type with methods to map to. /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - public MethodParentRewriter(string fromType, Type toType, bool onlyIfPlatformChanged = false) + /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). + public MethodParentRewriter(string fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = $"{fromType.Split('.').Last()} methods"; + this.NounPhrase = nounPhrase ?? $"{fromType.Split('.').Last()} methods"; this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index fb7141e7..665147e4 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); - yield return new MethodParentRewriter("HarmonyLib.Harmony", typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false); + yield return new MethodParentRewriter("HarmonyLib.Harmony", typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); /**** ** detect mod issues -- cgit From 7a60e6d2a1fbddc60a4052ba577e4f64c35736b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 22:15:38 -0400 Subject: migrate to Harmony 2.0 finalizers (#711) --- src/SMAPI/Framework/Patching/PatchHelper.cs | 34 ------------- src/SMAPI/Patches/DialogueErrorPatch.cs | 75 +++++++++-------------------- src/SMAPI/Patches/EventErrorPatch.cs | 31 ++++-------- src/SMAPI/Patches/ObjectErrorPatch.cs | 39 +++++---------- src/SMAPI/Patches/ScheduleErrorPatch.cs | 31 ++++-------- 5 files changed, 54 insertions(+), 156 deletions(-) delete mode 100644 src/SMAPI/Framework/Patching/PatchHelper.cs (limited to 'src') 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 -{ - /// Provides generic methods for implementing Harmony patches. - internal class PatchHelper - { - /********* - ** Fields - *********/ - /// The interception keys currently being intercepted. - private static readonly HashSet InterceptingKeys = new HashSet(StringComparer.OrdinalIgnoreCase); - - - /********* - ** Public methods - *********/ - /// Track a method that will be intercepted. - /// The intercept key. - /// Returns false if the method was already marked for interception, else true. - public static bool StartIntercept(string key) - { - return PatchHelper.InterceptingKeys.Add(key); - } - - /// Track a method as no longer being intercepted. - /// The intercept key. - public static void StopIntercept(string key) - { - PatchHelper.InterceptingKeys.Remove(key); - } - } -} diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index 80029540..cddf29d6 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using HarmonyLib; +using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -51,11 +51,11 @@ namespace StardewModdingAPI.Patches { harmony.Patch( original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), - prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_Dialogue_Constructor)) + finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_Dialogue_Constructor)) ); harmony.Patch( original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, - prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue)) + finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_NPC_CurrentDialogue)) ); } @@ -63,71 +63,44 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ - /// The method to call instead of the Dialogue constructor. + /// The method to call after the Dialogue constructor. /// The instance being patched. /// The dialogue being parsed. /// The NPC for which the dialogue is being parsed. - /// Returns whether to execute the original method. - private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker, Exception __exception) { - // get private members - bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); - IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); - IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); - IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); - IReflectedField> dialogues = DialogueErrorPatch.Reflection.GetField>(__instance, "dialogues"); - - // replicate base constructor - if (dialogues.GetValue() == null) - dialogues.SetValue(new List()); - - // duplicate code with try..catch - try - { - if (!nameArraysTranslated) - translateArraysOfStrings.Invoke(); - __instance.speaker = speaker; - parseDialogueString.Invoke(masterDialogue); - checkForSpecialDialogueAttributes.Invoke(); - } - catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + if (__exception != null) { + // log message string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; - DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{__exception.GetLogSummary()}", LogLevel.Error); + // set default dialogue + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); parseDialogueString.Invoke("..."); checkForSpecialDialogueAttributes.Invoke(); } - return false; + return null; } - /// The method to call instead of . + /// The method to call after . /// The instance being patched. /// The return value of the original method. - /// The method being wrapped. - /// Returns whether to execute the original method. - private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_NPC_CurrentDialogue(NPC __instance, ref Stack __result, Exception __exception) { - const string key = nameof(Before_NPC_CurrentDialogue); - if (!PatchHelper.StartIntercept(key)) - return true; + if (__exception == null) + return null; - try - { - __result = (Stack)__originalMethod.Invoke(__instance, new object[0]); - return false; - } - catch (TargetInvocationException ex) - { - DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{ex.InnerException ?? ex}", LogLevel.Error); - __result = new Stack(); - return false; - } - finally - { - PatchHelper.StopIntercept(key); - } + DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{__exception.GetLogSummary()}", LogLevel.Error); + __result = new Stack(); + + return null; } } } diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index a2b94e8b..de9dea29 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -1,5 +1,5 @@ +using System; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Patches { harmony.Patch( original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), - prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) ); } @@ -51,32 +51,19 @@ namespace StardewModdingAPI.Patches ** Private methods *********/ /// The method to call instead of the GameLocation.CheckEventPrecondition. - /// The instance being patched. /// The return value of the original method. /// The precondition to be parsed. - /// The method being wrapped. - /// Returns whether to execute the original method. - private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_GameLocation_CheckEventPrecondition(ref int __result, string precondition, Exception __exception) { - const string key = nameof(Before_GameLocation_CheckEventPrecondition); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); - return false; - } - catch (TargetInvocationException ex) + if (__exception != null) { __result = -1; - EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); - return false; - } - finally - { - PatchHelper.StopIntercept(key); + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{__exception.InnerException}", LogLevel.Error); } + + return null; } } } diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index b9655043..189a14a0 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -38,7 +38,7 @@ namespace StardewModdingAPI.Patches // object.getDisplayName harmony.Patch( original: AccessTools.Method(typeof(SObject), "loadDisplayName"), - prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName)) + finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Finalize_Object_loadDisplayName)) ); // IClickableMenu.drawToolTip @@ -68,42 +68,25 @@ namespace StardewModdingAPI.Patches return true; } - /// The method to call instead of . - /// The instance being patched. + /// The method to call after . /// The patched method's return value. - /// The method being wrapped. - /// Returns whether to execute the original method. - private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod) + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_Object_loadDisplayName(ref string __result, Exception __exception) { - const string key = nameof(Before_Object_loadDisplayName); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __result = (string)__originalMethod.Invoke(__instance, new object[0]); - return false; - } - catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException) + if (__exception is KeyNotFoundException) { __result = "???"; - return false; - } - catch - { - return true; - } - finally - { - PatchHelper.StopIntercept(key); + return null; } + + return __exception; } /// The method to call instead of . - /// The instance being patched. /// The item for which to draw a tooltip. /// Returns whether to execute the original method. - private static bool Before_IClickableMenu_DrawTooltip(IClickableMenu __instance, Item hoveredItem) + private static bool Before_IClickableMenu_DrawTooltip(Item hoveredItem) { // invalid edible item cause crash when drawing tooltips if (hoveredItem is SObject obj && obj.Edibility != -300 && !Game1.objectInformation.ContainsKey(obj.ParentSheetIndex)) diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index 386230a6..df6ffab3 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -1,7 +1,8 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using HarmonyLib; +using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -43,7 +44,7 @@ namespace StardewModdingAPI.Patches { harmony.Patch( original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), - prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) + finalizer: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Finalize_NPC_parseMasterSchedule)) ); } @@ -55,29 +56,17 @@ namespace StardewModdingAPI.Patches /// The raw schedule data to parse. /// The instance being patched. /// The patched method's return value. - /// The method being wrapped. - /// Returns whether to execute the original method. - private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + /// The exception thrown by the wrapped method, if any. + /// Returns the exception to throw, if any. + private static Exception Finalize_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, Exception __exception) { - const string key = nameof(Before_NPC_parseMasterSchedule); - if (!PatchHelper.StartIntercept(key)) - return true; - - try - { - __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); - return false; - } - catch (TargetInvocationException ex) + if (__exception != null) { - ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{__exception.GetLogSummary()}", LogLevel.Error); __result = new Dictionary(); - return false; - } - finally - { - PatchHelper.StopIntercept(key); } + + return null; } } } -- cgit From cfc07c1ee58b113c4e2ec8d6de28a57397589068 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 May 2020 23:31:06 -0400 Subject: fix reference to old Harmony namespace (#711) --- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 665147e4..64216138 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -50,7 +50,7 @@ namespace StardewModdingAPI.Metadata /**** ** detect code which may impact game stability ****/ - yield return new TypeFinder("Harmony.HarmonyInstance", InstructionHandleResult.DetectedGamePatch); + yield return new TypeFinder(typeof(HarmonyLib.Harmony).FullName, InstructionHandleResult.DetectedGamePatch); yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer); -- cgit From f16e477fc22bf76c33d8860acda090a199a0dcdb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 May 2020 00:02:10 -0400 Subject: add base instruction handler (#711) --- .../Framework/ModLoading/Finders/EventFinder.cs | 26 +++-------- .../Framework/ModLoading/Finders/FieldFinder.cs | 26 +++-------- .../Framework/ModLoading/Finders/MethodFinder.cs | 26 +++-------- .../Framework/ModLoading/Finders/PropertyFinder.cs | 26 +++-------- .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 25 +++-------- .../Finders/ReferenceToMissingMemberFinder.cs | 25 +++-------- .../ModLoading/Framework/BaseInstructionHandler.cs | 51 ++++++++++++++++++++++ .../ModLoading/Framework/BaseTypeFinder.cs | 43 +++++++++--------- .../Framework/BaseTypeReferenceRewriter.cs | 38 +++++++--------- .../Framework/ModLoading/IInstructionHandler.cs | 4 +- .../ModLoading/Rewriters/MethodParentRewriter.cs | 25 +++-------- 11 files changed, 124 insertions(+), 191 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index 898bafb4..1a7ae636 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,10 +1,11 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given event. - internal class EventFinder : IInstructionHandler + internal class EventFinder : BaseInstructionHandler { /********* ** Fields @@ -19,13 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly InstructionHandleResult Result; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ @@ -34,30 +28,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The event name for which to find references. /// The result to return for matching instructions. public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) + : base(nounPhrase: $"{fullTypeName}.{eventName} event") { this.FullTypeName = fullTypeName; this.EventName = eventName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{eventName} event"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs index 606ca8b7..9ae07916 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,10 +1,11 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given field. - internal class FieldFinder : IInstructionHandler + internal class FieldFinder : BaseInstructionHandler { /********* ** Fields @@ -19,13 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly InstructionHandleResult Result; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ @@ -34,30 +28,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The field name for which to find references. /// The result to return for matching instructions. public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) + : base(nounPhrase: $"{fullTypeName}.{fieldName} field") { this.FullTypeName = fullTypeName; this.FieldName = fieldName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{fieldName} field"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs index 9ca246ff..75584f1f 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,10 +1,11 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given method. - internal class MethodFinder : IInstructionHandler + internal class MethodFinder : BaseInstructionHandler { /********* ** Fields @@ -19,13 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly InstructionHandleResult Result; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ @@ -34,30 +28,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The method name for which to find references. /// The result to return for matching instructions. public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) + : base(nounPhrase: $"{fullTypeName}.{methodName} method") { this.FullTypeName = fullTypeName; this.MethodName = methodName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{methodName} method"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs index 0677aa88..811420c5 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,10 +1,11 @@ using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given property. - internal class PropertyFinder : IInstructionHandler + internal class PropertyFinder : BaseInstructionHandler { /********* ** Fields @@ -19,13 +20,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly InstructionHandleResult Result; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ @@ -34,30 +28,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The property name for which to find references. /// The result to return for matching instructions. public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) + : base(nounPhrase: $"{fullTypeName}.{propertyName} property") { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; this.Result = result; - this.NounPhrase = $"{fullTypeName}.{propertyName} property"; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; } /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs index 459e3210..1029d350 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds references to a field, property, or method which returns a different type than the code expects. /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMemberWithUnexpectedTypeFinder : IInstructionHandler + internal class ReferenceToMemberWithUnexpectedTypeFinder : BaseInstructionHandler { /********* ** Fields @@ -16,40 +17,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly HashSet ValidateReferencesToAssemblies; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; private set; } = ""; - - /********* ** Public methods *********/ /// Construct an instance. /// The assembly names to which to heuristically detect broken references. public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) + : base(nounPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs index 44b531a5..fefa88f4 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds references to a field, property, or method which no longer exists. /// This implementation is purely heuristic. It should never return a false positive, but won't detect all cases. - internal class ReferenceToMissingMemberFinder : IInstructionHandler + internal class ReferenceToMissingMemberFinder : BaseInstructionHandler { /********* ** Fields @@ -16,40 +17,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders private readonly HashSet ValidateReferencesToAssemblies; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; private set; } = ""; - - /********* ** Public methods *********/ /// Construct an instance. /// The assembly names to which to heuristically detect broken references. public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) + : base(nounPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs new file mode 100644 index 00000000..10780d07 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -0,0 +1,51 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// The base implementation for a CIL instruction handler or rewriter. + internal abstract class BaseInstructionHandler : IInstructionHandler + { + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the handler matches. + public string NounPhrase { get; protected set; } + + + /********* + ** Public methods + *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The method definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + /// Perform the predefined logic for an instruction if applicable. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The CIL instruction to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// A brief noun phrase indicating what the handler matches. + protected BaseInstructionHandler(string nounPhrase) + { + this.NounPhrase = nounPhrase; + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index b1547334..cfd87922 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -6,7 +6,7 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Framework { /// Finds incompatible CIL type reference instructions. - internal abstract class BaseTypeFinder : IInstructionHandler + internal abstract class BaseTypeFinder : BaseInstructionHandler { /********* ** Accessors @@ -18,33 +18,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework private readonly InstructionHandleResult Result; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ - /// Construct an instance. - /// Matches the type references to handle. - /// The result to return for matching instructions. - /// A brief noun phrase indicating what the instruction finder matches. - public BaseTypeFinder(Func isMatch, InstructionHandleResult result, string nounPhrase) - { - this.IsMatchImpl = isMatch; - this.Result = result; - this.NounPhrase = nounPhrase; - } - /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. - /// The method definition containing the instruction. + /// The method definition to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(method) ? this.Result @@ -54,10 +36,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { return this.IsMatch(instruction) ? this.Result @@ -127,5 +109,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework return false; } + + + /********* + ** Protected methods + *********/ + /// Construct an instance. + /// Matches the type references to handle. + /// The result to return for matching instructions. + /// A brief noun phrase indicating what the instruction finder matches. + protected BaseTypeFinder(Func isMatch, InstructionHandleResult result, string nounPhrase) + : base(nounPhrase) + { + this.IsMatchImpl = isMatch; + this.Result = result; + } } } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs index 55ce6b5a..5f38a30b 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -5,7 +5,7 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.Framework.ModLoading.Framework { /// Rewrites all references to a type. - internal abstract class BaseTypeReferenceRewriter : IInstructionHandler + internal abstract class BaseTypeReferenceRewriter : BaseInstructionHandler { /********* ** Fields @@ -14,31 +14,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework private readonly BaseTypeFinder Finder; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the handler matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ - /// Construct an instance. - /// The type finder which matches types to rewrite. - /// A brief noun phrase indicating what the instruction finder matches. - public BaseTypeReferenceRewriter(BaseTypeFinder finder, string nounPhrase) - { - this.Finder = finder; - this.NounPhrase = nounPhrase; - } - /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. - /// The method definition containing the instruction. + /// The method definition to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) { bool rewritten = false; @@ -78,10 +62,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.Finder.IsMatch(instruction)) return InstructionHandleResult.None; @@ -114,9 +98,19 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework : InstructionHandleResult.None; } + /********* - ** Private methods + ** Protected methods *********/ + /// Construct an instance. + /// The type finder which matches types to rewrite. + /// A brief noun phrase indicating what the instruction finder matches. + protected BaseTypeReferenceRewriter(BaseTypeFinder finder, string nounPhrase) + : base(nounPhrase) + { + this.Finder = finder; + } + /// Change a type reference if needed. /// The assembly module containing the instruction. /// The type to replace if it matches. diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 8830cc74..65b45b08 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -18,7 +18,7 @@ namespace StardewModdingAPI.Framework.ModLoading *********/ /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. - /// The method definition containing the instruction. + /// The method definition to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); @@ -26,7 +26,7 @@ namespace StardewModdingAPI.Framework.ModLoading /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index c4c740b3..c6388295 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -2,11 +2,12 @@ using System; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites method references from one parent type to another if the signatures match. - internal class MethodParentRewriter : IInstructionHandler + internal class MethodParentRewriter : BaseInstructionHandler { /********* ** Fields @@ -21,13 +22,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters private readonly bool OnlyIfPlatformChanged; - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - public string NounPhrase { get; } - - /********* ** Public methods *********/ @@ -37,10 +31,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). public MethodParentRewriter(string fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) + : base(nounPhrase ?? $"{fromType.Split('.').Last()} methods") { this.FromType = fromType; this.ToType = toType; - this.NounPhrase = nounPhrase ?? $"{fromType.Split('.').Last()} methods"; this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } @@ -51,23 +45,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) : this(fromType.FullName, toType, onlyIfPlatformChanged) { } - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition containing the instruction. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } /// Perform the predefined logic for an instruction if applicable. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. + /// The CIL instruction to handle. /// Metadata for mapping assemblies to the current platform. /// Whether the mod was compiled on a different platform. - public InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { if (!this.IsMatch(instruction, platformChanged)) return InstructionHandleResult.None; -- cgit From f9eebff332073177d48e62bf13f4ffd4d926b063 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 May 2020 21:43:20 -0400 Subject: update Content Patcher format in schema --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index f627ab95..726b50be 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -11,9 +11,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "const": "1.13.0", + "const": "1.14.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.13.0'." + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.14.0'." } }, "ConfigSchema": { -- cgit From c58d01d0cf45d6f5c7f75281cb6270afc68272db Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 May 2020 21:15:29 -0400 Subject: update packages --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 2 +- src/SMAPI.Tests/SMAPI.Tests.csproj | 2 +- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- .../SMAPI.Web.LegacyRedirects.csproj | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 16 ++++++++-------- 5 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index e2be66d9..5ae6574d 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -7,7 +7,7 @@ - + all diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 639c22a4..b1548e3a 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index edb1d612..4e6918ad 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj index 36831961..15ca7272 100644 --- a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj +++ b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 0a978b30..c3972758 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,23 +12,23 @@ - - + + - - - + + + - + - - + + -- cgit From a500812e88ac5a8acdd9732963e6fae95c0a73e6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 7 May 2020 22:41:37 -0400 Subject: update web project to .NET Core 3.1 --- src/SMAPI.Web/BackgroundService.cs | 5 ++- src/SMAPI.Web/Framework/Extensions.cs | 15 +++++++ src/SMAPI.Web/Program.cs | 14 +++---- src/SMAPI.Web/SMAPI.Web.csproj | 7 +--- src/SMAPI.Web/Startup.cs | 58 ++++++++++++++++---------- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 2 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 14 +++---- src/SMAPI.Web/Views/Mods/Index.cshtml | 8 ++-- 8 files changed, 75 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index ee7a60f3..275622fe 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Hangfire; @@ -36,7 +37,9 @@ namespace StardewModdingAPI.Web /// Construct an instance. /// The cache in which to store wiki metadata. /// The cache in which to store mod data. - public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache) + /// The Hangfire storage implementation. + [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service.")] + public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache, JobStorage hangfireStorage) { BackgroundService.WikiCache = wikiCache; BackgroundService.ModCache = modCache; diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index e0da1424..96463233 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -1,8 +1,12 @@ using System; using JetBrains.Annotations; +using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Routing; +using Newtonsoft.Json; namespace StardewModdingAPI.Web.Framework { @@ -34,5 +38,16 @@ namespace StardewModdingAPI.Web.Framework } return url; } + + /// Get a serialized JSON representation of the value. + /// The page to extend. + /// The value to serialize. + /// The serialized JSON. + /// This bypasses unnecessary validation (e.g. not allowing null values) in . + public static IHtmlContent ForJson(this RazorPageBase page, object value) + { + string json = JsonConvert.SerializeObject(value); + return new HtmlString(json); + } } } diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs index 70082160..1fdd3185 100644 --- a/src/SMAPI.Web/Program.cs +++ b/src/SMAPI.Web/Program.cs @@ -1,5 +1,5 @@ -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; namespace StardewModdingAPI.Web { @@ -13,13 +13,13 @@ namespace StardewModdingAPI.Web /// The command-line arguments. public static void Main(string[] args) { - // configure web server - WebHost + Host .CreateDefaultBuilder(args) - .CaptureStartupErrors(true) - .UseSetting("detailedErrors", "true") - .UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123 - .UseStartup() + .ConfigureWebHostDefaults(builder => builder + .CaptureStartupErrors(true) + .UseSetting("detailedErrors", "true") + .UseStartup() + ) .Build() .Run(); } diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index c3972758..7ed79ea3 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -3,7 +3,7 @@ SMAPI.Web StardewModdingAPI.Web - netcoreapp2.0 + netcoreapp3.1 latest @@ -20,10 +20,7 @@ - - - - + diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 56ef9a79..07869797 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Web *********/ /// Construct an instance. /// The hosting environment. - public Startup(IHostingEnvironment env) + public Startup(IWebHostEnvironment env) { this.Configuration = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) @@ -71,25 +71,16 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() - .AddMemoryCache() - .AddMvc() - .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())) - .AddJsonOptions(options => - { - foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) - options.SerializerSettings.Converters.Add(converter); - - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); + .AddMemoryCache(); MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); - // init background service - { - BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get(); - if (config.Enabled) - services.AddHostedService(); - } + // init MVC + services + .AddControllers() + .AddNewtonsoftJson(options => this.ConfigureJsonNet(options.SerializerSettings)) + .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider())); + services + .AddRazorPages(); // init MongoDB services.AddSingleton(serv => !mongoConfig.IsConfigured() @@ -121,7 +112,7 @@ namespace StardewModdingAPI.Web if (mongoConfig.IsConfigured()) { - config.UseMongoStorage(mongoConfig.ConnectionString, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions + config.UseMongoStorage(MongoClientSettings.FromConnectionString(mongoConfig.ConnectionString), $"{mongoConfig.Database}-hangfire", new MongoStorageOptions { MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), CheckConnection = false // error on startup takes down entire process @@ -131,6 +122,13 @@ namespace StardewModdingAPI.Web config.UseMemoryStorage(); }); + // init background service + { + BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get(); + if (config.Enabled) + services.AddHostedService(); + } + // init API clients { ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get(); @@ -188,8 +186,7 @@ namespace StardewModdingAPI.Web /// The method called by the runtime to configure the HTTP request pipeline. /// The application builder. - /// The hosting environment. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app) { // basic config app.UseDeveloperExceptionPage(); @@ -201,7 +198,13 @@ namespace StardewModdingAPI.Web ) .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder - .UseMvc(); + .UseRouting() + .UseAuthorization() + .UseEndpoints(p => + { + p.MapControllers(); + p.MapRazorPages(); + }); // enable Hangfire dashboard app.UseHangfireDashboard("/tasks", new DashboardOptions @@ -215,6 +218,17 @@ namespace StardewModdingAPI.Web /********* ** Private methods *********/ + /// Configure a Json.NET serializer. + /// The serializer settings to edit. + private void ConfigureJsonNet(JsonSerializerSettings settings) + { + foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) + settings.Converters.Add(converter); + + settings.Formatting = Formatting.Indented; + settings.NullValueHandling = NullValueHandling.Ignore; + } + /// Get the redirect rules to apply. private RewriteOptions GetRedirectRules() { diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 7287e00b..f255a72f 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -40,7 +40,7 @@ } diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 2183992b..9b611bcd 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,5 +1,4 @@ @using Humanizer -@using Newtonsoft.Json @using StardewModdingAPI.Toolkit.Utilities @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.LogParsing.Models @@ -12,7 +11,6 @@ .GetValues(typeof(LogLevel)) .Cast() .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace); - JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None }; string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true); } @@ -32,12 +30,12 @@ diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index b1d9ae2c..f5506287 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -1,6 +1,6 @@ @using Humanizer @using Humanizer.Localisation -@using Newtonsoft.Json +@using StardewModdingAPI.Web.Framework @model StardewModdingAPI.Web.ViewModels.ModListModel @{ ViewData["Title"] = "Mod compatibility"; @@ -15,8 +15,8 @@ @@ -86,7 +86,7 @@ else - {{link.Text}}{{i < mod.ModPages.length - 1 ? ', ' : ''}} + {{link.Text}}{{i < mod.ModPages.length - 1 ? ', ' : ''}} -- cgit From 311033964924b69961c3a9f3e21e6e3ea880910e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 00:51:56 -0400 Subject: add attribute type rewriting (#711) --- docs/release-notes.md | 2 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 46 +++++------ .../ModLoading/Framework/BaseInstructionHandler.cs | 10 +++ .../ModLoading/Framework/BaseTypeFinder.cs | 29 +++++++ .../Framework/BaseTypeReferenceRewriter.cs | 88 +++++++++++++++++++++- .../Framework/ModLoading/IInstructionHandler.cs | 7 ++ src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 67 ++++++++++++++++ 7 files changed, 224 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8037c10c..91acb14e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,8 @@ ## Upcoming released * For modders: * Added `Multiplayer.PeerConnected` event. + * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Fixed asset propagation for Gil's portraits. ## 3.5 diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b5533335..5b5a621b 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -285,31 +285,44 @@ namespace StardewModdingAPI.Framework.ModLoading // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; IInstructionHandler[] handlers = new InstructionMetadata().GetHandlers(this.ParanoidMode).ToArray(); - foreach (MethodDefinition method in this.GetMethods(module)) + foreach (TypeDefinition type in module.GetTypes()) { - // check method definition + // check type definition foreach (IInstructionHandler handler in handlers) { - InstructionHandleResult result = handler.Handle(module, method, this.AssemblyMap, platformChanged); + InstructionHandleResult result = handler.Handle(module, type, 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++) + // check methods + foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) { + // check method definition foreach (IInstructionHandler handler in handlers) { - Instruction instruction = instructions[offset]; - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); + 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++) + { + 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; + } + } } } @@ -395,18 +408,5 @@ namespace StardewModdingAPI.Framework.ModLoading AssemblyNameReference assemblyRef = this.AssemblyMap.TargetReferences[assembly]; type.Scope = assemblyRef; } - - /// Get all methods in a module. - /// The module to search. - private IEnumerable 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/Framework/BaseInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs index 10780d07..353de464 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -16,6 +16,16 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Public methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public virtual InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + return InstructionHandleResult.None; + } + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index cfd87922..48165c4c 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -50,9 +50,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method definition. public bool IsMatch(MethodDefinition method) { + // return type if (this.IsMatch(method.ReturnType)) return true; + // parameters + foreach (ParameterDefinition parameter in method.Parameters) + { + if (this.IsMatch(parameter.ParameterType)) + return true; + } + + // generic parameters + foreach (GenericParameter parameter in method.GenericParameters) + { + if (this.IsMatch(parameter)) + return true; + } + + // custom attributes + foreach (CustomAttribute attribute in method.CustomAttributes) + { + if (this.IsMatch(attribute.AttributeType)) + return true; + + foreach (var arg in attribute.ConstructorArguments) + { + if (this.IsMatch(arg.Type)) + return true; + } + } + + // local variables foreach (VariableDefinition variable in method.Body.Variables) { if (this.IsMatch(variable.VariableType)) diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs index 5f38a30b..445ac2cb 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Collections.Generic; namespace StardewModdingAPI.Framework.ModLoading.Framework { @@ -17,6 +19,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Public methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + public override InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + bool rewritten = this.RewriteCustomAttributesIfNeeded(module, type.CustomAttributes); + + return rewritten + ? InstructionHandleResult.Rewritten + : InstructionHandleResult.None; + } + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. @@ -28,9 +44,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // return type if (this.Finder.IsMatch(method.ReturnType)) - { rewritten |= this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - } // parameters foreach (ParameterDefinition parameter in method.Parameters) @@ -47,6 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework rewritten |= this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); } + // custom attributes + rewritten |= this.RewriteCustomAttributesIfNeeded(module, method.CustomAttributes); + // local variables foreach (VariableDefinition variable in method.Body.Variables) { @@ -116,5 +133,72 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The type to replace if it matches. /// Assign the new type reference. protected abstract bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set); + + /// Rewrite custom attributes if needed. + /// The assembly module containing the attributes. + /// The custom attributes to handle. + private bool RewriteCustomAttributesIfNeeded(ModuleDefinition module, Collection attributes) + { + bool rewritten = false; + + for (int attrIndex = 0; attrIndex < attributes.Count; attrIndex++) + { + CustomAttribute attribute = attributes[attrIndex]; + bool curChanged = false; + + // attribute type + TypeReference newAttrType = null; + if (this.Finder.IsMatch(attribute.AttributeType)) + { + rewritten |= this.RewriteIfNeeded(module, 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.RewriteIfNeeded(module, 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(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; + } } } diff --git a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs index 65b45b08..f9d320a6 100644 --- a/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/IInstructionHandler.cs @@ -16,6 +16,13 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Methods *********/ + /// Perform the predefined logic for a method if applicable. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// Perform the predefined logic for a method if applicable. /// The assembly module containing the instruction. /// The method definition to handle. diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs index d9a49cfa..553679f9 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs @@ -42,6 +42,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The type reference. 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 +70,39 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + 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; + } + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. /// The type ID to compare. /// The other type ID to compare. @@ -80,6 +117,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The method reference. public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) { + // + // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below + // + // same name if (definition.Name != reference.Name) return false; @@ -97,6 +138,32 @@ namespace StardewModdingAPI.Framework.ModLoading return true; } + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodInfo, 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; + } + /// Get whether a type has a method whose signature matches the one expected by a method reference. /// The type to check. /// The method reference. -- cgit From 19397a89ff6f0d216637e1272b78c7e15c85bf76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 09:23:27 -0400 Subject: log detailed error for rewritten patch failures (#711) --- .../RewriteFacades/HarmonyInstanceMethods.cs | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs index 0f906f51..bca76981 100644 --- a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -24,10 +26,30 @@ namespace StardewModdingAPI.Framework.RewriteFacades return new Harmony(id); } + /// Apply one or more patches to a method. + /// The original method. + /// The prefix to apply. + /// The postfix to apply. + /// The transpiler to apply. public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) { - MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); - return new DynamicMethod(method.Name, method.Attributes, method.CallingConvention, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray(), method.Module, true); + try + { + MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); + return new DynamicMethod(method.Name, method.Attributes, method.CallingConvention, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray(), method.Module, true); + } + catch (Exception ex) + { + var patchTypes = new List(); + if (prefix != null) + patchTypes.Add("prefix"); + if (postfix != null) + patchTypes.Add("postfix"); + if (transpiler != null) + patchTypes.Add("transpiler"); + + throw new Exception($"Failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}", ex); + } } } } -- cgit From d8d8cac2d89c27c6d28be444cebaabf0e2077a53 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 10:43:05 -0400 Subject: simplify logged paranoid warnings --- docs/release-notes.md | 1 + src/SMAPI/Framework/IModMetadata.cs | 6 +- src/SMAPI/Framework/ModLoading/ModMetadata.cs | 11 ++- src/SMAPI/Framework/SCore.cs | 133 ++++++++++++++++++-------- 4 files changed, 102 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8037c10c..ee8bd468 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming released * For modders: * Added `Multiplayer.PeerConnected` event. + * Simplified paranoid warnings in the log and reduced their log level. * Fixed asset propagation for Gil's portraits. ## 3.5 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 /// Whether the mod has at least one valid update key set. bool HasValidUpdateKeys(); - /// Get whether the mod has a given warning and it hasn't been suppressed in the . - /// The warning to check. - bool HasUnsuppressWarning(ModWarning warning); + /// Get whether the mod has any of the given warnings which haven't been suppressed in the . + /// The warnings to check. + bool HasUnsuppressedWarnings(params ModWarning[] warnings); /// Get a relative path which includes the root folder name. string GetRelativePathWithRoot(); 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(); } - /// Get whether the mod has a given warning and it hasn't been suppressed in the . - /// The warning to check. - public bool HasUnsuppressWarning(ModWarning warning) + /// Get whether the mod has any of the given warnings which haven't been suppressed in the . + /// The warnings to check. + 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)) + ); } /// Get a relative path which includes the root folder name. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..8c9424c1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1129,67 +1129,118 @@ namespace StardewModdingAPI.Framework // log warnings if (modsWithWarnings.Any()) { - // issue block format logic - void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) - { - IModMetadata[] matches = modsWithWarnings - .Where(mod => mod.HasUnsuppressWarning(warning)) - .ToArray(); - if (!matches.Any()) - return; - - this.Monitor.Log(" " + heading, logLevel); - this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel); - foreach (string line in blurb) - this.Monitor.Log(" " + line, logLevel); - this.Monitor.Newline(); - foreach (IModMetadata match in matches) - this.Monitor.Log($" - {match.DisplayName}", logLevel); - this.Monitor.Newline(); - } - - // supported issues - LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", + // broken code + this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods", "These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,", "errors, or crashes in-game." ); - LogWarningGroup(ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", + + // changes serializer + this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer", "These mods change the save serializer. They may corrupt your save files, or make them unusable if", "you uninstall these mods." ); - if (this.Settings.ParanoidWarnings) - { - LogWarningGroup(ModWarning.AccessesConsole, LogLevel.Warn, "Accesses the console directly", - "These mods directly access the SMAPI console, and you enabled paranoid warnings. (Note that this may be", - "legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly", - "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be", - "legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly", - "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that", - "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)" - ); - } - LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code", + + // patched game code + this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code", "These mods directly change the game code. They're more likely to cause errors or bugs in-game; if", "your game has issues, try removing these first. Otherwise you can ignore this warning." ); - LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", + + // unvalidated update tick + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks", "These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save", "corruption. If your game has issues, try removing these first." ); - LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", + + // paranoid warnings + if (this.Settings.ParanoidWarnings) + { + this.LogModWarningGroup( + modsWithWarnings, + match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell), + level: LogLevel.Debug, + heading: "Direct system access", + blurb: new[] + { + "You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or", + "SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for", + "further investigation.)" + }, + modLabel: mod => + { + List labels = new List(); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole)) + labels.Add("console"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem)) + labels.Add("files"); + if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell)) + labels.Add("shells/processes"); + + return $"{mod.DisplayName} ({string.Join(", ", labels)})"; + } + ); + } + + // no update keys + this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys", "These mods have no update keys in their manifest. SMAPI may not notify you about updates for these", "mods. Consider notifying the mod authors about this problem." ); - LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", + + // not crossplatform + this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform", "These mods use the 'dynamic' keyword, and won't work on Linux/Mac." ); } } + /// Write a mod warning group to the console and log. + /// The mods to search. + /// Matches mods to include in the warning group. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + /// Formats the mod label, or null to use the . + private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) + { + // get matching mods + IModMetadata[] matches = mods + .Where(match) + .ToArray(); + if (!matches.Any()) + return; + + // log header/blurb + this.Monitor.Log(" " + heading, level); + this.Monitor.Log(" " + "".PadRight(50, '-'), level); + foreach (string line in blurb) + this.Monitor.Log(" " + line, level); + this.Monitor.Newline(); + + // log mod list + foreach (IModMetadata modMatch in matches) + { + string label = modLabel != null + ? modLabel(modMatch) + : modMatch.DisplayName; + this.Monitor.Log($" - {label}", level); + } + + this.Monitor.Newline(); + } + + /// Write a mod warning group to the console and log. + /// The mods to search. + /// The mod warning to match. + /// The log level for the logged messages. + /// A brief heading label for the group. + /// A detailed explanation of the warning, split into lines. + void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb) + { + this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb); + } + /// Load a mod's entry class. /// The mod assembly. /// The loaded instance. -- cgit From 719831c15a74a4987496dc77a3caa6999940bf90 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 10:58:10 -0400 Subject: sort mod warning lists --- docs/release-notes.md | 3 +++ src/SMAPI/Framework/SCore.cs | 13 +++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ee8bd468..f12455bd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming released +* For players: + * Mod warnings are now listed alphabetically. + * For modders: * Added `Multiplayer.PeerConnected` event. * Simplified paranoid warnings in the log and reduced their log level. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 8c9424c1..cd292bfc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1205,10 +1205,12 @@ namespace StardewModdingAPI.Framework private void LogModWarningGroup(IModMetadata[] mods, Func match, LogLevel level, string heading, string[] blurb, Func modLabel = null) { // get matching mods - IModMetadata[] matches = mods + string[] modLabels = mods .Where(match) + .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName) + .OrderBy(p => p) .ToArray(); - if (!matches.Any()) + if (!modLabels.Any()) return; // log header/blurb @@ -1219,13 +1221,8 @@ namespace StardewModdingAPI.Framework this.Monitor.Newline(); // log mod list - foreach (IModMetadata modMatch in matches) - { - string label = modLabel != null - ? modLabel(modMatch) - : modMatch.DisplayName; + foreach (string label in modLabels) this.Monitor.Log($" - {label}", level); - } this.Monitor.Newline(); } -- cgit From f82a8e3c2dbc52e9db293fb71be693aed48825c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 9 May 2020 13:06:21 -0400 Subject: update web scripts --- src/SMAPI.Web/Views/Index/Index.cshtml | 2 +- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 2 +- src/SMAPI.Web/Views/LogParser/Index.cshtml | 4 ++-- src/SMAPI.Web/Views/Mods/Index.cshtml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index eded9df3..d78a155e 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -9,7 +9,7 @@ } @section Head { - + } diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index f255a72f..f23bd150 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -32,7 +32,7 @@ - + diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index 9b611bcd..71e12d47 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -23,8 +23,8 @@ - - + + - - + + + @@ -40,9 +43,9 @@ else

The list is updated every few days (you can help update it!). It doesn't include XNB mods (see using XNB mods on the wiki instead) or compatible content packs.

- @if (Model.BetaVersion != null) + @if (hasBeta) { -

Note: "SDV @Model.BetaVersion only" lines are for an unreleased version of the game, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of the game.

+

Note: "@betaLabel" lines are for an unreleased version of SMAPI, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of SMAPI.

} @@ -97,7 +100,7 @@ else
- SDV @Model.BetaVersion only: + @betaLabel:
âš  {{warning}}
-- cgit From 10531e537fda7c4901304b295f4ef60ac1f83eea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 10 May 2020 11:50:35 -0400 Subject: rewrite AccessTools methods which changed in Harmony 2.0 (#711) --- .../ModLoading/Rewriters/MethodParentRewriter.cs | 5 ++-- .../Framework/RewriteFacades/AccessToolsMethods.cs | 32 ++++++++++++++++++++++ .../RewriteFacades/HarmonyInstanceMethods.cs | 11 ++------ src/SMAPI/Metadata/InstructionMetadata.cs | 3 +- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs index c6388295..d0fe8b13 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -42,8 +42,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type whose methods to remap. /// The type with methods to map to. /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false) - : this(fromType.FullName, toType, onlyIfPlatformChanged) { } + /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). + public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) + : this(fromType.FullName, toType, onlyIfPlatformChanged, nounPhrase) { } /// Perform the predefined logic for an instruction if applicable. diff --git a/src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs b/src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs new file mode 100644 index 00000000..cb40bbcc --- /dev/null +++ b/src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.RewriteFacades +{ + /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class AccessToolsMethods + { + /********* + ** Public methods + *********/ + public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) + { + return AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true); + } + + public static ConstructorInfo Constructor(Type type, Type[] parameters = null) + { + return AccessTools.Constructor(type, parameters, searchForStatic: true); + } + + public static List GetDeclaredConstructors(Type type) + { + return AccessTools.GetDeclaredConstructors(type, searchForStatic: true); + } + } +} diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs index bca76981..aad62c9b 100644 --- a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; @@ -7,8 +8,9 @@ using HarmonyLib; namespace StardewModdingAPI.Framework.RewriteFacades { - /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] public class HarmonyInstanceMethods : Harmony { /********* @@ -19,18 +21,11 @@ namespace StardewModdingAPI.Framework.RewriteFacades public HarmonyInstanceMethods(string id) : base(id) { } - /// Creates a new Harmony instance. - /// A unique identifier for the instance. public static Harmony Create(string id) { return new Harmony(id); } - /// Apply one or more patches to a method. - /// The original method. - /// The prefix to apply. - /// The postfix to apply. - /// The transpiler to apply. public DynamicMethod Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) { try diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 64216138..40a7588e 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,7 +38,8 @@ namespace StardewModdingAPI.Metadata // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); - yield return new MethodParentRewriter("HarmonyLib.Harmony", typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); + yield return new MethodParentRewriter(typeof(HarmonyLib.Harmony), typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); + yield return new MethodParentRewriter(typeof(HarmonyLib.AccessTools), typeof(AccessToolsMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); /**** ** detect mod issues -- cgit From 2b9703f98fedcf97fd5e511f1e30bcc8fd94b5cc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 11 May 2020 01:40:46 -0400 Subject: fix Harmony issue when assembly is loaded from memory (#711) --- src/SMAPI.Installer/InteractiveInstaller.cs | 40 ++---------------------- src/SMAPI/Constants.cs | 3 ++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 27 ++++++++++++++-- src/SMAPI/Framework/SCore.cs | 16 +++++++++- 4 files changed, 44 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 5b0c6e1f..1457848b 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using Microsoft.Win32; using StardewModdingApi.Installer.Enums; using StardewModdingAPI.Installer.Framework; @@ -624,7 +623,7 @@ namespace StardewModdingApi.Installer { try { - this.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); + FileUtilities.ForceDelete(Directory.Exists(path) ? new DirectoryInfo(path) : (FileSystemInfo)new FileInfo(path)); break; } catch (Exception ex) @@ -665,41 +664,6 @@ namespace StardewModdingApi.Installer } } - /// Delete a file or folder regardless of file permissions, and block until deletion completes. - /// The file or folder to reset. - /// This method is mirrored from FileUtilities.ForceDelete in the toolkit. - private void ForceDelete(FileSystemInfo entry) - { - // ignore if already deleted - entry.Refresh(); - if (!entry.Exists) - return; - - // delete children - if (entry is DirectoryInfo folder) - { - foreach (FileSystemInfo child in folder.GetFileSystemInfos()) - this.ForceDelete(child); - } - - // reset permissions & delete - entry.Attributes = FileAttributes.Normal; - entry.Delete(); - - // wait for deletion to finish - for (int i = 0; i < 10; i++) - { - entry.Refresh(); - if (entry.Exists) - Thread.Sleep(500); - } - - // throw exception if deletion didn't happen before timeout - entry.Refresh(); - if (entry.Exists) - throw new IOException($"Timed out trying to delete {entry.FullName}"); - } - /// Interactively ask the user to choose a value. /// A callback which prints a message to the console. /// The message to print. @@ -707,7 +671,7 @@ namespace StardewModdingApi.Installer /// The indentation to prefix to output. private string InteractivelyChoose(string message, string[] options, string indent = "", Action print = null) { - print = print ?? this.PrintInfo; + print ??= this.PrintInfo; while (true) { diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..907a93b2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; + /// The folder containing temporary files that are only valid for the current session. + internal static string InternalTempFilesPath => Path.Combine(Program.DllSearchPath, ".temp"); + /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 570686fe..78e717e9 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -36,6 +36,9 @@ namespace StardewModdingAPI.Framework.ModLoading /// The objects to dispose as part of this instance. private readonly HashSet Disposables = new HashSet(); + /// The full path to the folder in which to save rewritten assemblies. + private readonly string TempFolderPath; + /********* ** Public methods @@ -44,11 +47,15 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current game platform. /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) + /// The full path to the folder in which to save rewritten assemblies. + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, string tempFolderPath) { this.Monitor = monitor; this.ParanoidMode = paranoidMode; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); + this.TempFolderPath = tempFolderPath; + + // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.ExecutionPath); this.AssemblyDefinitionResolver.AddSearchDirectory(Constants.InternalFilesPath); @@ -124,9 +131,23 @@ 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); + + if (assembly.Definition.MainModule.AssemblyReferences.Any(p => p.Name == "0Harmony")) + { + // Note: the assembly must be loaded from disk for Harmony compatibility. + // Loading it from memory sets the assembly module's FullyQualifiedName to + // "", so Harmony incorrectly identifies the module in its + // Patch.PatchMethod when handling multiple patches for the same method, + // leading to "Token 0x... is not valid in the scope of module HarmonySharedState" + // errors (e.g. https://smapi.io/log/A0gAsc3M). + string tempPath = Path.Combine(this.TempFolderPath, $"{Path.GetFileNameWithoutExtension(assemblyPath)}.{Guid.NewGuid()}.dll"); + assembly.Definition.Write(tempPath); + lastAssembly = Assembly.LoadFile(tempPath); + } + else { + using MemoryStream outStream = new MemoryStream(); assembly.Definition.Write(outStream); byte[] bytes = outStream.ToArray(); lastAssembly = Assembly.Load(bytes); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..12dc9c3d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -213,6 +213,20 @@ namespace StardewModdingAPI.Framework return; } #endif + + // reset temp folder + if (Directory.Exists(Constants.InternalTempFilesPath)) + { + try + { + FileUtilities.ForceDelete(new DirectoryInfo(Constants.InternalTempFilesPath)); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't delete temporary files at {Constants.InternalTempFilesPath}: {ex}", LogLevel.Trace); + } + } + Directory.CreateDirectory(Constants.InternalTempFilesPath); } /// Launch SMAPI. @@ -748,7 +762,7 @@ namespace StardewModdingAPI.Framework // load mods IDictionary> skippedMods = new Dictionary>(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, Constants.InternalTempFilesPath)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); -- cgit From e626e5817e66f1133d125ab49866b8a0475dde07 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 13 May 2020 20:52:09 -0400 Subject: tweak Harmony patch error (#711) --- src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs index aad62c9b..995a40d6 100644 --- a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs @@ -43,7 +43,7 @@ namespace StardewModdingAPI.Framework.RewriteFacades if (transpiler != null) patchTypes.Add("transpiler"); - throw new Exception($"Failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}", ex); + throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}.", ex); } } } -- cgit From 896f531f4f6fc7f0e8dfcfc0d6433850233ee749 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 14 May 2020 19:25:51 -0400 Subject: fix broken action links after update to .NET Core 3.0 --- src/SMAPI.Web/Framework/Extensions.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index 96463233..ad7e645a 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -22,6 +22,7 @@ namespace StardewModdingAPI.Web.Framework /// The generated URL. public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false) { + // get route values RouteValueDictionary valuesDict = new RouteValueDictionary(values); foreach (var value in helper.ActionContext.RouteData.Values) { @@ -29,13 +30,19 @@ namespace StardewModdingAPI.Web.Framework valuesDict[value.Key] = null; // explicitly remove it from the URL } + // get relative URL string url = helper.Action(action, controller, valuesDict); + if (url == null && action.EndsWith("Async")) + url = helper.Action(action[..^"Async".Length], controller, valuesDict); + + // get absolute URL if (absoluteUrl) { HttpRequest request = helper.ActionContext.HttpContext.Request; Uri baseUri = new Uri($"{request.Scheme}://{request.Host}"); url = new Uri(baseUri, url).ToString(); } + return url; } -- cgit From a090b6c21c877e8835f25e1d70d667abf07d1d3c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 11:29:40 -0400 Subject: use newer C# features --- src/SMAPI.Installer/InteractiveInstaller.cs | 2 +- src/SMAPI.ModBuildConfig/DeployModTask.cs | 27 +++++++++--------- .../Framework/Clients/WebApi/WebApiClient.cs | 17 ++++++------ .../Framework/GameScanning/GameScanner.cs | 4 +-- src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs | 5 +--- .../Controllers/JsonValidatorController.cs | 32 ++++++++++------------ src/SMAPI.Web/Controllers/ModsApiController.cs | 4 +-- .../Clients/CurseForge/CurseForgeClient.cs | 3 +- src/SMAPI.Web/Framework/Compression/GzipHelper.cs | 2 +- src/SMAPI.Web/Startup.cs | 8 +++--- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 17 +++++------- src/SMAPI/Framework/Input/MouseStateBuilder.cs | 23 +++++++--------- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 +++---- src/SMAPI/Framework/Networking/SGalaxyNetServer.cs | 29 ++++++++++---------- src/SMAPI/Framework/Networking/SLidgrenServer.cs | 31 ++++++++++----------- .../PerformanceMonitoring/AlertContext.cs | 2 +- .../Framework/PerformanceMonitoring/AlertEntry.cs | 2 +- .../Framework/PerformanceMonitoring/PeakEntry.cs | 2 +- .../PerformanceCounterEntry.cs | 2 +- .../Framework/Serialization/ColorConverter.cs | 2 +- .../Framework/Serialization/PointConverter.cs | 2 +- .../Framework/Serialization/RectangleConverter.cs | 2 +- .../Framework/Serialization/Vector2Converter.cs | 2 +- 23 files changed, 105 insertions(+), 125 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 5b0c6e1f..f8371d57 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -707,7 +707,7 @@ namespace StardewModdingApi.Installer /// The indentation to prefix to output. private string InteractivelyChoose(string message, string[] options, string indent = "", Action print = null) { - print = print ?? this.PrintInfo; + print ??= this.PrintInfo; while (true) { diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs index 96d95e06..ced05a28 100644 --- a/src/SMAPI.ModBuildConfig/DeployModTask.cs +++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs @@ -153,23 +153,22 @@ namespace StardewModdingAPI.ModBuildConfig // create zip file Directory.CreateDirectory(outputFolderPath); - using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) - using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create)) + using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write); + using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create); + + foreach (var fileEntry in files) { - foreach (var fileEntry in files) - { - string relativePath = fileEntry.Key; - FileInfo file = fileEntry.Value; + string relativePath = fileEntry.Key; + FileInfo file = fileEntry.Value; - // get file info - string filePath = file.FullName; - string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); + // get file info + string filePath = file.FullName; + string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/'); - // add to zip - using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read)) - using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open()) - fileStream.CopyTo(fileStreamInZip); - } + // add to zip + using Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using Stream fileStreamInZip = archive.CreateEntry(entryName).Open(); + fileStream.CopyTo(fileStreamInZip); } } diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index f0a7c82a..2fb6ed20 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -62,16 +62,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi private TResult Post(string url, TBody content) { // note: avoid HttpClient for Mac compatibility - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(this.BaseUrl, url); - string data = JsonConvert.SerializeObject(content); + using WebClient client = new WebClient(); - client.Headers["Content-Type"] = "application/json"; - client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; - string response = client.UploadString(fullUrl, data); - return JsonConvert.DeserializeObject(response, this.JsonSettings); - } + Uri fullUrl = new Uri(this.BaseUrl, url); + string data = JsonConvert.SerializeObject(content); + + client.Headers["Content-Type"] = "application/json"; + client.Headers["User-Agent"] = $"SMAPI/{this.Version}"; + string response = client.UploadString(fullUrl, data); + return JsonConvert.DeserializeObject(response, this.JsonSettings); } } } diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index 212c70ef..4eec3424 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -124,8 +124,8 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning XElement root; try { - using (FileStream stream = file.OpenRead()) - root = XElement.Load(stream); + using FileStream stream = file.OpenRead(); + root = XElement.Load(stream); } catch { diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs index c45448f3..1e490448 100644 --- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs @@ -30,10 +30,7 @@ namespace StardewModdingAPI.Toolkit.Utilities /// Detect the current OS. public static Platform DetectPlatform() { - if (EnvironmentUtility.CachedPlatform == null) - EnvironmentUtility.CachedPlatform = EnvironmentUtility.DetectPlatformImpl(); - - return EnvironmentUtility.CachedPlatform.Value; + return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl(); } diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 2ade3e3d..c43fb929 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -275,21 +275,20 @@ namespace StardewModdingAPI.Web.Controllers errors = new Dictionary(errors, StringComparer.InvariantCultureIgnoreCase); // match error by type and message - foreach (var pair in errors) + foreach ((string target, string errorMessage) in errors) { - if (!pair.Key.Contains(":")) + if (!target.Contains(":")) continue; - string[] parts = pair.Key.Split(':', 2); + string[] parts = target.Split(':', 2); if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1])) - return pair.Value?.Trim(); + return errorMessage?.Trim(); } // match by type - if (errors.TryGetValue(error.ErrorType.ToString(), out string message)) - return message?.Trim(); - - return null; + return errors.TryGetValue(error.ErrorType.ToString(), out string message) + ? message?.Trim() + : null; } return GetRawOverrideError() @@ -304,10 +303,10 @@ namespace StardewModdingAPI.Web.Controllers { if (schema.ExtensionData != null) { - foreach (var pair in schema.ExtensionData) + foreach ((string curKey, JToken value) in schema.ExtensionData) { - if (pair.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)) - return pair.Value.ToObject(); + if (curKey.Equals(key, StringComparison.InvariantCultureIgnoreCase)) + return value.ToObject(); } } @@ -318,14 +317,11 @@ namespace StardewModdingAPI.Web.Controllers /// The value to format. private string FormatValue(object value) { - switch (value) + return value switch { - case List list: - return string.Join(", ", list); - - default: - return value?.ToString() ?? "null"; - } + List list => string.Join(", ", list), + _ => value?.ToString() ?? "null" + }; } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 06768f03..e841a075 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -388,9 +388,9 @@ namespace StardewModdingAPI.Web.Controllers if (map.ContainsKey(parsed.ToString())) return map[parsed.ToString()]; - foreach (var pair in map) + foreach ((string fromRaw, string toRaw) in map) { - if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion)) + if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion)) return newVersion.ToString(); } } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index 140b854e..a6fd21fd 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -57,8 +57,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge if (!SemanticVersion.TryParse(raw, out version)) { - if (invalidVersion == null) - invalidVersion = raw; + invalidVersion ??= raw; continue; } } diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs index cc8f4737..676d660d 100644 --- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs +++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs @@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression return rawText; // decompress - using (MemoryStream memoryStream = new MemoryStream()) + using MemoryStream memoryStream = new MemoryStream(); { // read length prefix int dataLength = BitConverter.ToInt32(zipBuffer, 0); diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 07869797..35d22459 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -83,7 +83,7 @@ namespace StardewModdingAPI.Web .AddRazorPages(); // init MongoDB - services.AddSingleton(serv => !mongoConfig.IsConfigured() + services.AddSingleton(_ => !mongoConfig.IsConfigured() ? MongoDbRunner.Start() : throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.") ); @@ -265,10 +265,10 @@ namespace StardewModdingAPI.Web ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" }, ["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" } }; - foreach (KeyValuePair pair in wikiRedirects) + foreach ((string page, string[] patterns) in wikiRedirects) { - foreach (string pattern in pair.Value) - redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key)); + foreach (string pattern in patterns) + redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + page)); } return redirects; 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 /// Get the equivalent state. 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 /// Get the equivalent state. 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/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b5533335..8df492eb 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -125,12 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading { if (!oneAssembly) this.Monitor.Log($" Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); - using (MemoryStream outStream = new MemoryStream()) - { - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } + using MemoryStream outStream = new MemoryStream(); + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); } else { 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()); + } + }); } /// Send a message to a remote peer. 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/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 { /// The context for an alert. - 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 { /// A single alert entry. - 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 { /// A peak invocation time. - internal struct PeakEntry + internal readonly struct PeakEntry { /********* ** Accessors 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 { /// A single performance counter entry. - internal struct PerformanceCounterEntry + internal readonly struct PerformanceCounterEntry { /********* ** Accessors 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:(?\d+) Y:(?\d+) Width:(?\d+) 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]); -- cgit From c776f6053bd0b9db909ebda2853a86c1cd21c2cf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 11:33:17 -0400 Subject: update deprecated code --- src/SMAPI.Web/Controllers/ModsApiController.cs | 2 +- src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index e841a075..6032186f 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -61,7 +61,7 @@ namespace StardewModdingAPI.Web.Controllers /// The GitHub API client. /// The ModDrop API client. /// The Nexus API client. - public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs index 2e7804a7..d6906866 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs @@ -88,7 +88,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods this.Mods.ReplaceOne( entry => entry.ID == id && entry.Site == mod.Site, mod, - new UpdateOptions { IsUpsert = true } + new ReplaceOptions { IsUpsert = true } ); return mod; -- cgit From a2cfb71d898aca98d621f1b86dd5611337eea034 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 11:34:00 -0400 Subject: minor cleanup --- src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 6 +++--- src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 2 +- src/SMAPI.Web/wwwroot/Content/js/mods.js | 2 -- src/SMAPI/Framework/Rendering/SDisplayDevice.cs | 13 +------------ src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs | 2 +- src/SMAPI/Framework/SGame.cs | 2 +- 8 files changed, 9 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs index d6906866..6ba1d73d 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods public void RemoveStaleMods(TimeSpan age) { DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); - var result = this.Mods.DeleteMany(p => p.LastRequested < minDate); + this.Mods.DeleteMany(p => p.LastRequested < minDate); } diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index cce80816..227dcd89 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching SMAPI's update line. - private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /********* @@ -181,9 +181,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing message.Section = LogSection.ModUpdateList; } - else if (message.Level == LogLevel.Alert && this.SMAPIUpdatePattern.IsMatch(message.Text)) + else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text)) { - Match match = this.SMAPIUpdatePattern.Match(message.Text); + Match match = this.SmapiUpdatePattern.Match(message.Text); string version = match.Groups["version"].Value; string link = match.Groups["link"].Value; smapiMod.UpdateVersion = version; diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index fa1375ec..c62e1466 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -83,7 +83,7 @@ else - + {{mod.Name}} (aka {{mod.AlternateNames}}) diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 2d06ceb1..67dcd3b3 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -29,7 +29,7 @@
- @if (ViewData["ViewTitle"] != string.Empty) + @if (ViewData["ViewTitle"] as string != string.Empty) {

@(ViewData["ViewTitle"] ?? ViewData["Title"])

} diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js index 35098b60..ac2754a4 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/mods.js +++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js @@ -1,5 +1,3 @@ -/* globals $ */ - var smapi = smapi || {}; var app; smapi.modList = function (mods, enableBeta) { 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; @@ -13,13 +12,6 @@ namespace StardewModdingAPI.Framework.Rendering /// A map display device which overrides the draw logic to support tile rotation. internal class SDisplayDevice : SXnaDisplayDevice { - /********* - ** Fields - *********/ - /// The origin to use when rotating tiles. - private readonly Vector2 RotationOrigin; - - /********* ** Public methods *********/ @@ -27,10 +19,7 @@ namespace StardewModdingAPI.Framework.Rendering /// The content manager through which to load tiles. /// The graphics device with which to render tiles. public SDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice) - : base(contentManager, graphicsDevice) - { - this.RotationOrigin = new Vector2((Game1.tileSize * Game1.pixelZoom) / 2f); - } + : base(contentManager, graphicsDevice) { } /// Draw a tile to the screen. /// The tile to draw. 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 { /// A map display device which reimplements the default logic. /// This is an exact copy of , except that private fields are protected and all methods are virtual. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 2a30b595..82db5857 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) -- cgit From 5e6f1640dcb8e30a44f8ff07572874850b12cc2e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 14:30:07 -0400 Subject: simplify single-instance deployment and make MongoDB server optional --- docs/release-notes.md | 4 + docs/technical/web.md | 44 +++++++-- .../Framework/Caching/Mods/IModCacheRepository.cs | 2 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 89 +++++++++++++++++ .../Caching/Mods/ModCacheMongoRepository.cs | 105 +++++++++++++++++++++ .../Framework/Caching/Mods/ModCacheRepository.cs | 104 -------------------- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 2 +- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 54 +++++++++++ .../Caching/Wiki/WikiCacheMongoRepository.cs | 73 ++++++++++++++ .../Framework/Caching/Wiki/WikiCacheRepository.cs | 73 -------------- .../Framework/ConfigModels/MongoDbConfig.cs | 25 ----- .../Framework/ConfigModels/StorageConfig.cs | 18 ++++ .../Framework/ConfigModels/StorageMode.cs | 15 +++ src/SMAPI.Web/Startup.cs | 94 ++++++++++++------ src/SMAPI.Web/appsettings.Development.json | 3 +- src/SMAPI.Web/appsettings.json | 3 +- 16 files changed, 464 insertions(+), 244 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs create mode 100644 src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index a660888c..abe07dd9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,12 +8,16 @@ * For the web UI: * Updated web framework to improve site performance and reliability. * Added GitHub licenses to mod compatibility list. + * Internal changes to improve performance and reliability. * For modders: * Added `Multiplayer.PeerConnected` event. * Simplified paranoid warnings in the log and reduced their log level. * Fixed asset propagation for Gil's portraits. +* For SMAPI developers: + * When deploying web services to a single-instance app, the MongoDB server can now be replaced with in-memory storage. + ## 3.5 Released 27 April 2020 for Stardew Valley 1.4.1 or later. diff --git a/docs/technical/web.md b/docs/technical/web.md index 67e86c8b..ef591aee 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -340,9 +340,20 @@ short url | → | target page A local environment lets you run a complete copy of the web project (including cache database) on your machine, with no external dependencies aside from the actual mod sites. -1. Enter the Nexus credentials in `appsettings.Development.json` . You can leave the other - credentials empty to default to fetching data anonymously, and storing data in-memory and - on disk. +1. Edit `appsettings.Development.json` and set these options: + + property name | description + ------------- | ----------- + `NexusApiKey` | [Your Nexus API key](https://www.nexusmods.com/users/myaccount?tab=api#personal_key). + + Optional settings: + + property name | description + --------------------------- | ----------- + `AzureBlobConnectionString` | The connection string for the Azure Blob storage account. Defaults to using the system's temporary file folder if not specified. + `GitHubUsername`
`GitHubPassword` | The GitHub credentials with which to query GitHub release info. Defaults to anonymous requests if not specified. + `Storage` | How to storage cached wiki/mod data. `InMemory` is recommended in most cases, or `MongoInMemory` to test the MongoDB storage code. See [production environment](#production-environment) for more info on `Mongo`. + 2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site. ### Production environment @@ -355,19 +366,15 @@ accordingly. Initial setup: -1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)) - for mod data. -2. Create an Azure Blob storage account for uploaded files. -3. Create an Azure App Services environment running the latest .NET Core on Linux or Windows. -4. Add these application settings in the new App Services environment: +1. Create an Azure Blob storage account for uploaded files. +2. Create an Azure App Services environment running the latest .NET Core on Linux or Windows. +3. Add these application settings in the new App Services environment: property name | description ------------------------------- | ----------------- `ApiClients.AzureBlobConnectionString` | The connection string for the Azure Blob storage account created in step 2. `ApiClients.GitHubUsername`
`ApiClients.GitHubPassword` | The login credentials for the GitHub account with which to fetch release info. If these are omitted, GitHub will impose much stricter rate limits. `ApiClients:NexusApiKey` | The [Nexus API authentication key](https://github.com/Pathoschild/FluentNexus#init-a-client). - `MongoDB:ConnectionString` | The connection string for the MongoDB instance. - `MongoDB:Database` | The MongoDB database name (e.g. `smapi` in production or `smapi-edge` in testing environments). Optional settings: @@ -378,6 +385,23 @@ Initial setup: `Site:BetaBlurb` | If `Site:BetaEnabled` is true and there's a beta version of SMAPI in its GitHub releases, this is shown on the beta download button as explanatory subtext. `Site:SupporterList` | A list of Patreon supports to credit on the download page. +To enable distributed servers: + +1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)) + for mod data. +2. Add these application settings in the App Services environment: + + property name | description + ------------------------------- | ----------------- + `Storage:Mode` | Set to `Mongo`. + `Storage:ConnectionString` | Set to the connection string for the MongoDB instance. + + Optional settings: + + property name | description + ------------------------------- | ----------------- + `Storage:Database` | Set to the MongoDB database name (defaults to `smapi`). + To deploy updates: 1. [Deploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure). 2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.) diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index bcec8b36..08749f3b 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -4,7 +4,7 @@ using StardewModdingAPI.Web.Framework.ModRepositories; namespace StardewModdingAPI.Web.Framework.Caching.Mods { - /// Encapsulates logic for accessing the mod data cache. + /// Manages cached mod data. internal interface IModCacheRepository : ICacheRepository { /********* diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs new file mode 100644 index 00000000..9c5a217e --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Framework.Caching.Mods +{ + /// Manages cached mod data in-memory. + internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheRepository + { + /********* + ** Fields + *********/ + /// The cached mod data indexed by {site key}:{ID}. + private readonly IDictionary Mods = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Get the cached mod data. + /// The mod site to search. + /// The mod's unique ID within the . + /// The fetched mod. + /// Whether to update the mod's 'last requested' date. + public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) + { + // get mod + if (!this.Mods.TryGetValue(this.GetKey(site, id), out mod)) + return false; + + // bump 'last requested' + if (markRequested) + { + mod.LastRequested = DateTimeOffset.UtcNow; + mod = this.SaveMod(mod); + } + + return true; + } + + /// Save data fetched for a mod. + /// The mod site on which the mod is found. + /// The mod's unique ID within the . + /// The mod data. + /// The stored mod record. + public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) + { + string key = this.GetKey(site, id); + cachedMod = this.SaveMod(new CachedMod(site, id, mod)); + } + + /// Delete data for mods which haven't been requested within a given time limit. + /// The minimum age for which to remove mods. + public void RemoveStaleMods(TimeSpan age) + { + DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); + + string[] staleKeys = this.Mods + .Where(p => p.Value.LastRequested < minDate) + .Select(p => p.Key) + .ToArray(); + + foreach (string key in staleKeys) + this.Mods.Remove(key); + } + + /// Save data fetched for a mod. + /// The mod data. + public CachedMod SaveMod(CachedMod mod) + { + string key = this.GetKey(mod.Site, mod.ID); + return this.Mods[key] = mod; + } + + + /********* + ** Private methods + *********/ + /// Get a cache key. + /// The mod site. + /// The mod ID. + public string GetKey(ModRepositoryKey site, string id) + { + return $"{site}:{id.Trim()}".ToLower(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs new file mode 100644 index 00000000..f105baab --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs @@ -0,0 +1,105 @@ +using System; +using MongoDB.Driver; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.ModRepositories; + +namespace StardewModdingAPI.Web.Framework.Caching.Mods +{ + /// Manages cached mod data in MongoDB. + internal class ModCacheMongoRepository : BaseCacheRepository, IModCacheRepository + { + /********* + ** Fields + *********/ + /// The collection for cached mod data. + private readonly IMongoCollection Mods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The authenticated MongoDB database. + public ModCacheMongoRepository(IMongoDatabase database) + { + // get collections + this.Mods = database.GetCollection("mods"); + + // add indexes if needed + this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site))); + } + + + /********* + ** Public methods + *********/ + /// Get the cached mod data. + /// The mod site to search. + /// The mod's unique ID within the . + /// The fetched mod. + /// Whether to update the mod's 'last requested' date. + public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) + { + // get mod + id = this.NormalizeId(id); + mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault(); + if (mod == null) + return false; + + // bump 'last requested' + if (markRequested) + { + mod.LastRequested = DateTimeOffset.UtcNow; + mod = this.SaveMod(mod); + } + + return true; + } + + /// Save data fetched for a mod. + /// The mod site on which the mod is found. + /// The mod's unique ID within the . + /// The mod data. + /// The stored mod record. + public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) + { + id = this.NormalizeId(id); + + cachedMod = this.SaveMod(new CachedMod(site, id, mod)); + } + + /// Delete data for mods which haven't been requested within a given time limit. + /// The minimum age for which to remove mods. + public void RemoveStaleMods(TimeSpan age) + { + DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); + this.Mods.DeleteMany(p => p.LastRequested < minDate); + } + + /// Save data fetched for a mod. + /// The mod data. + public CachedMod SaveMod(CachedMod mod) + { + string id = this.NormalizeId(mod.ID); + + this.Mods.ReplaceOne( + entry => entry.ID == id && entry.Site == mod.Site, + mod, + new ReplaceOptions { IsUpsert = true } + ); + + return mod; + } + + + /********* + ** Private methods + *********/ + /// Normalize a mod ID for case-insensitive search. + /// The mod ID. + public string NormalizeId(string id) + { + return id.Trim().ToLower(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs deleted file mode 100644 index 6ba1d73d..00000000 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using MongoDB.Driver; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Framework.Caching.Mods -{ - /// Encapsulates logic for accessing the mod data cache. - internal class ModCacheRepository : BaseCacheRepository, IModCacheRepository - { - /********* - ** Fields - *********/ - /// The collection for cached mod data. - private readonly IMongoCollection Mods; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The authenticated MongoDB database. - public ModCacheRepository(IMongoDatabase database) - { - // get collections - this.Mods = database.GetCollection("mods"); - - // add indexes if needed - this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site))); - } - - /********* - ** Public methods - *********/ - /// Get the cached mod data. - /// The mod site to search. - /// The mod's unique ID within the . - /// The fetched mod. - /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) - { - // get mod - id = this.NormalizeId(id); - mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault(); - if (mod == null) - return false; - - // bump 'last requested' - if (markRequested) - { - mod.LastRequested = DateTimeOffset.UtcNow; - mod = this.SaveMod(mod); - } - - return true; - } - - /// Save data fetched for a mod. - /// The mod site on which the mod is found. - /// The mod's unique ID within the . - /// The mod data. - /// The stored mod record. - public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) - { - id = this.NormalizeId(id); - - cachedMod = this.SaveMod(new CachedMod(site, id, mod)); - } - - /// Delete data for mods which haven't been requested within a given time limit. - /// The minimum age for which to remove mods. - public void RemoveStaleMods(TimeSpan age) - { - DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); - this.Mods.DeleteMany(p => p.LastRequested < minDate); - } - - - /********* - ** Private methods - *********/ - /// Save data fetched for a mod. - /// The mod data. - public CachedMod SaveMod(CachedMod mod) - { - string id = this.NormalizeId(mod.ID); - - this.Mods.ReplaceOne( - entry => entry.ID == id && entry.Site == mod.Site, - mod, - new ReplaceOptions { IsUpsert = true } - ); - - return mod; - } - - /// Normalize a mod ID for case-insensitive search. - /// The mod ID. - public string NormalizeId(string id) - { - return id.Trim().ToLower(); - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index b54c8a2f..02097f52 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki { - /// Encapsulates logic for accessing the wiki data cache. + /// Manages cached wiki data. internal interface IWikiCacheRepository : ICacheRepository { /********* diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs new file mode 100644 index 00000000..4621f5e3 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Manages cached wiki data in-memory. + internal class WikiCacheMemoryRepository : BaseCacheRepository, IWikiCacheRepository + { + /********* + ** Fields + *********/ + /// The saved wiki metadata. + private CachedWikiMetadata Metadata; + + /// The cached wiki data. + private CachedWikiMod[] Mods = new CachedWikiMod[0]; + + + /********* + ** Public methods + *********/ + /// Get the cached wiki metadata. + /// The fetched metadata. + public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) + { + metadata = this.Metadata; + return metadata != null; + } + + /// Get the cached wiki mods. + /// A filter to apply, if any. + public IEnumerable GetWikiMods(Expression> filter = null) + { + return filter != null + ? this.Mods.Where(filter.Compile()) + : this.Mods.ToArray(); + } + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) + { + this.Metadata = cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); + this.Mods = cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs new file mode 100644 index 00000000..07e7c721 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using MongoDB.Driver; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; + +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// Manages cached wiki data in MongoDB. + internal class WikiCacheMongoRepository : BaseCacheRepository, IWikiCacheRepository + { + /********* + ** Fields + *********/ + /// The collection for wiki metadata. + private readonly IMongoCollection Metadata; + + /// The collection for wiki mod data. + private readonly IMongoCollection Mods; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The authenticated MongoDB database. + public WikiCacheMongoRepository(IMongoDatabase database) + { + // get collections + this.Metadata = database.GetCollection("wiki-metadata"); + this.Mods = database.GetCollection("wiki-mods"); + + // add indexes if needed + this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID))); + } + + /// Get the cached wiki metadata. + /// The fetched metadata. + public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) + { + metadata = this.Metadata.Find("{}").FirstOrDefault(); + return metadata != null; + } + + /// Get the cached wiki mods. + /// A filter to apply, if any. + public IEnumerable GetWikiMods(Expression> filter = null) + { + return filter != null + ? this.Mods.Find(filter).ToList() + : this.Mods.Find("{}").ToList(); + } + + /// Save data fetched from the wiki compatibility list. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + /// The mod data. + /// The stored metadata record. + /// The stored mod records. + public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) + { + cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); + cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); + + this.Mods.DeleteMany("{}"); + this.Mods.InsertMany(cachedMods); + + this.Metadata.DeleteMany("{}"); + this.Metadata.InsertOne(cachedMetadata); + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs deleted file mode 100644 index 1ae9d38f..00000000 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using MongoDB.Driver; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; - -namespace StardewModdingAPI.Web.Framework.Caching.Wiki -{ - /// Encapsulates logic for accessing the wiki data cache. - internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository - { - /********* - ** Fields - *********/ - /// The collection for wiki metadata. - private readonly IMongoCollection WikiMetadata; - - /// The collection for wiki mod data. - private readonly IMongoCollection WikiMods; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The authenticated MongoDB database. - public WikiCacheRepository(IMongoDatabase database) - { - // get collections - this.WikiMetadata = database.GetCollection("wiki-metadata"); - this.WikiMods = database.GetCollection("wiki-mods"); - - // add indexes if needed - this.WikiMods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID))); - } - - /// Get the cached wiki metadata. - /// The fetched metadata. - public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) - { - metadata = this.WikiMetadata.Find("{}").FirstOrDefault(); - return metadata != null; - } - - /// Get the cached wiki mods. - /// A filter to apply, if any. - public IEnumerable GetWikiMods(Expression> filter = null) - { - return filter != null - ? this.WikiMods.Find(filter).ToList() - : this.WikiMods.Find("{}").ToList(); - } - - /// Save data fetched from the wiki compatibility list. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. - /// The mod data. - /// The stored metadata record. - /// The stored mod records. - public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) - { - cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); - cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); - - this.WikiMods.DeleteMany("{}"); - this.WikiMods.InsertMany(cachedMods); - - this.WikiMetadata.DeleteMany("{}"); - this.WikiMetadata.InsertOne(cachedMetadata); - } - } -} diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs deleted file mode 100644 index c7b6cb00..00000000 --- a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// The config settings for mod compatibility list. - internal class MongoDbConfig - { - /********* - ** Accessors - *********/ - /// The MongoDB connection string. - public string ConnectionString { get; set; } - - /// The database name. - public string Database { get; set; } - - - /********* - ** Public method - *********/ - /// Get whether a MongoDB instance is configured. - public bool IsConfigured() - { - return !string.IsNullOrWhiteSpace(this.ConnectionString); - } - } -} diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs new file mode 100644 index 00000000..61cc4855 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// The config settings for cache storage. + internal class StorageConfig + { + /********* + ** Accessors + *********/ + /// The storage mechanism to use. + public StorageMode Mode { get; set; } + + /// The connection string for the storage mechanism, if applicable. + public string ConnectionString { get; set; } + + /// The database name for the storage mechanism, if applicable. + public string Database { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs new file mode 100644 index 00000000..4c2ea801 --- /dev/null +++ b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework.ConfigModels +{ + /// Indicates a storage mechanism to use. + internal enum StorageMode + { + /// Store data in a hosted MongoDB instance. + Mongo, + + /// Store data in an in-memory MongoDB instance. This is useful for testing MongoDB storage locally, but will likely fail when deployed since it needs permission to open a local port. + MongoInMemory, + + /// Store data in-memory. This is suitable for local testing or single-instance servers, but will cause issues when distributed across multiple servers. + InMemory + } +} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 35d22459..ddfae166 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -67,12 +67,13 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("BackgroundServices")) .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("MongoDB")) + .Configure(this.Configuration.GetSection("Storage")) .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() .AddMemoryCache(); - MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get(); + StorageConfig storageConfig = this.Configuration.GetSection("Storage").Get(); + StorageMode storageMode = storageConfig.Mode; // init MVC services @@ -82,44 +83,66 @@ namespace StardewModdingAPI.Web services .AddRazorPages(); - // init MongoDB - services.AddSingleton(_ => !mongoConfig.IsConfigured() - ? MongoDbRunner.Start() - : throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.") - ); - services.AddSingleton(serv => + // init storage + switch (storageMode) { - // get connection string - string connectionString = mongoConfig.IsConfigured() - ? mongoConfig.ConnectionString - : serv.GetRequiredService().ConnectionString; - - // get client - BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); - return new MongoClient(connectionString).GetDatabase(mongoConfig.Database); - }); - services.AddSingleton(serv => new ModCacheRepository(serv.GetRequiredService())); - services.AddSingleton(serv => new WikiCacheRepository(serv.GetRequiredService())); + case StorageMode.InMemory: + services.AddSingleton(new ModCacheMemoryRepository()); + services.AddSingleton(new WikiCacheMemoryRepository()); + break; + + case StorageMode.Mongo: + case StorageMode.MongoInMemory: + { + // local MongoDB instance + services.AddSingleton(_ => storageMode == StorageMode.MongoInMemory + ? MongoDbRunner.Start() + : throw new NotSupportedException($"The in-memory MongoDB runner isn't available in storage mode {storageMode}.") + ); + + // MongoDB + services.AddSingleton(serv => + { + BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); + return new MongoClient(this.GetMongoDbConnectionString(serv, storageConfig)) + .GetDatabase(storageConfig.Database); + }); + + // repositories + services.AddSingleton(serv => new ModCacheMongoRepository(serv.GetRequiredService())); + services.AddSingleton(serv => new WikiCacheMongoRepository(serv.GetRequiredService())); + } + break; + + default: + throw new NotSupportedException($"Unhandled storage mode '{storageMode}'."); + } // init Hangfire services - .AddHangfire(config => + .AddHangfire((serv, config) => { config .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings(); - if (mongoConfig.IsConfigured()) + switch (storageMode) { - config.UseMongoStorage(MongoClientSettings.FromConnectionString(mongoConfig.ConnectionString), $"{mongoConfig.Database}-hangfire", new MongoStorageOptions - { - MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), - CheckConnection = false // error on startup takes down entire process - }); + case StorageMode.InMemory: + config.UseMemoryStorage(); + break; + + case StorageMode.MongoInMemory: + case StorageMode.Mongo: + string connectionString = this.GetMongoDbConnectionString(serv, storageConfig); + config.UseMongoStorage(MongoClientSettings.FromConnectionString(connectionString), $"{storageConfig.Database}-hangfire", new MongoStorageOptions + { + MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), + CheckConnection = false // error on startup takes down entire process + }); + break; } - else - config.UseMemoryStorage(); }); // init background service @@ -140,6 +163,7 @@ namespace StardewModdingAPI.Web baseUrl: api.ChucklefishBaseUrl, modPageUrlFormat: api.ChucklefishModPageUrlFormat )); + services.AddSingleton(new CurseForgeClient( userAgent: userAgent, apiUrl: api.CurseForgeBaseUrl @@ -229,6 +253,20 @@ namespace StardewModdingAPI.Web settings.NullValueHandling = NullValueHandling.Ignore; } + /// Get the MongoDB connection string for the given storage configuration. + /// The service provider. + /// The storage configuration + /// There's no MongoDB instance in the given storage mode. + private string GetMongoDbConnectionString(IServiceProvider services, StorageConfig storageConfig) + { + return storageConfig.Mode switch + { + StorageMode.Mongo => storageConfig.ConnectionString, + StorageMode.MongoInMemory => services.GetRequiredService().ConnectionString, + _ => throw new NotSupportedException($"There's no MongoDB instance in storage mode {storageConfig.Mode}.") + }; + } + /// Get the redirect rules to apply. private RewriteOptions GetRedirectRules() { diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 54460c46..41c00e79 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -17,7 +17,8 @@ "NexusApiKey": null }, - "MongoDB": { + "Storage": { + "Mode": "MongoInMemory", "ConnectionString": null, "Database": "smapi-edge" }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 9cd1efc8..b1d39a6f 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -49,7 +49,8 @@ "PastebinBaseUrl": "https://pastebin.com/" }, - "MongoDB": { + "Storage": { + "Mode": "InMemory", "ConnectionString": null, "Database": "smapi" }, -- cgit From 9d86f20ca728811c1da908337a4d5e7a998e5b48 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 20:01:52 -0400 Subject: migrate subdomain redirects to Azure --- docs/release-notes.md | 1 + .../Controllers/ModsApiController.cs | 33 ----------- .../Framework/LambdaRewriteRule.cs | 37 ------------ src/SMAPI.Web.LegacyRedirects/Program.cs | 23 -------- .../Properties/launchSettings.json | 29 --------- .../SMAPI.Web.LegacyRedirects.csproj | 21 ------- .../RedirectRules/RedirectHostsToUrlsRule.cs | 54 +++++++++++++++++ .../Framework/RedirectRules/RedirectMatchRule.cs | 58 ++++++++++++++++++ .../RedirectRules/RedirectPathsToUrlsRule.cs | 54 +++++++++++++++++ .../Framework/RedirectRules/RedirectToHttpsRule.cs | 47 +++++++++++++++ .../RewriteRules/ConditionalRedirectToHttpsRule.cs | 62 -------------------- .../Framework/RewriteRules/RedirectToUrlRule.cs | 57 ------------------ src/SMAPI.Web/Startup.cs | 68 +++++++++++++++------- src/SMAPI.sln | 6 -- 14 files changed, 260 insertions(+), 290 deletions(-) delete mode 100644 src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs delete mode 100644 src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs delete mode 100644 src/SMAPI.Web.LegacyRedirects/Program.cs delete mode 100644 src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json delete mode 100644 src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj create mode 100644 src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs create mode 100644 src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs create mode 100644 src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs create mode 100644 src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs delete mode 100644 src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs delete mode 100644 src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index abe07dd9..48a47af6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ * For SMAPI developers: * When deploying web services to a single-instance app, the MongoDB server can now be replaced with in-memory storage. + * Merged the separate legacy redirects app on AWS into the main app on Azure. ## 3.5 Released 27 April 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs b/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs deleted file mode 100644 index 44ed0b6b..00000000 --- a/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; - -namespace SMAPI.Web.LegacyRedirects.Controllers -{ - /// Provides an API to perform mod update checks. - [ApiController] - [Produces("application/json")] - [Route("api/v{version}/mods")] - public class ModsApiController : Controller - { - /********* - ** Public methods - *********/ - /// Fetch version metadata for the given mods. - /// The mod search criteria. - [HttpPost] - public async Task> PostAsync([FromBody] ModSearchModel model) - { - using IClient client = new FluentClient("https://smapi.io/api"); - - Startup.ConfigureJsonNet(client.Formatters.JsonFormatter.SerializerSettings); - - return await client - .PostAsync(this.Request.Path) - .WithBody(model) - .AsArray(); - } - } -} diff --git a/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs b/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs deleted file mode 100644 index e5138e5c..00000000 --- a/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; - -namespace SMAPI.Web.LegacyRedirects.Framework -{ - /// Rewrite requests to prepend the subdomain portion (if any) to the path. - /// Derived from . - internal class LambdaRewriteRule : IRule - { - /********* - ** Accessors - *********/ - /// Rewrite an HTTP request if needed. - private readonly Action Rewrite; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Rewrite an HTTP request if needed. - public LambdaRewriteRule(Action rewrite) - { - this.Rewrite = rewrite ?? throw new ArgumentNullException(nameof(rewrite)); - } - - /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). - /// The rewrite context. - public void ApplyRule(RewriteContext context) - { - HttpRequest request = context.HttpContext.Request; - HttpResponse response = context.HttpContext.Response; - this.Rewrite(context, request, response); - } - } -} diff --git a/src/SMAPI.Web.LegacyRedirects/Program.cs b/src/SMAPI.Web.LegacyRedirects/Program.cs deleted file mode 100644 index 6adee877..00000000 --- a/src/SMAPI.Web.LegacyRedirects/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; - -namespace SMAPI.Web.LegacyRedirects -{ - /// The main app entry point. - public class Program - { - /********* - ** Public methods - *********/ - /// The main app entry point. - /// The command-line arguments. - public static void Main(string[] args) - { - Host - .CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(builder => builder.UseStartup()) - .Build() - .Run(); - } - } -} diff --git a/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json b/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json deleted file mode 100644 index e9a1b210..00000000 --- a/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:52756", - "sslPort": 0 - } - }, - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "SMAPI.Web.LegacyRedirects": { - "commandName": "Project", - "launchBrowser": true, - "launchUrl": "/", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" - } - } -} \ No newline at end of file diff --git a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj deleted file mode 100644 index 15ca7272..00000000 --- a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netcoreapp3.0 - - - - - - - - - - - - - - - - - diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs new file mode 100644 index 00000000..d75ee791 --- /dev/null +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs @@ -0,0 +1,54 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RedirectRules +{ + /// Redirect hostnames to a URL if they match a condition. + internal class RedirectHostsToUrlsRule : RedirectMatchRule + { + /********* + ** Fields + *********/ + /// Maps a lowercase hostname to the resulting redirect URL. + private readonly Func Map; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The status code to use for redirects. + /// Hostnames mapped to the resulting redirect URL. + public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func map) + { + this.StatusCode = statusCode; + this.Map = map ?? throw new ArgumentNullException(nameof(map)); + } + + + /********* + ** Private methods + *********/ + /// Get the new redirect URL. + /// The rewrite context. + /// Returns the redirect URL, or null if the redirect doesn't apply. + protected override string GetNewUrl(RewriteContext context) + { + // get requested host + string host = context.HttpContext.Request.Host.Host; + if (host == null) + return null; + + // get new host + host = this.Map(host); + if (host == null) + return null; + + // rewrite URL + UriBuilder uri = this.GetUrl(context.HttpContext.Request); + uri.Host = host; + return uri.ToString(); + } + } +} diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs new file mode 100644 index 00000000..6e81c4ca --- /dev/null +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs @@ -0,0 +1,58 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RedirectRules +{ + /// Redirect matching requests to a URL. + internal abstract class RedirectMatchRule : IRule + { + /********* + ** Fields + *********/ + /// The status code to use for redirects. + protected HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Redirect; + + + /********* + ** Public methods + *********/ + /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). + /// The rewrite context. + public void ApplyRule(RewriteContext context) + { + string newUrl = this.GetNewUrl(context); + if (newUrl == null) + return; + + HttpResponse response = context.HttpContext.Response; + response.StatusCode = (int)HttpStatusCode.Redirect; + response.Headers["Location"] = newUrl; + context.Result = RuleResult.EndResponse; + } + + + /********* + ** Protected methods + *********/ + /// Get the new redirect URL. + /// The rewrite context. + /// Returns the redirect URL, or null if the redirect doesn't apply. + protected abstract string GetNewUrl(RewriteContext context); + + /// Get the full request URL. + /// The request. + protected UriBuilder GetUrl(HttpRequest request) + { + return new UriBuilder + { + Scheme = request.Scheme, + Host = request.Host.Host, + Port = request.Host.Port ?? -1, + Path = request.PathBase + request.Path, + Query = request.QueryString.Value + }; + } + } +} diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs new file mode 100644 index 00000000..16397c1e --- /dev/null +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RedirectRules +{ + /// Redirect paths to URLs if they match a condition. + internal class RedirectPathsToUrlsRule : RedirectMatchRule + { + /********* + ** Fields + *********/ + /// Regex patterns matching the current URL mapped to the resulting redirect URL. + private readonly IDictionary Map; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Regex patterns matching the current URL mapped to the resulting redirect URL. + public RedirectPathsToUrlsRule(IDictionary map) + { + this.Map = map.ToDictionary( + p => new Regex(p.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled), + p => p.Value + ); + } + + + /********* + ** Protected methods + *********/ + /// Get the new redirect URL. + /// The rewrite context. + /// Returns the redirect URL, or null if the redirect doesn't apply. + protected override string GetNewUrl(RewriteContext context) + { + string path = context.HttpContext.Request.Path.Value; + + if (!string.IsNullOrWhiteSpace(path)) + { + foreach ((Regex pattern, string url) in this.Map) + { + if (pattern.IsMatch(path)) + return pattern.Replace(path, url); + } + } + + return null; + } + } +} diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs new file mode 100644 index 00000000..2a503ae3 --- /dev/null +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs @@ -0,0 +1,47 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Rewrite; + +namespace StardewModdingAPI.Web.Framework.RedirectRules +{ + /// Redirect requests to HTTPS. + internal class RedirectToHttpsRule : RedirectMatchRule + { + /********* + ** Fields + *********/ + /// Matches requests which should be ignored. + private readonly Func Except; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Matches requests which should be ignored. + public RedirectToHttpsRule(Func except = null) + { + this.Except = except ?? (req => false); + this.StatusCode = HttpStatusCode.RedirectKeepVerb; + } + + + /********* + ** Protected methods + *********/ + /// Get the new redirect URL. + /// The rewrite context. + /// Returns the redirect URL, or null if the redirect doesn't apply. + protected override string GetNewUrl(RewriteContext context) + { + HttpRequest request = context.HttpContext.Request; + if (request.IsHttps || this.Except(request)) + return null; + + UriBuilder uri = this.GetUrl(request); + uri.Scheme = "https"; + return uri.ToString(); + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs deleted file mode 100644 index 36effd82..00000000 --- a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Net; -using System.Text; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; - -namespace StardewModdingAPI.Web.Framework.RewriteRules -{ - /// Redirect requests to HTTPS. - /// Derived from and . - internal class ConditionalRedirectToHttpsRule : IRule - { - /********* - ** Fields - *********/ - /// A predicate which indicates when the rule should be applied. - private readonly Func ShouldRewrite; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A predicate which indicates when the rule should be applied. - public ConditionalRedirectToHttpsRule(Func shouldRewrite = null) - { - this.ShouldRewrite = shouldRewrite ?? (req => true); - } - - /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). - /// The rewrite context. - public void ApplyRule(RewriteContext context) - { - HttpRequest request = context.HttpContext.Request; - - // check condition - if (this.IsSecure(request) || !this.ShouldRewrite(request)) - return; - - // redirect request - HttpResponse response = context.HttpContext.Response; - response.StatusCode = (int)HttpStatusCode.RedirectKeepVerb; - response.Headers["Location"] = new StringBuilder() - .Append("https://") - .Append(request.Host.Host) - .Append(request.PathBase) - .Append(request.Path) - .Append(request.QueryString) - .ToString(); - context.Result = RuleResult.EndResponse; - } - - /// Get whether the request was received over HTTPS. - /// The request to check. - public bool IsSecure(HttpRequest request) - { - return - request.IsHttps // HTTPS to server - || string.Equals(request.Headers["x-forwarded-proto"], "HTTPS", StringComparison.OrdinalIgnoreCase); // HTTPS to AWS load balancer - } - } -} diff --git a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs deleted file mode 100644 index ab9e019c..00000000 --- a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Net; -using System.Text.RegularExpressions; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; - -namespace StardewModdingAPI.Web.Framework.RewriteRules -{ - /// Redirect requests to an external URL if they match a condition. - internal class RedirectToUrlRule : IRule - { - /********* - ** Fields - *********/ - /// Get the new URL to which to redirect (or null to skip). - private readonly Func NewUrl; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A predicate which indicates when the rule should be applied. - /// The new URL to which to redirect. - public RedirectToUrlRule(Func shouldRewrite, string url) - { - this.NewUrl = req => shouldRewrite(req) ? url : null; - } - - /// Construct an instance. - /// A case-insensitive regex to match against the path. - /// The external URL. - public RedirectToUrlRule(string pathRegex, string url) - { - Regex regex = new Regex(pathRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled); - this.NewUrl = req => req.Path.HasValue ? regex.Replace(req.Path.Value, url) : null; - } - - /// Applies the rule. Implementations of ApplyRule should set the value for (defaults to RuleResult.ContinueRules). - /// The rewrite context. - public void ApplyRule(RewriteContext context) - { - HttpRequest request = context.HttpContext.Request; - - // check rewrite - string newUrl = this.NewUrl(request); - if (newUrl == null || newUrl == request.Path.Value) - return; - - // redirect request - HttpResponse response = context.HttpContext.Response; - response.StatusCode = (int)HttpStatusCode.Redirect; - response.Headers["Location"] = newUrl; - context.Result = RuleResult.EndResponse; - } - } -} diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index ddfae166..dee2edc2 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using Hangfire; using Hangfire.MemoryStorage; using Hangfire.Mongo; @@ -27,7 +28,7 @@ using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.Compression; using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.RewriteRules; +using StardewModdingAPI.Web.Framework.RedirectRules; using StardewModdingAPI.Web.Framework.Storage; namespace StardewModdingAPI.Web @@ -270,26 +271,49 @@ namespace StardewModdingAPI.Web /// Get the redirect rules to apply. private RewriteOptions GetRedirectRules() { - var redirects = new RewriteOptions(); - - // redirect to HTTPS (except API for Linux/Mac Mono compatibility) - redirects.Add(new ConditionalRedirectToHttpsRule( - shouldRewrite: req => - req.Host.Host != "localhost" - && !req.Path.StartsWithSegments("/api") - )); - - // shortcut redirects - redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0")); - redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released - redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community")); - redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods")); - redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); - redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); - redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1")); - redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods")); - - // redirect legacy canimod.com URLs + var redirects = new RewriteOptions() + // shortcut paths + .Add(new RedirectPathsToUrlsRule(new Dictionary + { + [@"^/3\.0\.?$"] = "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0", + [@"^/(?:buildmsg|package)(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", // buildmsg deprecated, remove when SDV 1.4 is released + [@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community", + [@"^/compat\.?$"] = "https://smapi.io/mods", + [@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index", + [@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI", + [@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1", + [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods" + })) + + // legacy paths + .Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects())) + + // subdomains + .Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch + { + "api.smapi.io" => "smapi.io/api", + "json.smapi.io" => "smapi.io/json", + "log.smapi.io" => "smapi.io/log", + "mods.smapi.io" => "smapi.io/mods", + _ => host.EndsWith(".smapi.io") + ? "smapi.io" + : null + })) + + // redirect to HTTPS (except API for Linux/Mac Mono compatibility) + .Add( + new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api")) + ); + + return redirects; + } + + /// Get the redirects for legacy paths that have been moved elsewhere. + private IDictionary GetLegacyPathRedirects() + { + var redirects = new Dictionary(); + + // canimod.com => wiki var wikiRedirects = new Dictionary { ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" }, @@ -306,7 +330,7 @@ namespace StardewModdingAPI.Web foreach ((string page, string[] patterns) in wikiRedirects) { foreach (string pattern in patterns) - redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + page)); + redirects.Add(pattern, "https://stardewvalleywiki.com/" + page); } return redirects; diff --git a/src/SMAPI.sln b/src/SMAPI.sln index f9c537c4..92b0cd2c 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -77,8 +77,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit.CoreInterface EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web", "SMAPI.Web\SMAPI.Web.csproj", "{80EFD92F-728F-41E0-8A5B-9F6F49A91899}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web.LegacyRedirects", "SMAPI.Web.LegacyRedirects\SMAPI.Web.LegacyRedirects.csproj", "{159AA5A5-35C2-488C-B23F-1613C80594AE}" -EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 @@ -138,10 +136,6 @@ Global {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU - {159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE -- cgit From aa5cc2c9be8bdc79c6fa7b1b9c2581a05b88117d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 16 May 2020 20:03:08 -0400 Subject: fix GitHub license images not using HTTPS --- src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index c62e1466..cda2923d 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -111,7 +111,7 @@ else source @* see https://shields.io/category/license *@ - (source) + (source) no source -- cgit From b34d7470e2769a50e87a33e1cb3a8da637a2f143 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 May 2020 17:24:16 -0400 Subject: simplify patch facade (#711) Thanks to 0x0ade for the suggestion! --- src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs index 995a40d6..8e4ef7df 100644 --- a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using System.Reflection.Emit; using HarmonyLib; @@ -31,7 +30,7 @@ namespace StardewModdingAPI.Framework.RewriteFacades try { MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); - return new DynamicMethod(method.Name, method.Attributes, method.CallingConvention, method.ReturnType, method.GetParameters().Select(p => p.ParameterType).ToArray(), method.Module, true); + return (DynamicMethod)method; } catch (Exception ex) { -- cgit From 21303a4e987e4169f3bf0c55c7099d0d07536ca5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 May 2020 17:26:47 -0400 Subject: remove workaround no longer needed with Harmony 2.0.2 (#711) --- src/SMAPI/Constants.cs | 3 --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 30 ++++-------------------- src/SMAPI/Framework/SCore.cs | 16 +------------ 3 files changed, 6 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 907a93b2..a898fccd 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -61,9 +61,6 @@ namespace StardewModdingAPI /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; - /// The folder containing temporary files that are only valid for the current session. - internal static string InternalTempFilesPath => Path.Combine(Program.DllSearchPath, ".temp"); - /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 78e717e9..b95a45b5 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -36,9 +36,6 @@ namespace StardewModdingAPI.Framework.ModLoading /// The objects to dispose as part of this instance. private readonly HashSet Disposables = new HashSet(); - /// The full path to the folder in which to save rewritten assemblies. - private readonly string TempFolderPath; - /********* ** Public methods @@ -47,13 +44,11 @@ namespace StardewModdingAPI.Framework.ModLoading /// The current game platform. /// Encapsulates monitoring and logging. /// Whether to detect paranoid mode issues. - /// The full path to the folder in which to save rewritten assemblies. - public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode, string tempFolderPath) + public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMode) { this.Monitor = monitor; this.ParanoidMode = paranoidMode; this.AssemblyMap = this.TrackForDisposal(Constants.GetAssemblyMap(targetPlatform)); - this.TempFolderPath = tempFolderPath; // init resolver this.AssemblyDefinitionResolver = this.TrackForDisposal(new AssemblyDefinitionResolver()); @@ -133,25 +128,10 @@ namespace StardewModdingAPI.Framework.ModLoading if (!oneAssembly) this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); - if (assembly.Definition.MainModule.AssemblyReferences.Any(p => p.Name == "0Harmony")) - { - // Note: the assembly must be loaded from disk for Harmony compatibility. - // Loading it from memory sets the assembly module's FullyQualifiedName to - // "", so Harmony incorrectly identifies the module in its - // Patch.PatchMethod when handling multiple patches for the same method, - // leading to "Token 0x... is not valid in the scope of module HarmonySharedState" - // errors (e.g. https://smapi.io/log/A0gAsc3M). - string tempPath = Path.Combine(this.TempFolderPath, $"{Path.GetFileNameWithoutExtension(assemblyPath)}.{Guid.NewGuid()}.dll"); - assembly.Definition.Write(tempPath); - lastAssembly = Assembly.LoadFile(tempPath); - } - else - { - using MemoryStream outStream = new MemoryStream(); - assembly.Definition.Write(outStream); - byte[] bytes = outStream.ToArray(); - lastAssembly = Assembly.Load(bytes); - } + using MemoryStream outStream = new MemoryStream(); + assembly.Definition.Write(outStream); + byte[] bytes = outStream.ToArray(); + lastAssembly = Assembly.Load(bytes); } else { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 12dc9c3d..de9c955d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -213,20 +213,6 @@ namespace StardewModdingAPI.Framework return; } #endif - - // reset temp folder - if (Directory.Exists(Constants.InternalTempFilesPath)) - { - try - { - FileUtilities.ForceDelete(new DirectoryInfo(Constants.InternalTempFilesPath)); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't delete temporary files at {Constants.InternalTempFilesPath}: {ex}", LogLevel.Trace); - } - } - Directory.CreateDirectory(Constants.InternalTempFilesPath); } /// Launch SMAPI. @@ -762,7 +748,7 @@ namespace StardewModdingAPI.Framework // load mods IDictionary> skippedMods = new Dictionary>(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, Constants.InternalTempFilesPath)) + using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase); -- cgit From d1bf3d52352df4bb720cf0fa87dcd6a64f35446a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 May 2020 22:44:06 -0400 Subject: move facade namespace (#711) --- .../RewriteFacades/AccessToolsMethods.cs | 32 ++++++++++++ .../RewriteFacades/HarmonyInstanceMethods.cs | 49 +++++++++++++++++ .../RewriteFacades/SpriteBatchMethods.cs | 61 ++++++++++++++++++++++ .../Framework/RewriteFacades/AccessToolsMethods.cs | 32 ------------ .../RewriteFacades/HarmonyInstanceMethods.cs | 49 ----------------- .../Framework/RewriteFacades/SpriteBatchMethods.cs | 61 ---------------------- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- 7 files changed, 143 insertions(+), 143 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs delete mode 100644 src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs delete mode 100644 src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs delete mode 100644 src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs new file mode 100644 index 00000000..ea35fec9 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class AccessToolsMethods + { + /********* + ** Public methods + *********/ + public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) + { + return AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true); + } + + public static ConstructorInfo Constructor(Type type, Type[] parameters = null) + { + return AccessTools.Constructor(type, parameters, searchForStatic: true); + } + + public static List GetDeclaredConstructors(Type type) + { + return AccessTools.GetDeclaredConstructors(type, searchForStatic: true); + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs new file mode 100644 index 00000000..78cf25f8 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] + public class HarmonyInstanceMethods : Harmony + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique patch identifier. + public HarmonyInstanceMethods(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) + { + try + { + MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); + return (DynamicMethod)method; + } + catch (Exception ex) + { + var patchTypes = new List(); + if (prefix != null) + patchTypes.Add("prefix"); + if (postfix != null) + patchTypes.Add("postfix"); + if (transpiler != null) + patchTypes.Add("transpiler"); + + throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}.", ex); + } + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs new file mode 100644 index 00000000..75bb61ef --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +#pragma warning disable 1591 // missing documentation +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + public class SpriteBatchMethods : SpriteBatch + { + /********* + ** Public methods + *********/ + /// Construct an instance. + public SpriteBatchMethods(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); + } + + /**** + ** 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/RewriteFacades/AccessToolsMethods.cs b/src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs deleted file mode 100644 index cb40bbcc..00000000 --- a/src/SMAPI/Framework/RewriteFacades/AccessToolsMethods.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using HarmonyLib; - -namespace StardewModdingAPI.Framework.RewriteFacades -{ - /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] - public class AccessToolsMethods - { - /********* - ** Public methods - *********/ - public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) - { - return AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true); - } - - public static ConstructorInfo Constructor(Type type, Type[] parameters = null) - { - return AccessTools.Constructor(type, parameters, searchForStatic: true); - } - - public static List GetDeclaredConstructors(Type type) - { - return AccessTools.GetDeclaredConstructors(type, searchForStatic: true); - } - } -} diff --git a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs deleted file mode 100644 index 8e4ef7df..00000000 --- a/src/SMAPI/Framework/RewriteFacades/HarmonyInstanceMethods.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; - -namespace StardewModdingAPI.Framework.RewriteFacades -{ - /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] - public class HarmonyInstanceMethods : Harmony - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique patch identifier. - public HarmonyInstanceMethods(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) - { - try - { - MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); - return (DynamicMethod)method; - } - catch (Exception ex) - { - var patchTypes = new List(); - if (prefix != null) - patchTypes.Add("prefix"); - if (postfix != null) - patchTypes.Add("postfix"); - if (transpiler != null) - patchTypes.Add("transpiler"); - - throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}.", ex); - } - } - } -} diff --git a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs deleted file mode 100644 index 26b22315..00000000 --- a/src/SMAPI/Framework/RewriteFacades/SpriteBatchMethods.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -#pragma warning disable 1591 // missing documentation -namespace StardewModdingAPI.Framework.RewriteFacades -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - public class SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(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); - } - - /**** - ** 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/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 40a7588e..d80f64e2 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -3,8 +3,8 @@ using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.ModLoading.Finders; +using StardewModdingAPI.Framework.ModLoading.RewriteFacades; using StardewModdingAPI.Framework.ModLoading.Rewriters; -using StardewModdingAPI.Framework.RewriteFacades; using StardewValley; namespace StardewModdingAPI.Metadata -- cgit From f96dde00f98a913557617f716673f1af355cc6b5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 18 May 2020 23:11:17 -0400 Subject: fix some type references not being rewritten (#711) --- src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs index 04b2e08d..8c85b6a5 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs @@ -125,6 +125,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework return true; } + // type reference + if (instruction.Operand is TypeReference typeRef && this.IsMatch(typeRef)) + return true; + return false; } -- cgit From 1838842bbc2db2d1049c193b8650bd101ba4858f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 May 2020 20:57:50 -0400 Subject: rewrite assembly rewriting, merge Harmony rewriters (#711) This reduces duplication, decouples it from the assembly loader, and makes it more flexible to handle Harmony rewriting. --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 100 ++++---- .../Framework/ModLoading/Finders/EventFinder.cs | 18 +- .../Framework/ModLoading/Finders/FieldFinder.cs | 31 +-- .../Framework/ModLoading/Finders/MethodFinder.cs | 18 +- .../Framework/ModLoading/Finders/PropertyFinder.cs | 18 +- .../ReferenceToMemberWithUnexpectedTypeFinder.cs | 27 +-- .../Finders/ReferenceToMissingMemberFinder.cs | 28 ++- .../ModLoading/Finders/TypeAssemblyFinder.cs | 42 +++- .../Framework/ModLoading/Finders/TypeFinder.cs | 42 +++- .../ModLoading/Framework/BaseInstructionHandler.cs | 66 ++++-- .../ModLoading/Framework/BaseTypeFinder.cs | 172 -------------- .../Framework/BaseTypeReferenceRewriter.cs | 209 ----------------- .../ModLoading/Framework/RecursiveRewriter.cs | 260 +++++++++++++++++++++ .../ModLoading/Framework/RewriteHelper.cs | 199 ++++++++++++++++ .../Framework/ModLoading/IInstructionHandler.cs | 35 +-- src/SMAPI/Framework/ModLoading/RewriteHelper.cs | 177 -------------- .../ModLoading/Rewriters/FieldReplaceRewriter.cs | 37 +-- .../Rewriters/FieldToPropertyRewriter.cs | 42 ++-- .../Rewriters/Harmony1AssemblyRewriter.cs | 107 ++++++--- .../ModLoading/Rewriters/MethodParentRewriter.cs | 40 ++-- .../Rewriters/StaticFieldToConstantRewriter.cs | 35 ++- .../ModLoading/Rewriters/TypeReferenceRewriter.cs | 50 ++-- src/SMAPI/Metadata/InstructionMetadata.cs | 8 +- 23 files changed, 879 insertions(+), 882 deletions(-) delete mode 100644 src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs delete mode 100644 src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs create mode 100644 src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs delete mode 100644 src/SMAPI/Framework/ModLoading/RewriteHelper.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index b95a45b5..d9b4af1b 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -4,8 +4,8 @@ using System.IO; using System.Linq; 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; @@ -283,54 +283,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 (TypeDefinition type in module.GetTypes()) - { - // check type 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, type, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); - if (result == InstructionHandleResult.Rewritten) - anyRewritten = true; - } - - // check methods - foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) + bool rewritten = false; + foreach (IInstructionHandler handler in handlers) + rewritten |= handler.Handle(module, type, replaceWith); + return rewritten; + }, + rewriteInstruction: (instruction, cil, replaceWith) => { - // check method definition + bool rewritten = false; foreach (IInstructionHandler handler in handlers) - { - 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++) - { - Instruction instruction = instructions[offset]; - if (instruction.OpCode.Code == Code.Nop) - continue; - - foreach (IInstructionHandler handler in handlers) - { - InstructionHandleResult result = handler.Handle(module, cil, instruction, this.AssemblyMap, platformChanged); - this.ProcessInstructionHandleResult(mod, handler, result, loggedMessages, logPrefix, filename); - if (result == InstructionHandleResult.Rewritten) - { - instruction = instructions[offset]; - 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; @@ -345,49 +323,52 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly filename for log messages. private void ProcessInstructionHandleResult(IModMetadata mod, IInstructionHandler handler, InstructionHandleResult result, HashSet 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; @@ -397,6 +378,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)); } /// Get the correct reference to use for compatibility with the current platform. diff --git a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs index 1a7ae636..e1476b73 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/EventFinder.cs @@ -1,3 +1,4 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -28,24 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The event name for which to find references. /// The result to return for matching instructions. public EventFinder(string fullTypeName, string eventName, InstructionHandleResult result) - : base(nounPhrase: $"{fullTypeName}.{eventName} event") + : base(defaultPhrase: $"{fullTypeName}.{eventName} event") { this.FullTypeName = fullTypeName; this.EventName = eventName; this.Result = result; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 9ae07916..c157ed9b 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/FieldFinder.cs @@ -1,3 +1,4 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -28,39 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The field name for which to find references. /// The result to return for matching instructions. public FieldFinder(string fullTypeName, string fieldName, InstructionHandleResult result) - : base(nounPhrase: $"{fullTypeName}.{fieldName} field") + : base(defaultPhrase: $"{fullTypeName}.{fieldName} field") { this.FullTypeName = fullTypeName; this.FieldName = fieldName; this.Result = result; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - 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 75584f1f..82c93a7c 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/MethodFinder.cs @@ -1,3 +1,4 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -28,24 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The method name for which to find references. /// The result to return for matching instructions. public MethodFinder(string fullTypeName, string methodName, InstructionHandleResult result) - : base(nounPhrase: $"{fullTypeName}.{methodName} method") + : base(defaultPhrase: $"{fullTypeName}.{methodName} method") { this.FullTypeName = fullTypeName; this.MethodName = methodName; this.Result = result; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 811420c5..c96d61a2 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/PropertyFinder.cs @@ -1,3 +1,4 @@ +using System; using Mono.Cecil; using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; @@ -28,24 +29,25 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// The property name for which to find references. /// The result to return for matching instructions. public PropertyFinder(string fullTypeName, string propertyName, InstructionHandleResult result) - : base(nounPhrase: $"{fullTypeName}.{propertyName} property") + : base(defaultPhrase: $"{fullTypeName}.{propertyName} property") { this.FullTypeName = fullTypeName; this.PropertyName = propertyName; this.Result = result; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 1029d350..a67cfa4f 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMemberWithUnexpectedTypeFinder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -23,18 +24,18 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The assembly names to which to heuristically detect broken references. public ReferenceToMemberWithUnexpectedTypeFinder(string[] validateReferencesToAssemblies) - : base(nounPhrase: "") + : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -43,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; } } @@ -60,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 fefa88f4..ebb62948 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/ReferenceToMissingMemberFinder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Mono.Cecil; @@ -23,18 +24,18 @@ namespace StardewModdingAPI.Framework.ModLoading.Finders /// Construct an instance. /// The assembly names to which to heuristically detect broken references. public ReferenceToMissingMemberFinder(string[] validateReferencesToAssemblies) - : base(nounPhrase: "") + : base(defaultPhrase: "") { this.ValidateReferencesToAssemblies = new HashSet(validateReferencesToAssemblies); } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { // field reference FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); @@ -43,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; } } @@ -55,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 index 5301186b..a1ade536 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeAssemblyFinder.cs @@ -5,21 +5,47 @@ using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference types in a given assembly. - internal class TypeAssemblyFinder : BaseTypeFinder + internal class TypeAssemblyFinder : BaseInstructionHandler { + /********* + ** Fields + *********/ + /// The full assembly name to which to find references. + private readonly string AssemblyName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + /// Get whether a matched type should be ignored. + private readonly Func ShouldIgnore; + + /********* ** Public methods *********/ /// Construct an instance. /// The full assembly name to which to find references. /// The result to return for matching instructions. - /// A lambda which overrides a matched type. + /// Get whether a matched type should be ignored. public TypeAssemblyFinder(string assemblyName, InstructionHandleResult result, Func shouldIgnore = null) - : base( - isMatch: type => type.Scope.Name == assemblyName && (shouldIgnore == null || !shouldIgnore(type)), - result: result, - nounPhrase: $"{assemblyName} assembly" - ) - { } + : base(defaultPhrase: $"{assemblyName} assembly") + { + this.AssemblyName = assemblyName; + this.Result = result; + this.ShouldIgnore = shouldIgnore; + } + + /// Rewrite a type reference if needed. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + public override bool Handle(ModuleDefinition module, TypeReference type, Action 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 3adc31c7..c285414a 100644 --- a/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs +++ b/src/SMAPI/Framework/ModLoading/Finders/TypeFinder.cs @@ -5,21 +5,47 @@ using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Finders { /// Finds incompatible CIL instructions that reference a given type. - internal class TypeFinder : BaseTypeFinder + internal class TypeFinder : BaseInstructionHandler { + /********* + ** Fields + *********/ + /// The full type name to match. + private readonly string FullTypeName; + + /// The result to return for matching instructions. + private readonly InstructionHandleResult Result; + + /// Get whether a matched type should be ignored. + private readonly Func ShouldIgnore; + + /********* ** Public methods *********/ /// Construct an instance. /// The full type name to match. /// The result to return for matching instructions. - /// A lambda which overrides a matched type. + /// Get whether a matched type should be ignored. public TypeFinder(string fullTypeName, InstructionHandleResult result, Func shouldIgnore = null) - : base( - isMatch: type => type.FullName == fullTypeName && (shouldIgnore == null || !shouldIgnore(type)), - result: result, - nounPhrase: $"{fullTypeName} type" - ) - { } + : base(defaultPhrase: $"{fullTypeName} type") + { + this.FullTypeName = fullTypeName; + this.Result = result; + this.ShouldIgnore = shouldIgnore; + } + + /// Rewrite a type reference if needed. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) + { + 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 index 353de464..79fb45b8 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/BaseInstructionHandler.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Mono.Cecil; using Mono.Cecil.Cil; @@ -9,42 +11,38 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /********* ** Accessors *********/ - /// A brief noun phrase indicating what the handler matches. - public string NounPhrase { get; protected set; } + /// A brief noun phrase indicating what the handler matches, used if is empty. + public string DefaultPhrase { get; } + + /// The rewrite flags raised for the current module. + public ISet Flags { get; } = new HashSet(); + + /// The brief noun phrases indicating what the handler matched for the current module. + public ISet Phrases { get; } = new HashSet(StringComparer.InvariantCultureIgnoreCase); /********* ** Public methods *********/ - /// Perform the predefined logic for a method if applicable. + /// Rewrite a type reference if needed. /// The assembly module containing the instruction. /// The type definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return InstructionHandleResult.None; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + public virtual bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { - return InstructionHandleResult.None; + return false; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public virtual InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public virtual bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - return InstructionHandleResult.None; + return false; } @@ -52,10 +50,28 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework ** Protected methods *********/ /// Construct an instance. - /// A brief noun phrase indicating what the handler matches. - protected BaseInstructionHandler(string nounPhrase) + /// A brief noun phrase indicating what the handler matches. + protected BaseInstructionHandler(string defaultPhrase) + { + this.DefaultPhrase = defaultPhrase; + } + + /// Raise a result flag. + /// The result flag to set. + /// The result message to add. + /// Returns true for convenience. + protected bool MarkFlag(InstructionHandleResult flag, string resultMessage = null) + { + this.Flags.Add(flag); + if (resultMessage != null) + this.Phrases.Add(resultMessage); + return true; + } + + /// Raise a generic flag indicating that the code was rewritten. + public bool MarkRewritten() { - this.NounPhrase = nounPhrase; + return this.MarkFlag(InstructionHandleResult.Rewritten); } } } diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs deleted file mode 100644 index 8c85b6a5..00000000 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeFinder.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading.Framework -{ - /// Finds incompatible CIL type reference instructions. - internal abstract class BaseTypeFinder : BaseInstructionHandler - { - /********* - ** Accessors - *********/ - /// Matches the type references to handle. - private readonly Func IsMatchImpl; - - /// The result to return for matching instructions. - private readonly InstructionHandleResult Result; - - - /********* - ** Public methods - *********/ - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(method) - ? this.Result - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - return this.IsMatch(instruction) - ? this.Result - : InstructionHandleResult.None; - } - - /// Get whether a CIL instruction matches. - /// The method definition. - public bool IsMatch(MethodDefinition method) - { - // return type - if (this.IsMatch(method.ReturnType)) - return true; - - // parameters - foreach (ParameterDefinition parameter in method.Parameters) - { - if (this.IsMatch(parameter.ParameterType)) - return true; - } - - // generic parameters - foreach (GenericParameter parameter in method.GenericParameters) - { - if (this.IsMatch(parameter)) - return true; - } - - // custom attributes - foreach (CustomAttribute attribute in method.CustomAttributes) - { - if (this.IsMatch(attribute.AttributeType)) - return true; - - foreach (var arg in attribute.ConstructorArguments) - { - if (this.IsMatch(arg.Type)) - return true; - } - } - - // local variables - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.IsMatch(variable.VariableType)) - return true; - } - - return false; - } - - /// Get whether a CIL instruction matches. - /// The IL instruction. - public 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) - { - // method on target class - if (this.IsMatch(methodRef.DeclaringType)) - return true; - - // method returns target class - if (this.IsMatch(methodRef.ReturnType)) - return true; - - // method parameters of target class - if (methodRef.Parameters.Any(p => this.IsMatch(p.ParameterType))) - return true; - - // generic args of target class - if (methodRef is GenericInstanceMethod genericRef && genericRef.GenericArguments.Any(this.IsMatch)) - return true; - } - - // type reference - if (instruction.Operand is TypeReference typeRef && this.IsMatch(typeRef)) - return true; - - return false; - } - - /// Get whether a type reference matches the expected type. - /// The type to check. - public bool IsMatch(TypeReference type) - { - // root type - if (this.IsMatchImpl(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; - - return false; - } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// Matches the type references to handle. - /// The result to return for matching instructions. - /// A brief noun phrase indicating what the instruction finder matches. - protected BaseTypeFinder(Func isMatch, InstructionHandleResult result, string nounPhrase) - : base(nounPhrase) - { - this.IsMatchImpl = isMatch; - this.Result = result; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs deleted file mode 100644 index 3ccacf22..00000000 --- a/src/SMAPI/Framework/ModLoading/Framework/BaseTypeReferenceRewriter.cs +++ /dev/null @@ -1,209 +0,0 @@ -using System; -using System.Linq; -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Collections.Generic; - -namespace StardewModdingAPI.Framework.ModLoading.Framework -{ - /// Rewrites all references to a type. - internal abstract class BaseTypeReferenceRewriter : BaseInstructionHandler - { - /********* - ** Fields - *********/ - /// The type finder which matches types to rewrite. - private readonly BaseTypeFinder Finder; - - - /********* - ** Public methods - *********/ - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The type definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - bool rewritten = this.RewriteCustomAttributesIfNeeded(module, type.CustomAttributes); - - return rewritten - ? InstructionHandleResult.Rewritten - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - bool rewritten = false; - - // return type - if (this.Finder.IsMatch(method.ReturnType)) - rewritten |= this.RewriteIfNeeded(module, method.ReturnType, newType => method.ReturnType = newType); - - // parameters - foreach (ParameterDefinition parameter in method.Parameters) - { - if (this.Finder.IsMatch(parameter.ParameterType)) - rewritten |= this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); - } - - // generic parameters - for (int i = 0; i < method.GenericParameters.Count; i++) - { - var parameter = method.GenericParameters[i]; - if (this.Finder.IsMatch(parameter)) - rewritten |= this.RewriteIfNeeded(module, parameter, newType => method.GenericParameters[i] = new GenericParameter(parameter.Name, newType)); - } - - // custom attributes - rewritten |= this.RewriteCustomAttributesIfNeeded(module, method.CustomAttributes); - - // local variables - foreach (VariableDefinition variable in method.Body.Variables) - { - if (this.Finder.IsMatch(variable.VariableType)) - rewritten |= this.RewriteIfNeeded(module, variable.VariableType, newType => variable.VariableType = newType); - } - - return rewritten - ? InstructionHandleResult.Rewritten - : InstructionHandleResult.None; - } - - /// Perform the predefined logic for an instruction if applicable. - /// The assembly module containing the instruction. - /// The CIL processor. - /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) - { - if (!this.Finder.IsMatch(instruction)) - return InstructionHandleResult.None; - bool rewritten = false; - - // field reference - FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); - if (fieldRef != null) - { - rewritten |= this.RewriteIfNeeded(module, fieldRef.DeclaringType, newType => fieldRef.DeclaringType = newType); - rewritten |= this.RewriteIfNeeded(module, fieldRef.FieldType, newType => fieldRef.FieldType = newType); - } - - // method reference - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); - if (methodRef != null) - { - rewritten |= this.RewriteIfNeeded(module, methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); - rewritten |= this.RewriteIfNeeded(module, methodRef.ReturnType, newType => methodRef.ReturnType = newType); - foreach (var parameter in methodRef.Parameters) - rewritten |= this.RewriteIfNeeded(module, parameter.ParameterType, newType => parameter.ParameterType = newType); - if (methodRef is GenericInstanceMethod genericRef) - { - for (int i = 0; i < genericRef.GenericArguments.Count; i++) - rewritten |= this.RewriteIfNeeded(module, genericRef.GenericArguments[i], newType => genericRef.GenericArguments[i] = newType); - } - } - - // type reference - if (instruction.Operand is TypeReference typeRef) - rewritten |= this.RewriteIfNeeded(module, typeRef, newType => cil.Replace(instruction, cil.Create(instruction.OpCode, newType))); - - return rewritten - ? InstructionHandleResult.Rewritten - : InstructionHandleResult.None; - } - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The type finder which matches types to rewrite. - /// A brief noun phrase indicating what the instruction finder matches. - protected BaseTypeReferenceRewriter(BaseTypeFinder finder, string nounPhrase) - : base(nounPhrase) - { - this.Finder = finder; - } - - /// Change a type reference if needed. - /// The assembly module containing the instruction. - /// The type to replace if it matches. - /// Assign the new type reference. - protected abstract bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set); - - /// Rewrite custom attributes if needed. - /// The assembly module containing the attributes. - /// The custom attributes to handle. - private bool RewriteCustomAttributesIfNeeded(ModuleDefinition module, Collection attributes) - { - bool rewritten = false; - - for (int attrIndex = 0; attrIndex < attributes.Count; attrIndex++) - { - CustomAttribute attribute = attributes[attrIndex]; - bool curChanged = false; - - // attribute type - TypeReference newAttrType = null; - if (this.Finder.IsMatch(attribute.AttributeType)) - { - rewritten |= this.RewriteIfNeeded(module, 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.RewriteIfNeeded(module, 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(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; - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs new file mode 100644 index 00000000..6aeb00ce --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -0,0 +1,260 @@ +using System; +using System.Linq; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Collections.Generic; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// Handles recursively rewriting loaded assembly code. + internal class RecursiveRewriter + { + /********* + ** Delegates + *********/ + /// Rewrite a type reference in the assembly code. + /// The current type reference. + /// Replaces the type reference with the given type. + /// Returns whether the type was changed. + public delegate bool RewriteTypeDelegate(TypeReference type, Action replaceWith); + + /// Rewrite a CIL instruction in the assembly code. + /// The current CIL instruction. + /// The CIL instruction processor. + /// Replaces the CIL instruction with the given instruction. + /// Returns whether the instruction was changed. + public delegate bool RewriteInstructionDelegate(Instruction instruction, ILProcessor cil, Action replaceWith); + + + /********* + ** Accessors + *********/ + /// The module to rewrite. + public ModuleDefinition Module { get; } + + /// Handle or rewrite a type reference if needed. + public RewriteTypeDelegate RewriteTypeImpl { get; } + + /// Handle or rewrite a CIL instruction if needed. + public RewriteInstructionDelegate RewriteInstructionImpl { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The module to rewrite. + /// Handle or rewrite a type reference if needed. + /// Handle or rewrite a CIL instruction if needed. + public RecursiveRewriter(ModuleDefinition module, RewriteTypeDelegate rewriteType, RewriteInstructionDelegate rewriteInstruction) + { + this.Module = module; + this.RewriteTypeImpl = rewriteType; + this.RewriteInstructionImpl = rewriteInstruction; + } + + /// Rewrite the loaded module code. + /// Returns whether the module was modified. + public bool RewriteModule() + { + bool anyRewritten = false; + + foreach (TypeDefinition type in this.Module.GetTypes()) + { + anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); + anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); + + foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) + { + anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); + anyRewritten |= this.RewriteGenericParameters(method.GenericParameters); + anyRewritten |= this.RewriteCustomAttributes(method.CustomAttributes); + + foreach (ParameterDefinition parameter in method.Parameters) + anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + + foreach (VariableDefinition variable in method.Body.Variables) + anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + Collection instructions = cil.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + if (instruction.OpCode.Code == Code.Nop) + continue; + + anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + { + anyRewritten = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + } + } + } + + return anyRewritten; + } + + + /********* + ** Private methods + *********/ + /// Rewrite a CIL instruction if needed. + /// The current CIL instruction. + /// The CIL instruction processor. + /// Replaces the CIL instruction with a new one. + private bool RewriteInstruction(Instruction instruction, ILProcessor cil, Action 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) + { + rewritten |= this.RewriteTypeReference(methodRef.DeclaringType, newType => methodRef.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); + } + } + + // 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(instruction, cil, newInstruction => + { + rewritten = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + + return rewritten; + } + + /// Rewrite a type reference if needed. + /// The current type reference. + /// Replaces the type reference with a new one. + private bool RewriteTypeReference(TypeReference type, Action 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; + } + + /// Rewrite custom attributes if needed. + /// The current custom attributes. + private bool RewriteCustomAttributes(Collection 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; + } + + /// Rewrites generic type parameters if needed. + /// The current generic type parameters. + private bool RewriteGenericParameters(Collection 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/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs new file mode 100644 index 00000000..91c9dec3 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -0,0 +1,199 @@ +using System; +using System.Linq; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace StardewModdingAPI.Framework.ModLoading.Framework +{ + /// Provides helper methods for field rewriters. + internal static class RewriteHelper + { + /********* + ** Fields + *********/ + /// The comparer which heuristically compares type definitions. + private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer(); + + + /********* + ** Public methods + *********/ + /// Get the field reference from an instruction if it matches. + /// The IL instruction. + public static FieldReference AsFieldReference(Instruction instruction) + { + return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld + ? (FieldReference)instruction.Operand + : null; + } + + /// Get whether the field is a reference to the expected type and field. + /// The IL instruction. + /// The full type name containing the expected field. + /// The name of the expected field. + public static bool IsFieldReferenceTo(Instruction instruction, string fullTypeName, string fieldName) + { + FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); + return RewriteHelper.IsFieldReferenceTo(fieldRef, fullTypeName, fieldName); + } + + /// Get whether the field is a reference to the expected type and field. + /// The field reference to check. + /// The full type name containing the expected field. + /// The name of the expected field. + public static bool IsFieldReferenceTo(FieldReference fieldRef, string fullTypeName, string fieldName) + { + return + fieldRef != null + && fieldRef.DeclaringType.FullName == fullTypeName + && fieldRef.Name == fieldName; + } + + /// Get the method reference from an instruction if it matches. + /// The IL instruction. + public static MethodReference AsMethodReference(Instruction instruction) + { + return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj + ? (MethodReference)instruction.Operand + : null; + } + + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + 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; + + // same generic parameters + if (type.IsGenericType) + { + if (!reference.IsGenericInstance) + return false; + + Type[] defGenerics = type.GetGenericArguments(); + 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; + } + + /// Get whether a type matches a type reference. + /// The defined type. + /// The type reference. + 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; + } + + /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. + /// The type ID to compare. + /// The other type ID to compare. + /// true if the type IDs look like the same type, false if not. + public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB) + { + return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); + } + + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below + // + + // same name + if (definition.Name != reference.Name) + return false; + + // same arguments + ParameterInfo[] definitionParameters = definition.GetParameters(); + 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; + } + + /// Get whether a method definition matches the signature expected by a method reference. + /// The method definition. + /// The method reference. + public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) + { + // + // duplicated by HasMatchingSignature(MethodInfo, 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; + } + + /// Get whether a type has a method whose signature matches the one expected by a method reference. + /// The type to check. + /// The method reference. + public static bool HasMatchingSignature(Type type, MethodReference reference) + { + return type + .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 f9d320a6..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,33 +11,32 @@ namespace StardewModdingAPI.Framework.ModLoading /********* ** Accessors *********/ - /// A brief noun phrase indicating what the handler matches. - string NounPhrase { get; } + /// A brief noun phrase indicating what the handler matches, used if is empty. + string DefaultPhrase { get; } + + /// The rewrite flags raised for the current module. + ISet Flags { get; } + + /// The brief noun phrases indicating what the handler matched for the current module. + ISet Phrases { get; } /********* ** Methods *********/ - /// Perform the predefined logic for a method if applicable. + /// Rewrite a type reference if needed. /// The assembly module containing the instruction. /// The type definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(ModuleDefinition module, TypeDefinition type, PlatformAssemblyMap assemblyMap, bool platformChanged); - - /// Perform the predefined logic for a method if applicable. - /// The assembly module containing the instruction. - /// The method definition to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(ModuleDefinition module, MethodDefinition method, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith); - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith); } } diff --git a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/RewriteHelper.cs deleted file mode 100644 index 553679f9..00000000 --- a/src/SMAPI/Framework/ModLoading/RewriteHelper.cs +++ /dev/null @@ -1,177 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using Mono.Cecil; -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.Framework.ModLoading -{ - /// Provides helper methods for field rewriters. - internal static class RewriteHelper - { - /********* - ** Fields - *********/ - /// The comparer which heuristically compares type definitions. - private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer(); - - - /********* - ** Public methods - *********/ - /// Get the field reference from an instruction if it matches. - /// The IL instruction. - public static FieldReference AsFieldReference(Instruction instruction) - { - return instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stfld || instruction.OpCode == OpCodes.Stsfld - ? (FieldReference)instruction.Operand - : null; - } - - /// Get the method reference from an instruction if it matches. - /// The IL instruction. - public static MethodReference AsMethodReference(Instruction instruction) - { - return instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt || instruction.OpCode == OpCodes.Newobj - ? (MethodReference)instruction.Operand - : null; - } - - /// Get whether a type matches a type reference. - /// The defined type. - /// The type reference. - 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; - - // same generic parameters - if (type.IsGenericType) - { - if (!reference.IsGenericInstance) - return false; - - Type[] defGenerics = type.GetGenericArguments(); - 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; - } - - /// Get whether a type matches a type reference. - /// The defined type. - /// The type reference. - 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; - } - - /// Determine whether two type IDs look like the same type, accounting for placeholder values such as !0. - /// The type ID to compare. - /// The other type ID to compare. - /// true if the type IDs look like the same type, false if not. - public static bool LooksLikeSameType(TypeReference typeA, TypeReference typeB) - { - return RewriteHelper.TypeDefinitionComparer.Equals(typeA, typeB); - } - - /// Get whether a method definition matches the signature expected by a method reference. - /// The method definition. - /// The method reference. - public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) - { - // - // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below - // - - // same name - if (definition.Name != reference.Name) - return false; - - // same arguments - ParameterInfo[] definitionParameters = definition.GetParameters(); - 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; - } - - /// Get whether a method definition matches the signature expected by a method reference. - /// The method definition. - /// The method reference. - public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) - { - // - // duplicated by HasMatchingSignature(MethodInfo, 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; - } - - /// Get whether a type has a method whose signature matches the one expected by a method reference. - /// The type to check. - /// The method reference. - public static bool HasMatchingSignature(Type type, MethodReference reference) - { - return type - .GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) - .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); - } - } -} 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 { /// Rewrites references to one field with another. - internal class FieldReplaceRewriter : FieldFinder + internal class FieldReplaceRewriter : BaseInstructionHandler { /********* ** Fields *********/ + /// The type containing the field to which references should be rewritten. + private readonly Type Type; + + /// The field name to which references should be rewritten. + private readonly string FromFieldName; + /// The new field to reference. private readonly FieldInfo ToField; @@ -20,31 +26,36 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters ** Public methods *********/ /// Construct an instance. - /// The type whose field to which references should be rewritten. + /// The type whose field to rewrite. /// The field name to rewrite. /// The new field name to reference. public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName) - : 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."); } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 { /// Rewrites field references into property references. - internal class FieldToPropertyRewriter : FieldFinder + internal class FieldToPropertyRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// The type whose field to which references should be rewritten. + /// The type containing the field to which references should be rewritten. private readonly Type Type; - /// The property name. - private readonly string PropertyName; + /// The field name to which references should be rewritten. + private readonly string FromFieldName; + + /// The new property name. + private readonly string ToPropertyName; /********* @@ -26,10 +29,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The field name to rewrite. /// The property name (if different). 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; } /// Construct an instance. @@ -38,22 +42,24 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters public FieldToPropertyRewriter(Type type, string fieldName) : this(type, fieldName, fieldName) { } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 index 9faca235..a7a0b9c3 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -1,28 +1,20 @@ using System; +using HarmonyLib; using Mono.Cecil; -using StardewModdingAPI.Framework.ModLoading.Finders; +using Mono.Cecil.Cil; using StardewModdingAPI.Framework.ModLoading.Framework; +using StardewModdingAPI.Framework.ModLoading.RewriteFacades; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites Harmony 1.x assembly references to work with Harmony 2.x. - internal class Harmony1AssemblyRewriter : BaseTypeReferenceRewriter + internal class Harmony1AssemblyRewriter : BaseInstructionHandler { /********* ** Fields *********/ - /// The full assembly name to which to find references. - private const string FromAssemblyName = "0Harmony"; - - /// The main Harmony type. - private readonly Type HarmonyType = typeof(HarmonyLib.Harmony); - - - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the rewriter matches. - public const string DefaultNounPhrase = "Harmony 1.x"; + /// Whether any Harmony 1.x types were replaced. + private bool ReplacedTypes; /********* @@ -30,41 +22,80 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters *********/ /// Construct an instance. public Harmony1AssemblyRewriter() - : base(new TypeAssemblyFinder(Harmony1AssemblyRewriter.FromAssemblyName, InstructionHandleResult.None), Harmony1AssemblyRewriter.DefaultNounPhrase) - { } + : base(defaultPhrase: "Harmony 1.x") { } + + /// Rewrite a type reference if needed. + /// The assembly module containing the instruction. + /// The type definition to handle. + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + public override bool Handle(ModuleDefinition module, TypeReference type, Action 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; + } + + /// Rewrite a CIL instruction reference if needed. + /// The assembly module containing the instruction. + /// The CIL processor. + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) + { + // rewrite Harmony 1.x methods to Harmony 2.0 + MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); + if (this.TryRewriteMethodsToFacade(module, methodRef)) + return true; + + return false; + } /********* ** Private methods *********/ - /// Change a type reference if needed. - /// The assembly module containing the instruction. - /// The type to replace if it matches. - /// Assign the new type reference. - protected override bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) + /// Rewrite methods to use Harmony facades if needed. + /// The assembly module containing the method reference. + /// The method reference to map. + private bool TryRewriteMethodsToFacade(ModuleDefinition module, MethodReference methodRef) { - bool rewritten = false; + if (!this.ReplacedTypes) + return false; // not Harmony (or already using Harmony 2.0) - // current type - if (type.Scope.Name == Harmony1AssemblyRewriter.FromAssemblyName && type.Scope is AssemblyNameReference assemblyScope && assemblyScope.Version.Major == 1) + // get facade type + Type toType; + switch (methodRef?.DeclaringType.FullName) { - Type targetType = this.GetMappedType(type); - set(module.ImportReference(targetType)); - return true; + case "HarmonyLib.Harmony": + toType = typeof(HarmonyInstanceMethods); + break; + + case "HarmonyLib.AccessTools": + toType = typeof(AccessToolsMethods); + break; + + default: + return false; } - // recurse into generic arguments - if (type is GenericInstanceType genericType) + // map if there's a matching method + if (RewriteHelper.HasMatchingSignature(toType, methodRef)) { - for (int i = 0; i < genericType.GenericArguments.Count; i++) - rewritten |= this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); + methodRef.DeclaringType = module.ImportReference(toType); + return true; } - // recurse into generic parameters (e.g. constraints) - for (int i = 0; i < type.GenericParameters.Count; i++) - rewritten |= this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); - - return rewritten; + return false; } /// Get an equivalent Harmony 2.x type. @@ -73,11 +104,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters { // main Harmony object if (type.FullName == "Harmony.HarmonyInstance") - return this.HarmonyType; + return typeof(Harmony); // other objects string fullName = type.FullName.Replace("Harmony.", "HarmonyLib."); - string targetName = this.HarmonyType.AssemblyQualifiedName.Replace(this.HarmonyType.FullName, fullName); + 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 d0fe8b13..b8e53f40 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/MethodParentRewriter.cs @@ -18,9 +18,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The type with methods to map to. private readonly Type ToType; - /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. - private readonly bool OnlyIfPlatformChanged; - /********* ** Public methods @@ -28,54 +25,49 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The type whose methods to remap. /// The type with methods to map to. - /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public MethodParentRewriter(string fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) + public MethodParentRewriter(string fromType, Type toType, string nounPhrase = null) : base(nounPhrase ?? $"{fromType.Split('.').Last()} methods") { this.FromType = fromType; this.ToType = toType; - this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } /// Construct an instance. /// The type whose methods to remap. /// The type with methods to map to. - /// Whether to only rewrite references if loading the assembly on a different platform than it was compiled on. /// A brief noun phrase indicating what the instruction finder matches (or null to generate one). - public MethodParentRewriter(Type fromType, Type toType, bool onlyIfPlatformChanged = false, string nounPhrase = null) - : this(fromType.FullName, toType, onlyIfPlatformChanged, nounPhrase) { } - + public MethodParentRewriter(Type fromType, Type toType, string nounPhrase = null) + : this(fromType.FullName, toType, nounPhrase) { } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. /// The CIL instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 *********/ /// Get whether a CIL instruction matches. - /// The IL instruction. - /// Whether the mod was compiled on a different platform. - protected bool IsMatch(Instruction instruction, bool platformChanged) + /// The method reference. + private bool IsMatch(MethodReference methodRef) { - MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); return methodRef != null - && (platformChanged || !this.OnlyIfPlatformChanged) && 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 { /// Rewrites static field references into constant values. /// The constant value type. - internal class StaticFieldToConstantRewriter : FieldFinder + internal class StaticFieldToConstantRewriter : BaseInstructionHandler { /********* ** Fields *********/ + /// The type containing the field to which references should be rewritten. + private readonly Type Type; + + /// The field name to which references should be rewritten. + private readonly string FromFieldName; + /// The constant value to replace with. private readonly TValue Value; @@ -24,24 +30,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The field name to rewrite. /// The constant value to replace with. 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; } - /// Perform the predefined logic for an instruction if applicable. + /// Rewrite a CIL instruction reference if needed. /// The assembly module containing the instruction. /// The CIL processor. - /// The instruction to handle. - /// Metadata for mapping assemblies to the current platform. - /// Whether the mod was compiled on a different platform. - public override InstructionHandleResult Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + /// The CIL instruction to handle. + /// Replaces the CIL instruction with a new one. + /// Returns whether the instruction was changed. + public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction, Action replaceWith) { - 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 d95e5ac9..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 StardewModdingAPI.Framework.ModLoading.Finders; using StardewModdingAPI.Framework.ModLoading.Framework; namespace StardewModdingAPI.Framework.ModLoading.Rewriters { /// Rewrites all references to a type. - internal class TypeReferenceRewriter : BaseTypeReferenceRewriter + internal class TypeReferenceRewriter : BaseInstructionHandler { /********* ** Fields @@ -17,6 +16,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// The new type to reference. private readonly Type ToType; + /// Get whether a matched type should be ignored. + private readonly Func ShouldIgnore; + /********* ** Public methods @@ -24,45 +26,29 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters /// Construct an instance. /// The full type name to which to find references. /// The new type to reference. - /// A lambda which overrides a matched type. + /// Get whether a matched type should be ignored. public TypeReferenceRewriter(string fromTypeFullName, Type toType, Func shouldIgnore = null) - : base(new TypeFinder(fromTypeFullName, InstructionHandleResult.None, shouldIgnore), $"{fromTypeFullName} type") + : base($"{fromTypeFullName} type") { this.FromTypeName = fromTypeFullName; this.ToType = toType; + this.ShouldIgnore = shouldIgnore; } - - /********* - ** Protected methods - *********/ - /// Change a type reference if needed. + /// Rewrite a type reference if needed. /// The assembly module containing the instruction. - /// The type to replace if it matches. - /// Assign the new type reference. - protected override bool RewriteIfNeeded(ModuleDefinition module, TypeReference type, Action set) + /// The type definition to handle. + /// Replaces the type reference with a new one. + /// Returns whether the type was changed. + public override bool Handle(ModuleDefinition module, TypeReference type, Action replaceWith) { - bool rewritten = false; - - // current type - if (type.FullName == this.FromTypeName) - { - set(module.ImportReference(this.ToType)); - return true; - } - - // recurse into generic arguments - if (type is GenericInstanceType genericType) - { - for (int i = 0; i < genericType.GenericArguments.Count; i++) - rewritten |= this.RewriteIfNeeded(module, genericType.GenericArguments[i], typeRef => genericType.GenericArguments[i] = typeRef); - } - - // recurse into generic parameters (e.g. constraints) - for (int i = 0; i < type.GenericParameters.Count; i++) - rewritten |= this.RewriteIfNeeded(module, type.GenericParameters[i], typeRef => type.GenericParameters[i] = new GenericParameter(typeRef)); + // check type reference + if (type.FullName != this.FromTypeName || this.ShouldIgnore?.Invoke(type) == true) + return false; - return rewritten; + // rewrite to new type + replaceWith(module.ImportReference(this.ToType)); + return this.MarkRewritten(); } } } diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index d80f64e2..b7aad9da 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -25,21 +25,21 @@ namespace StardewModdingAPI.Metadata *********/ /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. /// Whether to detect paranoid mode issues. - public IEnumerable GetHandlers(bool paranoidMode) + /// Whether the assembly was rewritten for crossplatform compatibility. + public IEnumerable GetHandlers(bool paranoidMode, bool platformChanged) { /**** ** rewrite CIL to fix incompatible code ****/ // rewrite for crossplatform compatibility - yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods), onlyIfPlatformChanged: true); + if (platformChanged) + yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods)); // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); - yield return new MethodParentRewriter(typeof(HarmonyLib.Harmony), typeof(HarmonyInstanceMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); - yield return new MethodParentRewriter(typeof(HarmonyLib.AccessTools), typeof(AccessToolsMethods), onlyIfPlatformChanged: false, nounPhrase: Harmony1AssemblyRewriter.DefaultNounPhrase); /**** ** detect mod issues -- cgit From b54d892abf2bb5b7c05631d6f665b2a1d06529b1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 19 May 2020 22:50:33 -0400 Subject: fix rewriting declaring type for a generic method (#711) --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 6aeb00ce..4c707248 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -122,7 +122,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null) { - rewritten |= this.RewriteTypeReference(methodRef.DeclaringType, newType => methodRef.DeclaringType = newType); + 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) -- cgit From 71a11337d6e51a1d95d3a40634f2ce319c330c33 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 May 2020 00:53:46 -0400 Subject: ignore special types (#711) --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 3 +++ 1 file changed, 3 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 4c707248..5f7c2128 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework foreach (TypeDefinition type in this.Module.GetTypes()) { + if (type.BaseType == null) + continue; // special type like + anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); -- cgit From 136a548fbbb27e28ac7b8167760388eb5754bdb9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 May 2020 00:54:28 -0400 Subject: rewrite methods without a body (#711) --- .../ModLoading/Framework/RecursiveRewriter.cs | 35 ++++++++++++---------- 1 file changed, 19 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 5f7c2128..612d0fdd 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -67,7 +67,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); - foreach (MethodDefinition method in type.Methods.Where(p => p.HasBody)) + foreach (MethodDefinition method in type.Methods) { anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); anyRewritten |= this.RewriteGenericParameters(method.GenericParameters); @@ -76,24 +76,27 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework foreach (ParameterDefinition parameter in method.Parameters) anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); - foreach (VariableDefinition variable in method.Body.Variables) - anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - Collection instructions = cil.Body.Instructions; - for (int i = 0; i < instructions.Count; i++) + if (method.HasBody) { - var instruction = instructions[i]; - if (instruction.OpCode.Code == Code.Nop) - continue; + foreach (VariableDefinition variable in method.Body.Variables) + anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); - anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + Collection instructions = cil.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) { - anyRewritten = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); + var instruction = instructions[i]; + if (instruction.OpCode.Code == Code.Nop) + continue; + + anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + { + anyRewritten = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + } } } } -- cgit From b38b7af05496e9a844ce8f63a05d23bd6c39430f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 May 2020 00:56:28 -0400 Subject: rewrite base types & interfaces (#711) --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 612d0fdd..3b8cda88 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -67,6 +67,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); + foreach (InterfaceImplementation @interface in type.Interfaces) + anyRewritten |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); + + if (type.BaseType.FullName != "System.Object") + anyRewritten |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); + foreach (MethodDefinition method in type.Methods) { anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); -- cgit From 518bf7e3f13f10d2ef6ea4f064ecd8d58bf07c49 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 May 2020 02:00:16 -0400 Subject: rewrite renamed 'prioritiy' field (#711) --- .../Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index a7a0b9c3..be98a666 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -57,6 +57,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters 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; } -- cgit From c5c30189e43f93c3f3c66207945187a974656c9e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 20 May 2020 02:14:30 -0400 Subject: fix error-handling when patch is called with a null target method (#711) --- .../Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs index 78cf25f8..17b6bcd9 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs @@ -34,6 +34,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } catch (Exception ex) { + // get patch types var patchTypes = new List(); if (prefix != null) patchTypes.Add("prefix"); @@ -42,7 +43,12 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades if (transpiler != null) patchTypes.Add("transpiler"); - throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to method {original.DeclaringType?.FullName}.{original.Name}.", ex); + // get original method label + string methodLabel = original != null + ? $"method {original.DeclaringType?.FullName}.{original.Name}" + : "null method"; + + throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to {methodLabel}.", ex); } } } -- cgit From 1beee07a3548f4f7efe9d4044ec7a3eb78987f89 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 20:32:02 -0400 Subject: rewrite method overrides (#711) --- .../ModLoading/Framework/RecursiveRewriter.cs | 50 ++++++++++++++-------- 1 file changed, 31 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 3b8cda88..a0f075bd 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -82,6 +82,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework foreach (ParameterDefinition parameter in method.Parameters) anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + foreach (var methodOverride in method.Overrides) + anyRewritten |= this.RewriteMethodReference(methodOverride); + if (method.HasBody) { foreach (VariableDefinition variable in method.Body.Variables) @@ -133,25 +136,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // method reference MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); if (methodRef != null) - { - 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); - } - } + this.RewriteMethodReference(methodRef); // type reference if (instruction.Operand is TypeReference typeRef) @@ -169,6 +154,33 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework return rewritten; } + /// Rewrite a method reference if needed. + /// The current method reference. + 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; + } + /// Rewrite a type reference if needed. /// The current type reference. /// Replaces the type reference with a new one. -- cgit From f8e0600672952fa211b118df27f359581ee4b1f1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 21:59:45 -0400 Subject: load .pdb file when mod is loaded from bytes (#711) --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index e133a45c..f95a6192 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Fixed rewriting generic types to method references. * Simplified paranoid warnings in the log and reduced their log level. * Fixed asset propagation for Gil's portraits. + * Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. * For SMAPI developers: * When deploying web services to a single-instance app, the MongoDB server can now be replaced with in-memory storage. diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 5218938f..eadb2997 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -127,10 +127,21 @@ namespace StardewModdingAPI.Framework.ModLoading { if (!oneAssembly) this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace); + + // load PDB file if present + byte[] symbols; + { + 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); + lastAssembly = Assembly.Load(bytes, symbols); } else { -- cgit From 7fdc3a2ab2361145693cfbf0957ecdb7564ffaa1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 22:21:24 -0400 Subject: fix AccessTools facade constructor logic (#711) --- .../ModLoading/RewriteFacades/AccessToolsMethods.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs index ea35fec9..08857129 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs @@ -16,17 +16,26 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades *********/ public static ConstructorInfo DeclaredConstructor(Type type, Type[] parameters = null) { - return AccessTools.DeclaredConstructor(type, parameters, searchForStatic: true); + // 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) { - return AccessTools.Constructor(type, parameters, searchForStatic: true); + // 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 GetDeclaredConstructors(Type type) { - return AccessTools.GetDeclaredConstructors(type, searchForStatic: true); + // Harmony 1.x matched both static and instance constructors + return + AccessTools.GetDeclaredConstructors(type, searchForStatic: false) + ?? AccessTools.GetDeclaredConstructors(type, searchForStatic: true); } } } -- cgit From 4468f390985e4cdff330147b4e6c6089aedfb48c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 22:25:09 -0400 Subject: improve facade annotations --- .../Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs | 1 + .../ModLoading/RewriteFacades/HarmonyInstanceMethods.cs | 1 + .../Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs | 10 +++------- 3 files changed, 5 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs index 08857129..2f8eb5c4 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs @@ -8,6 +8,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades { /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] public class AccessToolsMethods { diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs index 17b6bcd9..68794f41 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs @@ -9,6 +9,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades { /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] public class HarmonyInstanceMethods : Harmony { diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs index 75bb61ef..ba26b827 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs @@ -2,11 +2,13 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; -#pragma warning disable 1591 // missing documentation namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades { /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [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 SpriteBatchMethods : SpriteBatch { /********* @@ -19,7 +21,6 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades /**** ** 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.ModLoading.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); -- cgit From f52370f6fa1fb3ab82a5c741fea2e8e5aee60223 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 22:29:42 -0400 Subject: rename facade classes --- .../ModLoading/RewriteFacades/AccessToolsFacade.cs | 42 ++++++++++++++++ .../RewriteFacades/AccessToolsMethods.cs | 42 ---------------- .../RewriteFacades/HarmonyInstanceFacade.cs | 56 +++++++++++++++++++++ .../RewriteFacades/HarmonyInstanceMethods.cs | 56 --------------------- .../ModLoading/RewriteFacades/SpriteBatchFacade.cs | 57 ++++++++++++++++++++++ .../RewriteFacades/SpriteBatchMethods.cs | 57 ---------------------- .../Rewriters/Harmony1AssemblyRewriter.cs | 4 +- src/SMAPI/Metadata/InstructionMetadata.cs | 2 +- 8 files changed, 158 insertions(+), 158 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs delete mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs delete mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs delete mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs (limited to 'src') 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 +{ + /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [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 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/AccessToolsMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs deleted file mode 100644 index 2f8eb5c4..00000000 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsMethods.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using HarmonyLib; - -namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades -{ - /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] - [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] - public class AccessToolsMethods - { - /********* - ** 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 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..fa340781 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [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 + *********/ + /// Construct an instance. + /// The unique patch identifier. + 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) + { + try + { + MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); + return (DynamicMethod)method; + } + catch (Exception ex) + { + // get patch types + var patchTypes = new List(); + if (prefix != null) + patchTypes.Add("prefix"); + if (postfix != null) + patchTypes.Add("postfix"); + if (transpiler != null) + patchTypes.Add("transpiler"); + + // get original method label + string methodLabel = original != null + ? $"method {original.DeclaringType?.FullName}.{original.Name}" + : "null method"; + + throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to {methodLabel}.", ex); + } + } + } +} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs deleted file mode 100644 index 68794f41..00000000 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceMethods.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; - -namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades -{ - /// Maps Harmony 1.x HarmonyInstance methods to Harmony 2.x's to avoid breaking older mods. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")] - [SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")] - public class HarmonyInstanceMethods : Harmony - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique patch identifier. - public HarmonyInstanceMethods(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) - { - try - { - MethodInfo method = base.Patch(original: original, prefix: prefix, postfix: postfix, transpiler: transpiler); - return (DynamicMethod)method; - } - catch (Exception ex) - { - // get patch types - var patchTypes = new List(); - if (prefix != null) - patchTypes.Add("prefix"); - if (postfix != null) - patchTypes.Add("postfix"); - if (transpiler != null) - patchTypes.Add("transpiler"); - - // get original method label - string methodLabel = original != null - ? $"method {original.DeclaringType?.FullName}.{original.Name}" - : "null method"; - - throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to {methodLabel}.", ex); - } - } - } -} diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs new file mode 100644 index 00000000..cf71af77 --- /dev/null +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades +{ + /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [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 + *********/ + /// Construct an instance. + public SpriteBatchFacade(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } + + + /**** + ** MonoGame signatures + ****/ + 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); + } + + /**** + ** XNA signatures + ****/ + public new void Begin() + { + base.Begin(); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState) + { + base.Begin(sortMode, blendState); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); + } + + public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) + { + base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); + } + + 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/RewriteFacades/SpriteBatchMethods.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs deleted file mode 100644 index ba26b827..00000000 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchMethods.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Graphics; - -namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades -{ - /// Provides method signatures that can be injected into mod code for compatibility between Linux/Mac or Windows. - /// This is public to support SMAPI rewriting and should not be referenced directly by mods. - [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 SpriteBatchMethods : SpriteBatch - { - /********* - ** Public methods - *********/ - /// Construct an instance. - public SpriteBatchMethods(GraphicsDevice graphicsDevice) : base(graphicsDevice) { } - - - /**** - ** MonoGame signatures - ****/ - 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); - } - - /**** - ** XNA signatures - ****/ - public new void Begin() - { - base.Begin(); - } - - public new void Begin(SpriteSortMode sortMode, BlendState blendState) - { - base.Begin(sortMode, blendState); - } - - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState); - } - - public new void Begin(SpriteSortMode sortMode, BlendState blendState, SamplerState samplerState, DepthStencilState depthStencilState, RasterizerState rasterizerState, Effect effect) - { - base.Begin(sortMode, blendState, samplerState, depthStencilState, rasterizerState, effect); - } - - 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/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index be98a666..ce6417a8 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -85,11 +85,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters switch (methodRef?.DeclaringType.FullName) { case "HarmonyLib.Harmony": - toType = typeof(HarmonyInstanceMethods); + toType = typeof(HarmonyInstanceFacade); break; case "HarmonyLib.AccessTools": - toType = typeof(AccessToolsMethods); + toType = typeof(AccessToolsFacade); break; default: diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index b7aad9da..89430a11 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Metadata ****/ // rewrite for crossplatform compatibility if (platformChanged) - yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchMethods)); + yield return new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchFacade)); // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); -- cgit From db0a46cb688ac47a06067b07dfe30bc2b65ec369 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 23:29:23 -0400 Subject: rewrite HarmonyMethod to allow null (#711) --- .../ModLoading/Framework/RewriteHelper.cs | 11 +++++- .../RewriteFacades/HarmonyMethodFacade.cs | 45 ++++++++++++++++++++++ .../Rewriters/Harmony1AssemblyRewriter.cs | 4 ++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs index 91c9dec3..36058b86 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs @@ -137,7 +137,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Get whether a method definition matches the signature expected by a method reference. /// The method definition. /// The method reference. - public static bool HasMatchingSignature(MethodInfo definition, MethodReference reference) + public static bool HasMatchingSignature(MethodBase definition, MethodReference reference) { // // duplicated by HasMatchingSignature(MethodDefinition, MethodReference) below @@ -166,7 +166,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework public static bool HasMatchingSignature(MethodDefinition definition, MethodReference reference) { // - // duplicated by HasMatchingSignature(MethodInfo, MethodReference) above + // duplicated by HasMatchingSignature(MethodBase, MethodReference) above // // same name @@ -191,6 +191,13 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The method reference. 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.Static | BindingFlags.DeclaredOnly | BindingFlags.Public) .Any(method => RewriteHelper.HasMatchingSignature(method, reference)); 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 +{ + /// Maps Harmony 1.x methods to Harmony 2.x to avoid breaking older mods. + /// This is public to support SMAPI rewriting and should not be referenced directly by mods. + [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 + *********/ + /// Import a method directly using the internal HarmonyMethod code. + /// The method to import. + 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/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index ce6417a8..8fed170a 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -92,6 +92,10 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters toType = typeof(AccessToolsFacade); break; + case "HarmonyLib.HarmonyMethod": + toType = typeof(HarmonyMethodFacade); + break; + default: return false; } -- cgit From 33da29b3e56a56c29ed6f36196d00881fa2aecfe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 21 May 2020 23:50:34 -0400 Subject: rewrite Harmony.Patch method to allow non-implemented virtual methods (#711) --- .../RewriteFacades/HarmonyInstanceFacade.cs | 56 ++++++++++++++++------ 1 file changed, 41 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs index fa340781..54b91679 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs @@ -28,6 +28,13 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades 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); @@ -35,22 +42,41 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } catch (Exception ex) { - // get patch types - var patchTypes = new List(); - if (prefix != null) - patchTypes.Add("prefix"); - if (postfix != null) - patchTypes.Add("postfix"); - if (transpiler != null) - patchTypes.Add("transpiler"); - - // get original method label - string methodLabel = original != null - ? $"method {original.DeclaringType?.FullName}.{original.Name}" - : "null method"; - - throw new Exception($"Harmony instance {this.Id} failed applying {string.Join("/", patchTypes)} to {methodLabel}.", 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 + *********/ + /// Get a human-readable label for the patch types being applies. + /// The prefix method, if any. + /// The postfix method, if any. + /// The transpiler method, if any. + private string GetPatchTypesLabel(HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null) + { + var patchTypes = new List(); + + if (prefix != null) + patchTypes.Add("prefix"); + if (postfix != null) + patchTypes.Add("postfix"); + if (transpiler != null) + patchTypes.Add("transpiler"); + + return string.Join("/", patchTypes); + } + + /// Get a human-readable label for the method being patched. + /// The method being patched. + private string GetMethodLabel(MethodBase method) + { + return method != null + ? $"method {method.DeclaringType?.FullName}.{method.Name}" + : "null method"; + } } } -- cgit From 163eebd92e21075698986a843850f0850514e778 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 May 2020 19:57:22 -0400 Subject: move internal commands out of SCore --- src/SMAPI/Framework/CommandManager.cs | 13 ++++- src/SMAPI/Framework/Commands/HelpCommand.cs | 64 +++++++++++++++++++++++ src/SMAPI/Framework/Commands/IInternalCommand.cs | 24 +++++++++ src/SMAPI/Framework/Commands/ReloadI18nCommand.cs | 44 ++++++++++++++++ src/SMAPI/Framework/SCore.cs | 54 ++++--------------- 5 files changed, 154 insertions(+), 45 deletions(-) create mode 100644 src/SMAPI/Framework/Commands/HelpCommand.cs create mode 100644 src/SMAPI/Framework/Commands/IInternalCommand.cs create mode 100644 src/SMAPI/Framework/Commands/ReloadI18nCommand.cs (limited to 'src') diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index ceeb6f93..eaa91c86 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using StardewModdingAPI.Framework.Commands; namespace StardewModdingAPI.Framework { @@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework /// The or is null or empty. /// The is not a valid format. /// There's already a command with that name. - public void Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) + public CommandManager Add(IModMetadata mod, string name, string documentation, Action callback, bool allowNullCallback = false) { name = this.GetNormalizedName(name); @@ -45,6 +46,16 @@ namespace StardewModdingAPI.Framework // add command this.Commands.Add(name, new Command(mod, name, documentation, callback)); + return this; + } + + /// Add a console command. + /// the SMAPI console command to add. + /// Writes messages to the console. + /// There's already a command with that name. + public CommandManager Add(IInternalCommand command, IMonitor monitor) + { + return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor)); } /// Get a command by its unique name. diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs new file mode 100644 index 00000000..b8730a00 --- /dev/null +++ b/src/SMAPI/Framework/Commands/HelpCommand.cs @@ -0,0 +1,64 @@ +using System.Linq; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'help' SMAPI console command. + internal class HelpCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// Manages console commands. + private readonly CommandManager CommandManager; + + + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "help"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display."; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Manages console commands. + public HelpCommand(CommandManager commandManager) + { + this.CommandManager = commandManager; + } + + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + if (args.Any()) + { + Command result = this.CommandManager.Get(args[0]); + if (result == null) + monitor.Log("There's no command with that name.", LogLevel.Error); + else + monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); + } + else + { + string message = "The following commands are registered:\n"; + IGrouping[] groups = (from command in this.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); + foreach (var group in groups) + { + string modName = group.Key ?? "SMAPI"; + string[] commandNames = group.ToArray(); + message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; + } + message += "For more information about a command, type 'help command_name'."; + + monitor.Log(message, LogLevel.Info); + } + } + } +} diff --git a/src/SMAPI/Framework/Commands/IInternalCommand.cs b/src/SMAPI/Framework/Commands/IInternalCommand.cs new file mode 100644 index 00000000..abf105b6 --- /dev/null +++ b/src/SMAPI/Framework/Commands/IInternalCommand.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Framework.Commands +{ + /// A core SMAPI console command. + interface IInternalCommand + { + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + string Name { get; } + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + string Description { get; } + + + /********* + ** Methods + *********/ + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + void HandleCommand(string[] args, IMonitor monitor); + } +} diff --git a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs new file mode 100644 index 00000000..12328bb6 --- /dev/null +++ b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs @@ -0,0 +1,44 @@ +using System; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'reload_i18n' SMAPI console command. + internal class ReloadI18nCommand : IInternalCommand + { + /********* + ** Fields + *********/ + /// Reload translations for all mods. + private readonly Action ReloadTranslations; + + + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "reload_i18n"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Reloads translation files for all mods.\n\nUsage: reload_i18n"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Reload translations for all mods.. + public ReloadI18nCommand(Action reloadTranslations) + { + this.ReloadTranslations = reloadTranslations; + } + + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + this.ReloadTranslations(); + monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index cd292bfc..a89616a3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -16,6 +16,7 @@ using System.Windows.Forms; #endif using Newtonsoft.Json; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.Commands; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Logging; @@ -508,8 +509,9 @@ namespace StardewModdingAPI.Framework { // prepare console this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + this.GameInstance.CommandManager + .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) + .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input Thread inputThread = new Thread(() => @@ -1273,6 +1275,12 @@ namespace StardewModdingAPI.Framework } /// Reload translations for all mods. + private void ReloadTranslations() + { + this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); + } + + /// Reload translations for the given mods. /// The mods for which to reload translations. private void ReloadTranslations(IEnumerable mods) { @@ -1357,48 +1365,6 @@ namespace StardewModdingAPI.Framework return translations; } - /// The method called when the user submits a core SMAPI command in the console. - /// The command name. - /// The command arguments. - private void HandleCommand(string name, string[] arguments) - { - switch (name) - { - case "help": - if (arguments.Any()) - { - Command result = this.GameInstance.CommandManager.Get(arguments[0]); - if (result == null) - this.Monitor.Log("There's no command with that name.", LogLevel.Error); - else - this.Monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info); - } - else - { - string message = "The following commands are registered:\n"; - IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray(); - foreach (var group in groups) - { - string modName = group.Key ?? "SMAPI"; - string[] commandNames = group.ToArray(); - message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n"; - } - message += "For more information about a command, type 'help command_name'."; - - this.Monitor.Log(message, LogLevel.Info); - } - break; - - case "reload_i18n": - this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); - this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); - break; - - default: - throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); - } - } - /// Redirect messages logged directly to the console to the given monitor. /// The monitor with which to log messages as the game. /// The message to log. -- cgit From b074eb279a649030e71ed2192bb6a528a9801fce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 May 2020 20:00:33 -0400 Subject: add harmony_summary command --- docs/release-notes.md | 3 +- .../Framework/Commands/HarmonySummaryCommand.cs | 167 +++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 1 + 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index f95a6192..4596a525 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,8 +11,9 @@ * Internal changes to improve performance and reliability. * For modders: - * Added `Multiplayer.PeerConnected` event. * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added `Multiplayer.PeerConnected` event. + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs new file mode 100644 index 00000000..bc8f4aa2 --- /dev/null +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using HarmonyLib; + +namespace StardewModdingAPI.Framework.Commands +{ + /// The 'harmony_summary' SMAPI console command. + internal class HarmonySummaryCommand : IInternalCommand + { + /********* + ** Accessors + *********/ + /// The command name, which the user must type to trigger it. + public string Name { get; } = "harmony_summary"; + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary \n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method."; + + + /********* + ** Public methods + *********/ + /// Handle the console command when it's entered by the user. + /// The command arguments. + /// Writes messages to the console. + public void HandleCommand(string[] args, IMonitor monitor) + { + SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.Method).ToArray(); + + StringBuilder result = new StringBuilder(); + + if (!matches.Any()) + result.AppendLine("No current patches match your search."); + else + { + result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:"); + result.AppendLine(); + foreach (var match in matches) + { + result.AppendLine($" {match.Method}"); + foreach (var ownerGroup in match.PatchTypesByOwner) + { + var sortedTypes = ownerGroup.Value + .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); + + result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); + } + } + } + + monitor.Log(result.ToString(), LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// Get all current Harmony patches matching any of the given search terms. + /// The search terms to match. + private IEnumerable FilterPatches(string[] searchTerms) + { + bool hasSearch = searchTerms.Any(); + bool IsMatch(string target) => searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); + + foreach (var patch in this.GetAllPatches()) + { + if (!hasSearch) + yield return patch; + + // matches entire patch + if (IsMatch(patch.Method)) + { + yield return patch; + continue; + } + + // matches individual patchers + foreach (var pair in patch.PatchTypesByOwner.ToArray()) + { + if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString()))) + patch.PatchTypesByOwner.Remove(pair.Key); + } + + if (patch.PatchTypesByOwner.Any()) + yield return patch; + } + } + + /// Get all current Harmony patches. + private IEnumerable GetAllPatches() + { + foreach (MethodBase method in Harmony.GetAllPatchedMethods()) + { + // get metadata for method + string methodLabel = method.FullDescription(); + HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); + IDictionary> patchGroups = new Dictionary> + { + [PatchType.Prefix] = patchInfo.Prefixes, + [PatchType.Postfix] = patchInfo.Postfixes, + [PatchType.Finalizer] = patchInfo.Finalizers, + [PatchType.Transpiler] = patchInfo.Transpilers + }; + + // get patch types by owner + var typesByOwner = new Dictionary>(); + foreach (var group in patchGroups) + { + foreach (var patch in group.Value) + { + if (!typesByOwner.TryGetValue(patch.owner, out ISet patchTypes)) + typesByOwner[patch.owner] = patchTypes = new HashSet(); + patchTypes.Add(group.Key); + } + } + + // create search result + yield return new SearchResult(methodLabel, typesByOwner); + } + } + + /// A Harmony patch type. + private enum PatchType + { + /// A prefix patch. + Prefix, + + /// A postfix patch. + Postfix, + + /// A finalizer patch. + Finalizer, + + /// A transpiler patch. + Transpiler + } + + /// A patch search result for a method. + private class SearchResult + { + /********* + ** Accessors + *********/ + /// A detailed human-readable label for the patched method. + public string Method { get; } + + /// The patch types by the Harmony instance ID that added them. + public IDictionary> PatchTypesByOwner { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A detailed human-readable label for the patched method. + /// The patch types by the Harmony instance ID that added them. + public SearchResult(string method, IDictionary> patchTypesByOwner) + { + this.Method = method; + this.PatchTypesByOwner = patchTypesByOwner; + } + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a89616a3..9d96bad1 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,6 +511,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) + .Add(new HarmonySummaryCommand(), this.Monitor) .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input -- cgit From 9aba50451b617e1af8215358afda22c8105477f2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 22 May 2020 23:40:22 -0400 Subject: keep verb when redirecting api.smapi.io --- src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs index 16397c1e..d9d44641 100644 --- a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs +++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Net; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Rewrite; @@ -22,6 +23,7 @@ namespace StardewModdingAPI.Web.Framework.RedirectRules /// Regex patterns matching the current URL mapped to the resulting redirect URL. public RedirectPathsToUrlsRule(IDictionary map) { + this.StatusCode = HttpStatusCode.RedirectKeepVerb; this.Map = map.ToDictionary( p => new Regex(p.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled), p => p.Value -- cgit From d7add894419543667e60569bfeb439e8e797a4d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 May 2020 19:25:34 -0400 Subject: drop MongoDB code MongoDB support unnecessarily complicated the code and there's no need to run distributed servers in the foreseeable future. This keeps the abstract storage interface so we can wrap a distributed cache in the future. --- docs/release-notes.md | 2 +- docs/technical/web.md | 22 +- src/SMAPI.Web/BackgroundService.cs | 2 +- src/SMAPI.Web/Controllers/ModsApiController.cs | 26 ++- src/SMAPI.Web/Controllers/ModsController.cs | 9 +- src/SMAPI.Web/Framework/Caching/Cached.cs | 37 ++++ src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs | 107 ---------- .../Framework/Caching/Mods/IModCacheRepository.cs | 5 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 30 +-- .../Caching/Mods/ModCacheMongoRepository.cs | 105 ---------- .../Caching/UtcDateTimeOffsetSerializer.cs | 40 ---- .../Framework/Caching/Wiki/CachedWikiMetadata.cs | 43 ---- .../Framework/Caching/Wiki/CachedWikiMod.cs | 230 --------------------- .../Framework/Caching/Wiki/IWikiCacheRepository.cs | 9 +- .../Caching/Wiki/WikiCacheMemoryRepository.cs | 25 ++- .../Caching/Wiki/WikiCacheMongoRepository.cs | 73 ------- .../Framework/Caching/Wiki/WikiMetadata.cs | 31 +++ .../Framework/ConfigModels/StorageConfig.cs | 18 -- .../Framework/ConfigModels/StorageMode.cs | 15 -- src/SMAPI.Web/SMAPI.Web.csproj | 3 - src/SMAPI.Web/Startup.cs | 78 +------ src/SMAPI.Web/appsettings.Development.json | 6 - 22 files changed, 123 insertions(+), 793 deletions(-) create mode 100644 src/SMAPI.Web/Framework/Caching/Cached.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs delete mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs create mode 100644 src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs delete mode 100644 src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs delete mode 100644 src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4596a525..f3f2efa4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,7 +23,7 @@ * Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. * For SMAPI developers: - * When deploying web services to a single-instance app, the MongoDB server can now be replaced with in-memory storage. + * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. * Merged the separate legacy redirects app on AWS into the main app on Azure. ## 3.5 diff --git a/docs/technical/web.md b/docs/technical/web.md index ef591aee..d21b87ac 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -352,7 +352,6 @@ your machine, with no external dependencies aside from the actual mod sites. --------------------------- | ----------- `AzureBlobConnectionString` | The connection string for the Azure Blob storage account. Defaults to using the system's temporary file folder if not specified. `GitHubUsername`
`GitHubPassword` | The GitHub credentials with which to query GitHub release info. Defaults to anonymous requests if not specified. - `Storage` | How to storage cached wiki/mod data. `InMemory` is recommended in most cases, or `MongoInMemory` to test the MongoDB storage code. See [production environment](#production-environment) for more info on `Mongo`. 2. Launch `SMAPI.Web` from Visual Studio to run a local version of the site. @@ -385,23 +384,4 @@ Initial setup: `Site:BetaBlurb` | If `Site:BetaEnabled` is true and there's a beta version of SMAPI in its GitHub releases, this is shown on the beta download button as explanatory subtext. `Site:SupporterList` | A list of Patreon supports to credit on the download page. -To enable distributed servers: - -1. Launch an empty MongoDB server (e.g. using [MongoDB Atlas](https://www.mongodb.com/cloud/atlas)) - for mod data. -2. Add these application settings in the App Services environment: - - property name | description - ------------------------------- | ----------------- - `Storage:Mode` | Set to `Mongo`. - `Storage:ConnectionString` | Set to the connection string for the MongoDB instance. - - Optional settings: - - property name | description - ------------------------------- | ----------------- - `Storage:Database` | Set to the MongoDB database name (defaults to `smapi`). - -To deploy updates: -1. [Deploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure). -2. If the MongoDB schema changed, delete the MongoDB database. (It'll be recreated automatically.) +To deploy updates, just [redeploy the web project from Visual Studio](https://docs.microsoft.com/en-us/visualstudio/deployment/quickstart-deploy-to-azure). diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 275622fe..64bd5ca5 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -84,7 +84,7 @@ namespace StardewModdingAPI.Web public static async Task UpdateWikiAsync() { WikiModList wikiCompatList = await new ModToolkit().GetWikiCompatibilityListAsync(); - BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods, out _, out _); + BackgroundService.WikiCache.SaveWikiData(wikiCompatList.StableVersion, wikiCompatList.BetaVersion, wikiCompatList.Mods); } /// Remove mods which haven't been requested in over 48 hours. diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 6032186f..b9d7c32d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -12,6 +12,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework; +using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; @@ -90,7 +91,7 @@ namespace StardewModdingAPI.Web.Controllers return new ModEntryModel[0]; // fetch wiki data - WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); + WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (ModSearchEntryModel mod in model.Mods) { @@ -283,27 +284,30 @@ namespace StardewModdingAPI.Web.Controllers /// Whether to allow non-standard versions. private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions) { - // get mod - if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes)) + // get from cache + if (this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out Cached cachedMod) && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes)) + return cachedMod.Data; + + // fetch from mod site { // get site if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository)) return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); // fetch mod - ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID); - if (result.Error == null) + ModInfoModel mod = await repository.GetModInfoAsync(updateKey.ID); + if (mod.Error == null) { - if (result.Version == null) - result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); - else if (!SemanticVersion.TryParse(result.Version, allowNonStandardVersions, out _)) - result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); + if (mod.Version == null) + mod.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); + else if (!SemanticVersion.TryParse(mod.Version, allowNonStandardVersions, out _)) + mod.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{mod.Version}'."); } // cache mod - this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod); + this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, mod); + return mod; } - return mod.GetModel(); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs index b621ded0..24e36709 100644 --- a/src/SMAPI.Web/Controllers/ModsController.cs +++ b/src/SMAPI.Web/Controllers/ModsController.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels; @@ -51,16 +52,16 @@ namespace StardewModdingAPI.Web.Controllers public ModListModel FetchData() { // fetch cached data - if (!this.Cache.TryGetWikiMetadata(out CachedWikiMetadata metadata)) + if (!this.Cache.TryGetWikiMetadata(out Cached metadata)) return new ModListModel(); // build model return new ModListModel( - stableVersion: metadata.StableVersion, - betaVersion: metadata.BetaVersion, + stableVersion: metadata.Data.StableVersion, + betaVersion: metadata.Data.BetaVersion, mods: this.Cache .GetWikiMods() - .Select(mod => new ModModel(mod.GetModel())) + .Select(mod => new ModModel(mod.Data)) .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting lastUpdated: metadata.LastUpdated, isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes) diff --git a/src/SMAPI.Web/Framework/Caching/Cached.cs b/src/SMAPI.Web/Framework/Caching/Cached.cs new file mode 100644 index 00000000..52041a16 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Cached.cs @@ -0,0 +1,37 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Caching +{ + /// A cache entry. + /// The cached value type. + internal class Cached + { + /********* + ** Accessors + *********/ + /// The cached data. + public T Data { get; set; } + + /// When the data was last updated. + public DateTimeOffset LastUpdated { get; set; } + + /// When the data was last requested through the mod API. + public DateTimeOffset LastRequested { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public Cached() { } + + /// Construct an instance. + /// The cached data. + public Cached(T data) + { + this.Data = data; + this.LastUpdated = DateTimeOffset.UtcNow; + this.LastRequested = DateTimeOffset.UtcNow; + } + } +} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs b/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs deleted file mode 100644 index 96eca847..00000000 --- a/src/SMAPI.Web/Framework/Caching/Mods/CachedMod.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Framework.Caching.Mods -{ - /// The model for cached mod data. - internal class CachedMod - { - /********* - ** Accessors - *********/ - /**** - ** Tracking - ****/ - /// The internal MongoDB ID. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] - [BsonIgnoreIfDefault] - public ObjectId _id { get; set; } - - /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } - - /// When the data was last requested through the web API. - public DateTimeOffset LastRequested { get; set; } - - /**** - ** Metadata - ****/ - /// The mod site on which the mod is found. - public ModRepositoryKey Site { get; set; } - - /// The mod's unique ID within the . - public string ID { get; set; } - - /// The mod availability status on the remote site. - public RemoteModStatus FetchStatus { get; set; } - - /// The error message providing more info for the , if applicable. - public string FetchError { get; set; } - - - /**** - ** Mod info - ****/ - /// The mod's display name. - public string Name { get; set; } - - /// The mod's latest version. - public string MainVersion { get; set; } - - /// The mod's latest optional or prerelease version, if newer than . - public string PreviewVersion { get; set; } - - /// The URL for the mod page. - public string Url { get; set; } - - /// The license URL, if available. - public string LicenseUrl { get; set; } - - /// The license name, if available. - public string LicenseName { get; set; } - - - /********* - ** Accessors - *********/ - /// Construct an instance. - public CachedMod() { } - - /// Construct an instance. - /// The mod site on which the mod is found. - /// The mod's unique ID within the . - /// The mod data. - public CachedMod(ModRepositoryKey site, string id, ModInfoModel mod) - { - // tracking - this.LastUpdated = DateTimeOffset.UtcNow; - this.LastRequested = DateTimeOffset.UtcNow; - - // metadata - this.Site = site; - this.ID = id; - this.FetchStatus = mod.Status; - this.FetchError = mod.Error; - - // mod info - this.Name = mod.Name; - this.MainVersion = mod.Version; - this.PreviewVersion = mod.PreviewVersion; - this.Url = mod.Url; - this.LicenseUrl = mod.LicenseUrl; - this.LicenseName = mod.LicenseName; - } - - /// Get the API model for the cached data. - public ModInfoModel GetModel() - { - return new ModInfoModel(name: this.Name, version: this.MainVersion, url: this.Url, previewVersion: this.PreviewVersion) - .SetLicense(this.LicenseUrl, this.LicenseName) - .SetError(this.FetchStatus, this.FetchError); - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 08749f3b..004202f9 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -15,14 +15,13 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true); + bool TryGetMod(ModRepositoryKey site, string id, out Cached mod, bool markRequested = true); /// Save data fetched for a mod. /// The mod site on which the mod is found. /// The mod's unique ID within the . /// The mod data. - /// The stored mod record. - void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod); + void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod); /// Delete data for mods which haven't been requested within a given time limit. /// The minimum age for which to remove mods. diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 9c5a217e..62461116 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods ** Fields *********/ /// The cached mod data indexed by {site key}:{ID}. - private readonly IDictionary Mods = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + private readonly IDictionary> Mods = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); /********* @@ -24,19 +24,20 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) + public bool TryGetMod(ModRepositoryKey site, string id, out Cached mod, bool markRequested = true) { // get mod - if (!this.Mods.TryGetValue(this.GetKey(site, id), out mod)) + if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod)) + { + mod = null; return false; + } // bump 'last requested' if (markRequested) - { - mod.LastRequested = DateTimeOffset.UtcNow; - mod = this.SaveMod(mod); - } + cachedMod.LastRequested = DateTimeOffset.UtcNow; + mod = cachedMod; return true; } @@ -44,11 +45,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod site on which the mod is found. /// The mod's unique ID within the . /// The mod data. - /// The stored mod record. - public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) + public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod) { string key = this.GetKey(site, id); - cachedMod = this.SaveMod(new CachedMod(site, id, mod)); + this.Mods[key] = new Cached(mod); } /// Delete data for mods which haven't been requested within a given time limit. @@ -66,14 +66,6 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods this.Mods.Remove(key); } - /// Save data fetched for a mod. - /// The mod data. - public CachedMod SaveMod(CachedMod mod) - { - string key = this.GetKey(mod.Site, mod.ID); - return this.Mods[key] = mod; - } - /********* ** Private methods @@ -81,7 +73,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// Get a cache key. /// The mod site. /// The mod ID. - public string GetKey(ModRepositoryKey site, string id) + private string GetKey(ModRepositoryKey site, string id) { return $"{site}:{id.Trim()}".ToLower(); } diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs deleted file mode 100644 index f105baab..00000000 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using MongoDB.Driver; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; - -namespace StardewModdingAPI.Web.Framework.Caching.Mods -{ - /// Manages cached mod data in MongoDB. - internal class ModCacheMongoRepository : BaseCacheRepository, IModCacheRepository - { - /********* - ** Fields - *********/ - /// The collection for cached mod data. - private readonly IMongoCollection Mods; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The authenticated MongoDB database. - public ModCacheMongoRepository(IMongoDatabase database) - { - // get collections - this.Mods = database.GetCollection("mods"); - - // add indexes if needed - this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site))); - } - - - /********* - ** Public methods - *********/ - /// Get the cached mod data. - /// The mod site to search. - /// The mod's unique ID within the . - /// The fetched mod. - /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true) - { - // get mod - id = this.NormalizeId(id); - mod = this.Mods.Find(entry => entry.ID == id && entry.Site == site).FirstOrDefault(); - if (mod == null) - return false; - - // bump 'last requested' - if (markRequested) - { - mod.LastRequested = DateTimeOffset.UtcNow; - mod = this.SaveMod(mod); - } - - return true; - } - - /// Save data fetched for a mod. - /// The mod site on which the mod is found. - /// The mod's unique ID within the . - /// The mod data. - /// The stored mod record. - public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) - { - id = this.NormalizeId(id); - - cachedMod = this.SaveMod(new CachedMod(site, id, mod)); - } - - /// Delete data for mods which haven't been requested within a given time limit. - /// The minimum age for which to remove mods. - public void RemoveStaleMods(TimeSpan age) - { - DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age); - this.Mods.DeleteMany(p => p.LastRequested < minDate); - } - - /// Save data fetched for a mod. - /// The mod data. - public CachedMod SaveMod(CachedMod mod) - { - string id = this.NormalizeId(mod.ID); - - this.Mods.ReplaceOne( - entry => entry.ID == id && entry.Site == mod.Site, - mod, - new ReplaceOptions { IsUpsert = true } - ); - - return mod; - } - - - /********* - ** Private methods - *********/ - /// Normalize a mod ID for case-insensitive search. - /// The mod ID. - public string NormalizeId(string id) - { - return id.Trim().ToLower(); - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs b/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs deleted file mode 100644 index 6a103e37..00000000 --- a/src/SMAPI.Web/Framework/Caching/UtcDateTimeOffsetSerializer.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using MongoDB.Bson; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; - -namespace StardewModdingAPI.Web.Framework.Caching -{ - /// Serializes to a UTC date field instead of the default array. - public class UtcDateTimeOffsetSerializer : StructSerializerBase - { - /********* - ** Fields - *********/ - /// The underlying date serializer. - private static readonly DateTimeSerializer DateTimeSerializer = new DateTimeSerializer(DateTimeKind.Utc, BsonType.DateTime); - - - /********* - ** Public methods - *********/ - /// Deserializes a value. - /// The deserialization context. - /// The deserialization args. - /// A deserialized value. - public override DateTimeOffset Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) - { - DateTime date = UtcDateTimeOffsetSerializer.DateTimeSerializer.Deserialize(context, args); - return new DateTimeOffset(date, TimeSpan.Zero); - } - - /// Serializes a value. - /// The serialization context. - /// The serialization args. - /// The object. - public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTimeOffset value) - { - UtcDateTimeOffsetSerializer.DateTimeSerializer.Serialize(context, args, value.UtcDateTime); - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs deleted file mode 100644 index 6a560eb4..00000000 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMetadata.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using MongoDB.Bson; - -namespace StardewModdingAPI.Web.Framework.Caching.Wiki -{ - /// The model for cached wiki metadata. - internal class CachedWikiMetadata - { - /********* - ** Accessors - *********/ - /// The internal MongoDB ID. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] - public ObjectId _id { get; set; } - - /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } - - /// The current stable Stardew Valley version. - public string StableVersion { get; set; } - - /// The current beta Stardew Valley version. - public string BetaVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public CachedWikiMetadata() { } - - /// Construct an instance. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. - public CachedWikiMetadata(string stableVersion, string betaVersion) - { - this.StableVersion = stableVersion; - this.BetaVersion = betaVersion; - this.LastUpdated = DateTimeOffset.UtcNow; - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs deleted file mode 100644 index 7e7c99bc..00000000 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ /dev/null @@ -1,230 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using MongoDB.Bson; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson.Serialization.Options; -using StardewModdingAPI.Toolkit; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; - -namespace StardewModdingAPI.Web.Framework.Caching.Wiki -{ - /// The model for cached wiki mods. - internal class CachedWikiMod - { - /********* - ** Accessors - *********/ - /**** - ** Tracking - ****/ - /// The internal MongoDB ID. - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Named per MongoDB conventions.")] - public ObjectId _id { get; set; } - - /// When the data was last updated. - public DateTimeOffset LastUpdated { get; set; } - - /**** - ** Mod info - ****/ - /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. - public string[] ID { get; set; } - - /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. - public string[] Name { get; set; } - - /// The mod's author name. If the author has multiple names, the first one is the most canonical name. - public string[] Author { get; set; } - - /// The mod ID on Nexus. - public int? NexusID { get; set; } - - /// The mod ID in the Chucklefish mod repo. - public int? ChucklefishID { get; set; } - - /// The mod ID in the CurseForge mod repo. - public int? CurseForgeID { get; set; } - - /// The mod key in the CurseForge mod repo (used in mod page URLs). - public string CurseForgeKey { get; set; } - - /// The mod ID in the ModDrop mod repo. - public int? ModDropID { get; set; } - - /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } - - /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } - - /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } - - /// The name of the mod which loads this content pack, if applicable. - public string ContentPackFor { get; set; } - - /// The human-readable warnings for players about this mod. - public string[] Warnings { get; set; } - - /// The URL of the pull request which submits changes for an unofficial update to the author, if any. - public string PullRequestUrl { get; set; } - - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. - public string DevNote { get; set; } - - /// The link anchor for the mod entry in the wiki compatibility list. - public string Anchor { get; set; } - - /**** - ** Stable compatibility - ****/ - /// The compatibility status. - public WikiCompatibilityStatus MainStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string MainSummary { get; set; } - - /// The game or SMAPI version which broke this mod (if applicable). - public string MainBrokeIn { get; set; } - - /// The version of the latest unofficial update, if applicable. - public string MainUnofficialVersion { get; set; } - - /// The URL to the latest unofficial update, if applicable. - public string MainUnofficialUrl { get; set; } - - /**** - ** Beta compatibility - ****/ - /// The compatibility status. - public WikiCompatibilityStatus? BetaStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string BetaSummary { get; set; } - - /// The game or SMAPI version which broke this mod (if applicable). - public string BetaBrokeIn { get; set; } - - /// The version of the latest unofficial update, if applicable. - public string BetaUnofficialVersion { get; set; } - - /// The URL to the latest unofficial update, if applicable. - public string BetaUnofficialUrl { get; set; } - - /**** - ** Version maps - ****/ - /// Maps local versions to a semantic version for update checks. - [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] - public IDictionary MapLocalVersions { get; set; } - - /// Maps remote versions to a semantic version for update checks. - [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfArrays)] - public IDictionary MapRemoteVersions { get; set; } - - - /********* - ** Accessors - *********/ - /// Construct an instance. - public CachedWikiMod() { } - - /// Construct an instance. - /// The mod data. - public CachedWikiMod(WikiModEntry mod) - { - // tracking - this.LastUpdated = DateTimeOffset.UtcNow; - - // mod info - this.ID = mod.ID; - this.Name = mod.Name; - this.Author = mod.Author; - this.NexusID = mod.NexusID; - this.ChucklefishID = mod.ChucklefishID; - this.CurseForgeID = mod.CurseForgeID; - this.CurseForgeKey = mod.CurseForgeKey; - this.ModDropID = mod.ModDropID; - this.GitHubRepo = mod.GitHubRepo; - this.CustomSourceUrl = mod.CustomSourceUrl; - this.CustomUrl = mod.CustomUrl; - this.ContentPackFor = mod.ContentPackFor; - this.PullRequestUrl = mod.PullRequestUrl; - this.Warnings = mod.Warnings; - this.DevNote = mod.DevNote; - this.Anchor = mod.Anchor; - - // stable compatibility - this.MainStatus = mod.Compatibility.Status; - this.MainSummary = mod.Compatibility.Summary; - this.MainBrokeIn = mod.Compatibility.BrokeIn; - this.MainUnofficialVersion = mod.Compatibility.UnofficialVersion?.ToString(); - this.MainUnofficialUrl = mod.Compatibility.UnofficialUrl; - - // beta compatibility - this.BetaStatus = mod.BetaCompatibility?.Status; - this.BetaSummary = mod.BetaCompatibility?.Summary; - this.BetaBrokeIn = mod.BetaCompatibility?.BrokeIn; - this.BetaUnofficialVersion = mod.BetaCompatibility?.UnofficialVersion?.ToString(); - this.BetaUnofficialUrl = mod.BetaCompatibility?.UnofficialUrl; - - // version maps - this.MapLocalVersions = mod.MapLocalVersions; - this.MapRemoteVersions = mod.MapRemoteVersions; - } - - /// Reconstruct the original model. - public WikiModEntry GetModel() - { - var mod = new WikiModEntry - { - ID = this.ID, - Name = this.Name, - Author = this.Author, - NexusID = this.NexusID, - ChucklefishID = this.ChucklefishID, - CurseForgeID = this.CurseForgeID, - CurseForgeKey = this.CurseForgeKey, - ModDropID = this.ModDropID, - GitHubRepo = this.GitHubRepo, - CustomSourceUrl = this.CustomSourceUrl, - CustomUrl = this.CustomUrl, - ContentPackFor = this.ContentPackFor, - Warnings = this.Warnings, - PullRequestUrl = this.PullRequestUrl, - DevNote = this.DevNote, - Anchor = this.Anchor, - - // stable compatibility - Compatibility = new WikiCompatibilityInfo - { - Status = this.MainStatus, - Summary = this.MainSummary, - BrokeIn = this.MainBrokeIn, - UnofficialVersion = this.MainUnofficialVersion != null ? new SemanticVersion(this.MainUnofficialVersion) : null, - UnofficialUrl = this.MainUnofficialUrl - }, - - // version maps - MapLocalVersions = this.MapLocalVersions, - MapRemoteVersions = this.MapRemoteVersions - }; - - // beta compatibility - if (this.BetaStatus != null) - { - mod.BetaCompatibility = new WikiCompatibilityInfo - { - Status = this.BetaStatus.Value, - Summary = this.BetaSummary, - BrokeIn = this.BetaBrokeIn, - UnofficialVersion = this.BetaUnofficialVersion != null ? new SemanticVersion(this.BetaUnofficialVersion) : null, - UnofficialUrl = this.BetaUnofficialUrl - }; - } - - return mod; - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs index 02097f52..2ab7ea5a 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq.Expressions; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki @@ -13,18 +12,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - bool TryGetWikiMetadata(out CachedWikiMetadata metadata); + bool TryGetWikiMetadata(out Cached metadata); /// Get the cached wiki mods. /// A filter to apply, if any. - IEnumerable GetWikiMods(Expression> filter = null); + IEnumerable> GetWikiMods(Func filter = null); /// Save data fetched from the wiki compatibility list. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - /// The stored metadata record. - /// The stored mod records. - void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods); + void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods); } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs index 4621f5e3..064a7c3c 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; namespace StardewModdingAPI.Web.Framework.Caching.Wiki @@ -13,10 +12,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki ** Fields *********/ /// The saved wiki metadata. - private CachedWikiMetadata Metadata; + private Cached Metadata; /// The cached wiki data. - private CachedWikiMod[] Mods = new CachedWikiMod[0]; + private Cached[] Mods = new Cached[0]; /********* @@ -24,7 +23,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki *********/ /// Get the cached wiki metadata. /// The fetched metadata. - public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) + public bool TryGetWikiMetadata(out Cached metadata) { metadata = this.Metadata; return metadata != null; @@ -32,23 +31,23 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// Get the cached wiki mods. /// A filter to apply, if any. - public IEnumerable GetWikiMods(Expression> filter = null) + public IEnumerable> GetWikiMods(Func filter = null) { - return filter != null - ? this.Mods.Where(filter.Compile()) - : this.Mods.ToArray(); + foreach (var mod in this.Mods) + { + if (filter == null || filter(mod.Data)) + yield return mod; + } } /// Save data fetched from the wiki compatibility list. /// The current stable Stardew Valley version. /// The current beta Stardew Valley version. /// The mod data. - /// The stored metadata record. - /// The stored mod records. - public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) + public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods) { - this.Metadata = cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); - this.Mods = cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); + this.Metadata = new Cached(new WikiMetadata(stableVersion, betaVersion)); + this.Mods = mods.Select(mod => new Cached(mod)).ToArray(); } } } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs deleted file mode 100644 index 07e7c721..00000000 --- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using MongoDB.Driver; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; - -namespace StardewModdingAPI.Web.Framework.Caching.Wiki -{ - /// Manages cached wiki data in MongoDB. - internal class WikiCacheMongoRepository : BaseCacheRepository, IWikiCacheRepository - { - /********* - ** Fields - *********/ - /// The collection for wiki metadata. - private readonly IMongoCollection Metadata; - - /// The collection for wiki mod data. - private readonly IMongoCollection Mods; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The authenticated MongoDB database. - public WikiCacheMongoRepository(IMongoDatabase database) - { - // get collections - this.Metadata = database.GetCollection("wiki-metadata"); - this.Mods = database.GetCollection("wiki-mods"); - - // add indexes if needed - this.Mods.Indexes.CreateOne(new CreateIndexModel(Builders.IndexKeys.Ascending(p => p.ID))); - } - - /// Get the cached wiki metadata. - /// The fetched metadata. - public bool TryGetWikiMetadata(out CachedWikiMetadata metadata) - { - metadata = this.Metadata.Find("{}").FirstOrDefault(); - return metadata != null; - } - - /// Get the cached wiki mods. - /// A filter to apply, if any. - public IEnumerable GetWikiMods(Expression> filter = null) - { - return filter != null - ? this.Mods.Find(filter).ToList() - : this.Mods.Find("{}").ToList(); - } - - /// Save data fetched from the wiki compatibility list. - /// The current stable Stardew Valley version. - /// The current beta Stardew Valley version. - /// The mod data. - /// The stored metadata record. - /// The stored mod records. - public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods) - { - cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion); - cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray(); - - this.Mods.DeleteMany("{}"); - this.Mods.InsertMany(cachedMods); - - this.Metadata.DeleteMany("{}"); - this.Metadata.InsertOne(cachedMetadata); - } - } -} diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs new file mode 100644 index 00000000..c04de4a5 --- /dev/null +++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiMetadata.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Web.Framework.Caching.Wiki +{ + /// The model for cached wiki metadata. + internal class WikiMetadata + { + /********* + ** Accessors + *********/ + /// The current stable Stardew Valley version. + public string StableVersion { get; set; } + + /// The current beta Stardew Valley version. + public string BetaVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public WikiMetadata() { } + + /// Construct an instance. + /// The current stable Stardew Valley version. + /// The current beta Stardew Valley version. + public WikiMetadata(string stableVersion, string betaVersion) + { + this.StableVersion = stableVersion; + this.BetaVersion = betaVersion; + } + } +} diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs deleted file mode 100644 index 61cc4855..00000000 --- a/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// The config settings for cache storage. - internal class StorageConfig - { - /********* - ** Accessors - *********/ - /// The storage mechanism to use. - public StorageMode Mode { get; set; } - - /// The connection string for the storage mechanism, if applicable. - public string ConnectionString { get; set; } - - /// The database name for the storage mechanism, if applicable. - public string Database { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs deleted file mode 100644 index 4c2ea801..00000000 --- a/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ConfigModels -{ - /// Indicates a storage mechanism to use. - internal enum StorageMode - { - /// Store data in a hosted MongoDB instance. - Mongo, - - /// Store data in an in-memory MongoDB instance. This is useful for testing MongoDB storage locally, but will likely fail when deployed since it needs permission to open a local port. - MongoInMemory, - - /// Store data in-memory. This is suitable for local testing or single-instance servers, but will cause issues when distributed across multiple servers. - InMemory - } -} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 7ed79ea3..c6c0f774 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -15,14 +15,11 @@ - - - diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index dee2edc2..586b0c3c 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using System.Net; using Hangfire; using Hangfire.MemoryStorage; -using Hangfire.Mongo; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Rewrite; @@ -11,13 +9,9 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Mongo2Go; -using MongoDB.Bson.Serialization; -using MongoDB.Driver; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Web.Framework; -using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; @@ -68,13 +62,10 @@ namespace StardewModdingAPI.Web .Configure(this.Configuration.GetSection("BackgroundServices")) .Configure(this.Configuration.GetSection("ModCompatibilityList")) .Configure(this.Configuration.GetSection("ModUpdateCheck")) - .Configure(this.Configuration.GetSection("Storage")) .Configure(this.Configuration.GetSection("Site")) .Configure(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint))) .AddLogging() .AddMemoryCache(); - StorageConfig storageConfig = this.Configuration.GetSection("Storage").Get(); - StorageMode storageMode = storageConfig.Mode; // init MVC services @@ -85,39 +76,8 @@ namespace StardewModdingAPI.Web .AddRazorPages(); // init storage - switch (storageMode) - { - case StorageMode.InMemory: - services.AddSingleton(new ModCacheMemoryRepository()); - services.AddSingleton(new WikiCacheMemoryRepository()); - break; - - case StorageMode.Mongo: - case StorageMode.MongoInMemory: - { - // local MongoDB instance - services.AddSingleton(_ => storageMode == StorageMode.MongoInMemory - ? MongoDbRunner.Start() - : throw new NotSupportedException($"The in-memory MongoDB runner isn't available in storage mode {storageMode}.") - ); - - // MongoDB - services.AddSingleton(serv => - { - BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer()); - return new MongoClient(this.GetMongoDbConnectionString(serv, storageConfig)) - .GetDatabase(storageConfig.Database); - }); - - // repositories - services.AddSingleton(serv => new ModCacheMongoRepository(serv.GetRequiredService())); - services.AddSingleton(serv => new WikiCacheMongoRepository(serv.GetRequiredService())); - } - break; - - default: - throw new NotSupportedException($"Unhandled storage mode '{storageMode}'."); - } + services.AddSingleton(new ModCacheMemoryRepository()); + services.AddSingleton(new WikiCacheMemoryRepository()); // init Hangfire services @@ -126,24 +86,8 @@ namespace StardewModdingAPI.Web config .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() - .UseRecommendedSerializerSettings(); - - switch (storageMode) - { - case StorageMode.InMemory: - config.UseMemoryStorage(); - break; - - case StorageMode.MongoInMemory: - case StorageMode.Mongo: - string connectionString = this.GetMongoDbConnectionString(serv, storageConfig); - config.UseMongoStorage(MongoClientSettings.FromConnectionString(connectionString), $"{storageConfig.Database}-hangfire", new MongoStorageOptions - { - MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop), - CheckConnection = false // error on startup takes down entire process - }); - break; - } + .UseRecommendedSerializerSettings() + .UseMemoryStorage(); }); // init background service @@ -254,20 +198,6 @@ namespace StardewModdingAPI.Web settings.NullValueHandling = NullValueHandling.Ignore; } - /// Get the MongoDB connection string for the given storage configuration. - /// The service provider. - /// The storage configuration - /// There's no MongoDB instance in the given storage mode. - private string GetMongoDbConnectionString(IServiceProvider services, StorageConfig storageConfig) - { - return storageConfig.Mode switch - { - StorageMode.Mongo => storageConfig.ConnectionString, - StorageMode.MongoInMemory => services.GetRequiredService().ConnectionString, - _ => throw new NotSupportedException($"There's no MongoDB instance in storage mode {storageConfig.Mode}.") - }; - } - /// Get the redirect rules to apply. private RewriteOptions GetRedirectRules() { diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 41c00e79..3aa69285 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -17,12 +17,6 @@ "NexusApiKey": null }, - "Storage": { - "Mode": "MongoInMemory", - "ConnectionString": null, - "Database": "smapi-edge" - }, - "BackgroundServices": { "Enabled": true } -- cgit From 786077340f2cea37d82455fc413535ae82a912ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 May 2020 21:55:11 -0400 Subject: refactor update check API This simplifies the logic for individual clients, centralises common logic, and prepares for upcoming features. --- docs/release-notes.md | 1 + .../Framework/UpdateData/ModRepositoryKey.cs | 24 --- .../Framework/UpdateData/ModSiteKey.cs | 24 +++ .../Framework/UpdateData/UpdateKey.cs | 67 ++++---- src/SMAPI.Web/Controllers/ModsApiController.cs | 146 ++++------------- .../Framework/Caching/Mods/IModCacheRepository.cs | 6 +- .../Caching/Mods/ModCacheMemoryRepository.cs | 12 +- .../Clients/Chucklefish/ChucklefishClient.cs | 38 +++-- .../Clients/Chucklefish/ChucklefishMod.cs | 18 --- .../Clients/Chucklefish/IChucklefishClient.cs | 12 +- .../Clients/CurseForge/CurseForgeClient.cs | 72 ++++----- .../Framework/Clients/CurseForge/CurseForgeMod.cs | 23 --- .../Clients/CurseForge/ICurseForgeClient.cs | 12 +- .../Framework/Clients/GenericModDownload.cs | 36 +++++ src/SMAPI.Web/Framework/Clients/GenericModPage.cs | 79 +++++++++ .../Framework/Clients/GitHub/GitHubClient.cs | 56 +++++++ .../Framework/Clients/GitHub/IGitHubClient.cs | 2 +- src/SMAPI.Web/Framework/Clients/IModSiteClient.cs | 23 +++ .../Framework/Clients/ModDrop/IModDropClient.cs | 12 +- .../Framework/Clients/ModDrop/ModDropClient.cs | 63 ++++---- .../Framework/Clients/ModDrop/ModDropMod.cs | 21 --- .../ModDrop/ResponseModels/FileDataModel.cs | 16 +- .../Framework/Clients/Nexus/INexusClient.cs | 12 +- .../Framework/Clients/Nexus/NexusClient.cs | 94 ++++++----- src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs | 32 ---- .../Clients/Nexus/ResponseModels/NexusMod.cs | 33 ++++ src/SMAPI.Web/Framework/Extensions.cs | 6 + src/SMAPI.Web/Framework/IModDownload.cs | 15 ++ src/SMAPI.Web/Framework/IModPage.cs | 52 ++++++ src/SMAPI.Web/Framework/ModInfoModel.cs | 81 ++++++++++ .../Framework/ModRepositories/BaseRepository.cs | 51 ------ .../ModRepositories/ChucklefishRepository.cs | 57 ------- .../ModRepositories/CurseForgeRepository.cs | 63 -------- .../Framework/ModRepositories/GitHubRepository.cs | 82 ---------- .../Framework/ModRepositories/IModRepository.cs | 24 --- .../Framework/ModRepositories/ModDropRepository.cs | 57 ------- .../Framework/ModRepositories/ModInfoModel.cs | 96 ----------- .../Framework/ModRepositories/NexusRepository.cs | 65 -------- .../Framework/ModRepositories/RemoteModStatus.cs | 18 --- src/SMAPI.Web/Framework/ModSiteManager.cs | 180 +++++++++++++++++++++ src/SMAPI.Web/Framework/RemoteModStatus.cs | 18 +++ 41 files changed, 825 insertions(+), 974 deletions(-) delete mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs create mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GenericModDownload.cs create mode 100644 src/SMAPI.Web/Framework/Clients/GenericModPage.cs create mode 100644 src/SMAPI.Web/Framework/Clients/IModSiteClient.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs delete mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs create mode 100644 src/SMAPI.Web/Framework/IModDownload.cs create mode 100644 src/SMAPI.Web/Framework/IModPage.cs create mode 100644 src/SMAPI.Web/Framework/ModInfoModel.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs delete mode 100644 src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs create mode 100644 src/SMAPI.Web/Framework/ModSiteManager.cs create mode 100644 src/SMAPI.Web/Framework/RemoteModStatus.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index f3f2efa4..894fd562 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -24,6 +24,7 @@ * For SMAPI developers: * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. + * Overhauled update checks to simplify individual clients, centralize common logic, and enable upcoming features. * Merged the separate legacy redirects app on AWS into the main app on Azure. ## 3.5 diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs deleted file mode 100644 index 765ca334..00000000 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.UpdateData -{ - /// A mod repository which SMAPI can check for updates. - public enum ModRepositoryKey - { - /// An unknown or invalid mod repository. - Unknown, - - /// The Chucklefish mod repository. - Chucklefish, - - /// The CurseForge mod repository. - CurseForge, - - /// A GitHub project containing releases. - GitHub, - - /// The ModDrop mod repository. - ModDrop, - - /// The Nexus Mods mod repository. - Nexus - } -} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs new file mode 100644 index 00000000..47cd3f7e --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A mod site which SMAPI can check for updates. + public enum ModSiteKey + { + /// An unknown or invalid mod repository. + Unknown, + + /// The Chucklefish mod repository. + Chucklefish, + + /// The CurseForge mod repository. + CurseForge, + + /// A GitHub project containing releases. + GitHub, + + /// The ModDrop mod repository. + ModDrop, + + /// The Nexus Mods mod repository. + Nexus + } +} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 3fc1759e..f6044148 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -11,8 +11,8 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The raw update key text. public string RawText { get; } - /// The mod repository containing the mod. - public ModRepositoryKey Repository { get; } + /// The mod site containing the mod. + public ModSiteKey Site { get; } /// The mod ID within the repository. public string ID { get; } @@ -26,53 +26,56 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData *********/ /// Construct an instance. /// The raw update key text. - /// The mod repository containing the mod. - /// The mod ID within the repository. - public UpdateKey(string rawText, ModRepositoryKey repository, string id) + /// The mod site containing the mod. + /// The mod ID within the site. + public UpdateKey(string rawText, ModSiteKey site, string id) { - this.RawText = rawText; - this.Repository = repository; - this.ID = id; + this.RawText = rawText?.Trim(); + this.Site = site; + this.ID = id?.Trim(); this.LooksValid = - repository != ModRepositoryKey.Unknown + site != ModSiteKey.Unknown && !string.IsNullOrWhiteSpace(id); } /// Construct an instance. - /// The mod repository containing the mod. - /// The mod ID within the repository. - public UpdateKey(ModRepositoryKey repository, string id) - : this($"{repository}:{id}", repository, id) { } + /// The mod site containing the mod. + /// The mod ID within the site. + public UpdateKey(ModSiteKey site, string id) + : this(UpdateKey.GetString(site, id), site, id) { } /// Parse a raw update key. /// The raw update key to parse. public static UpdateKey Parse(string raw) { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - return new UpdateKey(raw, ModRepositoryKey.Unknown, null); - - // extract parts - string repositoryKey = parts[0].Trim(); - string id = parts[1].Trim(); + // extract site + ID + string rawSite; + string id; + { + string[] parts = raw?.Trim().Split(':'); + if (parts == null || parts.Length != 2) + return new UpdateKey(raw, ModSiteKey.Unknown, null); + + rawSite = parts[0].Trim(); + id = parts[1].Trim(); + } if (string.IsNullOrWhiteSpace(id)) id = null; // parse - if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository)) - return new UpdateKey(raw, ModRepositoryKey.Unknown, id); + if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) + return new UpdateKey(raw, ModSiteKey.Unknown, id); if (id == null) - return new UpdateKey(raw, repository, null); + return new UpdateKey(raw, site, null); - return new UpdateKey(raw, repository, id); + return new UpdateKey(raw, site, id); } /// Get a string that represents the current object. public override string ToString() { return this.LooksValid - ? $"{this.Repository}:{this.ID}" + ? UpdateKey.GetString(this.Site, this.ID) : this.RawText; } @@ -82,7 +85,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { return other != null - && this.Repository == other.Repository + && this.Site == other.Site && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); } @@ -97,7 +100,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// A hash code for the current object. public override int GetHashCode() { - return $"{this.Repository}:{this.ID}".ToLower().GetHashCode(); + return $"{this.Site}:{this.ID}".ToLower().GetHashCode(); + } + + /// Get the string representation of an update key. + /// The mod site containing the mod. + /// The mod ID within the repository. + public static string GetString(ModSiteKey site, string id) + { + return $"{site}:{id}".Trim(); } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index b9d7c32d..14be520d 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -15,13 +15,13 @@ using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; +using StardewModdingAPI.Web.Framework.Clients; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; using StardewModdingAPI.Web.Framework.ConfigModels; -using StardewModdingAPI.Web.Framework.ModRepositories; namespace StardewModdingAPI.Web.Controllers { @@ -33,8 +33,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Fields *********/ - /// The mod repositories which provide mod metadata. - private readonly IDictionary Repositories; + /// The mod sites which provide mod metadata. + private readonly ModSiteManager ModSites; /// The cache in which to store wiki data. private readonly IWikiCacheRepository WikiCache; @@ -69,16 +69,7 @@ namespace StardewModdingAPI.Web.Controllers this.WikiCache = wikiCache; this.ModCache = modCache; this.Config = config; - this.Repositories = - new IModRepository[] - { - new ChucklefishRepository(chucklefish), - new CurseForgeRepository(curseForge), - new GitHubRepository(github), - new ModDropRepository(modDrop), - new NexusRepository(nexus) - } - .ToDictionary(p => p.VendorKey); + this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus }); } /// Fetch version metadata for the given mods. @@ -149,40 +140,18 @@ namespace StardewModdingAPI.Web.Controllers } // fetch data - ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions); - if (data.Error != null) + ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions); + if (data.Status != RemoteModStatus.Ok) { - errors.Add(data.Error); + errors.Add(data.Error ?? data.Status.ToString()); continue; } - // handle main version - if (data.Version != null) - { - ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); - if (version == null) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); - continue; - } - - if (this.IsNewer(version, main?.Version)) - main = new ModEntryVersionModel(version, data.Url); - } - - // handle optional version - if (data.PreviewVersion != null) - { - ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions); - if (version == null) - { - errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); - continue; - } - - if (this.IsNewer(version, optional?.Version)) - optional = new ModEntryVersionModel(version, data.Url); - } + // handle versions + if (this.IsNewer(data.Version, main?.Version)) + main = new ModEntryVersionModel(data.Version, data.Url); + if (this.IsNewer(data.PreviewVersion, optional?.Version)) + optional = new ModEntryVersionModel(data.PreviewVersion, data.Url); } // get unofficial version @@ -222,7 +191,7 @@ namespace StardewModdingAPI.Web.Controllers } // get recommended update (if any) - ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions); + ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions @@ -282,32 +251,27 @@ namespace StardewModdingAPI.Web.Controllers /// Get the mod info for an update key. /// The namespaced update key. /// Whether to allow non-standard versions. - private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions) + /// Maps remote versions to a semantic version for update checks. + private async Task GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, IDictionary mapRemoteVersions) { - // get from cache - if (this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out Cached cachedMod) && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes)) - return cachedMod.Data; - - // fetch from mod site + // get mod page + IModPage page; { - // get site - if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository)) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); + bool isCached = + this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached cachedMod) + && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes); - // fetch mod - ModInfoModel mod = await repository.GetModInfoAsync(updateKey.ID); - if (mod.Error == null) + if (isCached) + page = cachedMod.Data; + else { - if (mod.Version == null) - mod.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); - else if (!SemanticVersion.TryParse(mod.Version, allowNonStandardVersions, out _)) - mod.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{mod.Version}'."); + page = await this.ModSites.GetModPageAsync(updateKey); + this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page); } - - // cache mod - this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, mod); - return mod; } + + // get version info + return this.ModSites.GetPageVersions(page, allowNonStandardVersions, mapRemoteVersions); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. @@ -334,13 +298,13 @@ namespace StardewModdingAPI.Web.Controllers if (entry != null) { if (entry.NexusID.HasValue) - yield return $"{ModRepositoryKey.Nexus}:{entry.NexusID}"; + yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID?.ToString()); if (entry.ModDropID.HasValue) - yield return $"{ModRepositoryKey.ModDrop}:{entry.ModDropID}"; + yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID?.ToString()); if (entry.CurseForgeID.HasValue) - yield return $"{ModRepositoryKey.CurseForge}:{entry.CurseForgeID}"; + yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID?.ToString()); if (entry.ChucklefishID.HasValue) - yield return $"{ModRepositoryKey.Chucklefish}:{entry.ChucklefishID}"; + yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID?.ToString()); } } @@ -355,51 +319,5 @@ namespace StardewModdingAPI.Web.Controllers yield return key; } } - - /// Get a semantic local version for update checks. - /// The version to parse. - /// A map of version replacements. - /// Whether to allow non-standard versions. - private ISemanticVersion GetMappedVersion(string version, IDictionary map, bool allowNonStandard) - { - // try mapped version - string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard); - if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew)) - return parsedNew; - - // return original version - return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld) - ? parsedOld - : null; - } - - /// Get a semantic local version for update checks. - /// The version to map. - /// A map of version replacements. - /// Whether to allow non-standard versions. - private string GetRawMappedVersion(string version, IDictionary map, bool allowNonStandard) - { - if (version == null || map == null || !map.Any()) - return version; - - // match exact raw version - if (map.ContainsKey(version)) - return map[version]; - - // match parsed version - if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed)) - { - if (map.ContainsKey(parsed.ToString())) - return map[parsed.ToString()]; - - foreach ((string fromRaw, string toRaw) in map) - { - if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion)) - return newVersion.ToString(); - } - } - - return version; - } } } diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs index 004202f9..0d912c7b 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs @@ -1,6 +1,6 @@ using System; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; +using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { @@ -15,13 +15,13 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - bool TryGetMod(ModRepositoryKey site, string id, out Cached mod, bool markRequested = true); + bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true); /// Save data fetched for a mod. /// The mod site on which the mod is found. /// The mod's unique ID within the . /// The mod data. - void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod); + void SaveMod(ModSiteKey site, string id, IModPage mod); /// Delete data for mods which haven't been requested within a given time limit. /// The minimum age for which to remove mods. diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs index 62461116..6b0ec1ec 100644 --- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs +++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.ModRepositories; +using StardewModdingAPI.Web.Framework.Clients; namespace StardewModdingAPI.Web.Framework.Caching.Mods { @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods ** Fields *********/ /// The cached mod data indexed by {site key}:{ID}. - private readonly IDictionary> Mods = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + private readonly IDictionary> Mods = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); /********* @@ -24,7 +24,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod's unique ID within the . /// The fetched mod. /// Whether to update the mod's 'last requested' date. - public bool TryGetMod(ModRepositoryKey site, string id, out Cached mod, bool markRequested = true) + public bool TryGetMod(ModSiteKey site, string id, out Cached mod, bool markRequested = true) { // get mod if (!this.Mods.TryGetValue(this.GetKey(site, id), out var cachedMod)) @@ -45,10 +45,10 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// The mod site on which the mod is found. /// The mod's unique ID within the . /// The mod data. - public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod) + public void SaveMod(ModSiteKey site, string id, IModPage mod) { string key = this.GetKey(site, id); - this.Mods[key] = new Cached(mod); + this.Mods[key] = new Cached(mod); } /// Delete data for mods which haven't been requested within a given time limit. @@ -73,7 +73,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods /// Get a cache key. /// The mod site. /// The mod ID. - private string GetKey(ModRepositoryKey site, string id) + private string GetKey(ModSiteKey site, string id) { return $"{site}:{id.Trim()}".ToLower(); } diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index cdb281e2..ca156da4 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish { @@ -19,6 +20,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish private readonly IClient Client; + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.Chucklefish; + + /********* ** Public methods *********/ @@ -32,42 +40,40 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); } - /// Get metadata about a mod. - /// The Chucklefish mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(uint id) + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + // get mod ID + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); + // fetch HTML string html; try { html = await this.Client - .GetAsync(string.Format(this.ModPageUrlFormat, id)) + .GetAsync(string.Format(this.ModPageUrlFormat, parsedId)) .AsString(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound || ex.Status == HttpStatusCode.Forbidden) { - return null; + return page.SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); } - - // parse HTML var doc = new HtmlDocument(); doc.LoadHtml(html); // extract mod info - string url = this.GetModUrl(id); + string url = this.GetModUrl(parsedId); string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; if (name.StartsWith("[SMAPI] ")) name = name.Substring("[SMAPI] ".Length); string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; - // create model - return new ChucklefishMod - { - Name = name, - Version = version, - Url = url - }; + // return info + return page.SetInfo(name: name, version: version, url: url, downloads: Array.Empty()); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs deleted file mode 100644 index fd0101d4..00000000 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishMod.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish -{ - /// Mod metadata from the Chucklefish mod site. - internal class ChucklefishMod - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs index 1d8b256e..836d43f7 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/IChucklefishClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish { /// An HTTP client for fetching mod metadata from the Chucklefish mod site. - internal interface IChucklefishClient : IDisposable - { - /********* - ** Methods - *********/ - /// Get metadata about a mod. - /// The Chucklefish mod ID. - /// Returns the mod info if found, else null. - Task GetModAsync(uint id); - } + internal interface IChucklefishClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs index a6fd21fd..d8008721 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs @@ -1,8 +1,8 @@ -using System.Linq; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.CurseForge.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.CurseForge @@ -20,6 +20,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge private readonly Regex VersionInNamePattern = new Regex(@"^(?:.+? | *)v?(\d+\.\d+(?:\.\d+)?(?:-.+?)?) *(?:\.(?:zip|rar|7z))?$", RegexOptions.Compiled); + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.CurseForge; + + /********* ** Public methods *********/ @@ -31,59 +38,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge this.Client = new FluentClient(apiUrl).SetUserAgent(userAgent); } - /// Get metadata about a mod. - /// The CurseForge mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(long id) + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + // get ID + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); + // get raw data ModModel mod = await this.Client - .GetAsync($"addon/{id}") + .GetAsync($"addon/{parsedId}") .As(); if (mod == null) - return null; + return page.SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); - // get latest versions - string invalidVersion = null; - ISemanticVersion latest = null; + // get downloads + List downloads = new List(); foreach (ModFileModel file in mod.LatestFiles) { - // extract version - ISemanticVersion version; - { - string raw = this.GetRawVersion(file); - if (raw == null) - continue; - - if (!SemanticVersion.TryParse(raw, out version)) - { - invalidVersion ??= raw; - continue; - } - } - - // track latest version - if (latest == null || version.IsNewerThan(latest)) - latest = version; - } - - // get error - string error = null; - if (latest == null && invalidVersion == null) - { - error = mod.LatestFiles.Any() - ? $"CurseForge mod {id} has no downloads which specify the version in a recognised format." - : $"CurseForge mod {id} has no downloads."; + downloads.Add( + new GenericModDownload(name: file.DisplayName ?? file.FileName, description: null, version: this.GetRawVersion(file)) + ); } - // generate result - return new CurseForgeMod - { - Name = mod.Name, - LatestVersion = latest?.ToString() ?? invalidVersion, - Url = mod.WebsiteUrl, - Error = error - }; + // return info + return page.SetInfo(name: mod.Name, version: null, url: mod.WebsiteUrl, downloads: downloads); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs deleted file mode 100644 index e5bb8cf1..00000000 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeMod.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Web.Framework.Clients.CurseForge -{ - /// Mod metadata from the CurseForge API. - internal class CurseForgeMod - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The latest file version. - public string LatestVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - public string Error { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs index 907b4087..2018c230 100644 --- a/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs +++ b/src/SMAPI.Web/Framework/Clients/CurseForge/ICurseForgeClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.CurseForge { /// An HTTP client for fetching mod metadata from the CurseForge API. - internal interface ICurseForgeClient : IDisposable - { - /********* - ** Methods - *********/ - /// Get metadata about a mod. - /// The CurseForge mod ID. - /// Returns the mod info if found, else null. - Task GetModAsync(long id); - } + internal interface ICurseForgeClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs new file mode 100644 index 00000000..f08b471c --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -0,0 +1,36 @@ +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// Generic metadata about a file download on a mod page. + internal class GenericModDownload : IModDownload + { + /********* + ** Accessors + *********/ + /// The download's display name. + public string Name { get; set; } + + /// The download's description. + public string Description { get; set; } + + /// The download's file version. + public string Version { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public GenericModDownload() { } + + /// Construct an instance. + /// The download's display name. + /// The download's description. + /// The download's file version. + public GenericModDownload(string name, string description, string version) + { + this.Name = name; + this.Description = description; + this.Version = version; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs new file mode 100644 index 00000000..622e6c56 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// Generic metadata about a mod page. + internal class GenericModPage : IModPage + { + /********* + ** Accessors + *********/ + /// The mod site containing the mod. + public ModSiteKey Site { get; set; } + + /// The mod's unique ID within the site. + public string Id { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The mod downloads. + public IModDownload[] Downloads { get; set; } = new IModDownload[0]; + + /// The mod availability status on the remote site. + public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public GenericModPage() { } + + /// Construct an instance. + /// The mod site containing the mod. + /// The mod's unique ID within the site. + public GenericModPage(ModSiteKey site, string id) + { + this.Site = site; + this.Id = id; + } + + /// Set the fetched mod info. + /// The mod name. + /// The mod's semantic version number. + /// The mod's web URL. + /// The mod downloads. + public IModPage SetInfo(string name, string version, string url, IEnumerable downloads) + { + this.Name = name; + this.Version = version; + this.Url = url; + this.Downloads = downloads.ToArray(); + + return this; + } + + /// Set a mod fetch error. + /// The mod availability status on the remote site. + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + public IModPage SetError(RemoteModStatus status, string error) + { + this.Status = status; + this.Error = error; + + return this; + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs index 84c20957..2f1eb854 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/GitHubClient.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using Pathoschild.Http.Client; +using StardewModdingAPI.Toolkit.Framework.UpdateData; namespace StardewModdingAPI.Web.Framework.Clients.GitHub { @@ -16,6 +17,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub private readonly IClient Client; + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.GitHub; + + /********* ** Public methods *********/ @@ -79,6 +87,54 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub } } + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) + { + IModPage page = new GenericModPage(this.SiteKey, id); + + if (!id.Contains("/") || id.IndexOf("/", StringComparison.OrdinalIgnoreCase) != id.LastIndexOf("/", StringComparison.OrdinalIgnoreCase)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/SMAPI'."); + + // fetch repo info + GitRepo repository = await this.GetRepositoryAsync(id); + if (repository == null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); + string name = repository.FullName; + string url = $"{repository.WebUrl}/releases"; + + // get releases + GitRelease latest; + GitRelease preview; + { + // get latest release (whether preview or stable) + latest = await this.GetLatestReleaseAsync(id, includePrerelease: true); + if (latest == null) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); + + // get stable version if different + preview = null; + if (latest.IsPrerelease) + { + GitRelease release = await this.GetLatestReleaseAsync(id, includePrerelease: false); + if (release != null) + { + preview = latest; + latest = release; + } + } + } + + // get downloads + IModDownload[] downloads = new[] { latest, preview } + .Where(release => release != null) + .Select(release => (IModDownload)new GenericModDownload(release.Name, release.Body, release.Tag)) + .ToArray(); + + // return info + return page.SetInfo(name: name, url: url, version: null, downloads: downloads); + } + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { diff --git a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs index a34f03bd..0d6f4643 100644 --- a/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs +++ b/src/SMAPI.Web/Framework/Clients/GitHub/IGitHubClient.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.GitHub { /// An HTTP client for fetching metadata from GitHub. - internal interface IGitHubClient : IDisposable + internal interface IGitHubClient : IModSiteClient, IDisposable { /********* ** Methods diff --git a/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs new file mode 100644 index 00000000..33277711 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/IModSiteClient.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework.Clients +{ + /// A client for fetching update check info from a mod site. + internal interface IModSiteClient + { + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey { get; } + + + /********* + ** Methods + *********/ + /// Get update check info about a mod. + /// The mod ID. + Task GetModData(string id); + } +} diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/IModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/IModDropClient.cs index 3ede46e2..468b72b1 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/IModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/IModDropClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop { /// An HTTP client for fetching mod metadata from the ModDrop API. - internal interface IModDropClient : IDisposable - { - /********* - ** Methods - *********/ - /// Get metadata about a mod. - /// The ModDrop mod ID. - /// Returns the mod info if found, else null. - Task GetModAsync(long id); - } + internal interface IModDropClient : IDisposable, IModSiteClient { } } diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs index 5ad2d2f8..3a1c5b9d 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs @@ -1,6 +1,7 @@ +using System.Collections.Generic; using System.Threading.Tasks; using Pathoschild.Http.Client; -using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; using StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels; namespace StardewModdingAPI.Web.Framework.Clients.ModDrop @@ -18,6 +19,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop private readonly string ModUrlFormat; + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.ModDrop; + + /********* ** Public methods *********/ @@ -31,60 +39,45 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop this.ModUrlFormat = modUrlFormat; } - /// Get metadata about a mod. - /// The ModDrop mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(long id) + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) { + var page = new GenericModPage(this.SiteKey, id); + + if (!long.TryParse(id, out long parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); + // get raw data ModListModel response = await this.Client .PostAsync("") .WithBody(new { - ModIDs = new[] { id }, + ModIDs = new[] { parsedId }, Files = true, Mods = true }) .As(); - ModModel mod = response.Mods[id]; + ModModel mod = response.Mods[parsedId]; if (mod.Mod?.Title == null || mod.Mod.ErrorCode.HasValue) return null; - // get latest versions - ISemanticVersion latest = null; - ISemanticVersion optional = null; + // get files + var downloads = new List(); foreach (FileDataModel file in mod.Files) { if (file.IsOld || file.IsDeleted || file.IsHidden) continue; - if (!SemanticVersion.TryParse(file.Version, out ISemanticVersion version)) - continue; - - if (file.IsDefault) - { - if (latest == null || version.IsNewerThan(latest)) - latest = version; - } - else if (optional == null || version.IsNewerThan(optional)) - optional = version; + downloads.Add( + new GenericModDownload(file.Name, file.Description, file.Version) + ); } - if (latest == null) - { - latest = optional; - optional = null; - } - if (optional != null && latest.IsNewerThan(optional)) - optional = null; - // generate result - return new ModDropMod - { - Name = mod.Mod?.Title, - LatestDefaultVersion = latest, - LatestOptionalVersion = optional, - Url = string.Format(this.ModUrlFormat, id) - }; + // return info + string name = mod.Mod?.Title; + string url = string.Format(this.ModUrlFormat, id); + return page.SetInfo(name: name, version: null, url: url, downloads: downloads); } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs deleted file mode 100644 index def79106..00000000 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropMod.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.Clients.ModDrop -{ - /// Mod metadata from the ModDrop API. - internal class ModDropMod - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The latest default file version. - public ISemanticVersion LatestDefaultVersion { get; set; } - - /// The latest optional file version. - public ISemanticVersion LatestOptionalVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs index fa84b287..b01196f4 100644 --- a/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs +++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ResponseModels/FileDataModel.cs @@ -1,8 +1,21 @@ +using Newtonsoft.Json; + namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels { /// Metadata from the ModDrop API about a mod file. public class FileDataModel { + /// The file title. + [JsonProperty("title")] + public string Name { get; set; } + + /// The file description. + [JsonProperty("desc")] + public string Description { get; set; } + + /// The file version. + public string Version { get; set; } + /// Whether the file is deleted. public bool IsDeleted { get; set; } @@ -14,8 +27,5 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop.ResponseModels /// Whether this is an archived file. public bool IsOld { get; set; } - - /// The file version. - public string Version { get; set; } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs index e56e7af4..a44b8c66 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { /// An HTTP client for fetching mod metadata from Nexus Mods. - internal interface INexusClient : IDisposable - { - /********* - ** Methods - *********/ - /// Get metadata about a mod. - /// The Nexus mod ID. - /// Returns the mod info if found, else null. - Task GetModAsync(uint id); - } + internal interface INexusClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 753d3b4f..ef3ef22e 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -7,6 +7,8 @@ using HtmlAgilityPack; using Pathoschild.FluentNexus.Models; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels; using FluentNexusClient = Pathoschild.FluentNexus.NexusClient; namespace StardewModdingAPI.Web.Framework.Clients.Nexus @@ -30,6 +32,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus private readonly FluentNexusClient ApiClient; + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.Nexus; + + /********* ** Public methods *********/ @@ -48,20 +57,32 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion); } - /// Get metadata about a mod. - /// The Nexus mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(uint id) + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + // Fetch from the Nexus website when possible, since it has no rate limits. Mods with // adult content are hidden for anonymous users, so fall back to the API in that case. // Note that the API has very restrictive rate limits which means we can't just use it // for all cases. - NexusMod mod = await this.GetModFromWebsiteAsync(id); + NexusMod mod = await this.GetModFromWebsiteAsync(parsedId); if (mod?.Status == NexusModStatus.AdultContentForbidden) - mod = await this.GetModFromApiAsync(id); + mod = await this.GetModFromApiAsync(parsedId); + + // page doesn't exist + if (mod == null || mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); - return mod; + // return info + page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads); + if (mod.Status != NexusModStatus.Ok) + page.SetError(RemoteModStatus.TemporaryError, mod.Error); + return page; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -115,37 +136,28 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus // extract mod info string url = this.GetModUrl(id); - string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); + string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); - // extract file versions - List rawVersions = new List(); + // extract files + var downloads = new List(); foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) { string sectionName = fileSection.Descendants("h2").First().InnerText; if (sectionName != "Main files" && sectionName != "Optional files") continue; - rawVersions.AddRange( - from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version")) - from versionStat in statBox.Descendants().Where(p => p.HasClass("stat")) - select versionStat.InnerText.Trim() - ); - } - - // choose latest file version - ISemanticVersion latestFileVersion = null; - foreach (string rawVersion in rawVersions) - { - if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) - continue; - if (parsedVersion != null && !cur.IsNewerThan(parsedVersion)) - continue; - if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) - continue; + foreach (var container in fileSection.Descendants("dt")) + { + string fileName = container.GetDataAttribute("name").Value; + string fileVersion = container.GetDataAttribute("version").Value; + string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 - latestFileVersion = cur; + downloads.Add( + new GenericModDownload(fileName, description, fileVersion) + ); + } } // yield info @@ -153,8 +165,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus { Name = name, Version = parsedVersion?.ToString() ?? version, - LatestFileVersion = latestFileVersion, - Url = url + Url = url, + Downloads = downloads.ToArray() }; } @@ -167,29 +179,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus Mod mod = await this.ApiClient.Mods.GetMod("stardewvalley", (int)id); ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional); - // get versions - if (!SemanticVersion.TryParse(mod.Version, out ISemanticVersion mainVersion)) - mainVersion = null; - ISemanticVersion latestFileVersion = null; - foreach (string rawVersion in files.Files.Select(p => p.FileVersion)) - { - if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) - continue; - if (mainVersion != null && !cur.IsNewerThan(mainVersion)) - continue; - if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) - continue; - - latestFileVersion = cur; - } - // yield info return new NexusMod { Name = mod.Name, Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, - LatestFileVersion = latestFileVersion, - Url = this.GetModUrl(id) + Url = this.GetModUrl(id), + Downloads = files.Files + .Select(file => (IModDownload)new GenericModDownload(file.Name, null, file.FileVersion)) + .ToArray() }; } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs deleted file mode 100644 index 0f1b29d5..00000000 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Web.Framework.Clients.Nexus -{ - /// Mod metadata from Nexus Mods. - internal class NexusMod - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The latest file version. - public ISemanticVersion LatestFileVersion { get; set; } - - /// The mod's web URL. - [JsonProperty("mod_page_uri")] - public string Url { get; set; } - - /// The mod's publication status. - [JsonIgnore] - public NexusModStatus Status { get; set; } = NexusModStatus.Ok; - - /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - [JsonIgnore] - public string Error { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs new file mode 100644 index 00000000..aef90ede --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels +{ + /// Mod metadata from Nexus Mods. + internal class NexusMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + + /// The mod's publication status. + [JsonIgnore] + public NexusModStatus Status { get; set; } = NexusModStatus.Ok; + + /// The files available to download. + [JsonIgnore] + public IModDownload[] Downloads { get; set; } + + /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). + [JsonIgnore] + public string Error { get; set; } + } +} diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs index ad7e645a..3a246245 100644 --- a/src/SMAPI.Web/Framework/Extensions.cs +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -13,6 +13,12 @@ namespace StardewModdingAPI.Web.Framework /// Provides extensions on ASP.NET Core types. public static class Extensions { + /********* + ** Public methods + *********/ + /**** + ** View helpers + ****/ /// Get a URL with the absolute path for an action method. Unlike , only the specified are added to the URL without merging values from the current HTTP request. /// The URL helper to extend. /// The name of the action method. diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs new file mode 100644 index 00000000..dc058bcb --- /dev/null +++ b/src/SMAPI.Web/Framework/IModDownload.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Web.Framework +{ + /// Generic metadata about a file download on a mod page. + internal interface IModDownload + { + /// The download's display name. + string Name { get; } + + /// The download's description. + string Description { get; } + + /// The download's file version. + string Version { get; } + } +} diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs new file mode 100644 index 00000000..e66d401f --- /dev/null +++ b/src/SMAPI.Web/Framework/IModPage.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using StardewModdingAPI.Toolkit.Framework.UpdateData; + +namespace StardewModdingAPI.Web.Framework +{ + /// Generic metadata about a mod page. + internal interface IModPage + { + /********* + ** Accessors + *********/ + /// The mod site containing the mod. + ModSiteKey Site { get; } + + /// The mod's unique ID within the site. + string Id { get; } + + /// The mod name. + string Name { get; } + + /// The mod's semantic version number. + string Version { get; } + + /// The mod's web URL. + string Url { get; } + + /// The mod downloads. + IModDownload[] Downloads { get; } + + /// The mod page status. + RemoteModStatus Status { get; } + + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + string Error { get; } + + + /********* + ** Methods + *********/ + /// Set the fetched mod info. + /// The mod name. + /// The mod's semantic version number. + /// The mod's web URL. + /// The mod downloads. + IModPage SetInfo(string name, string version, string url, IEnumerable downloads); + + /// Set a mod fetch error. + /// The mod availability status on the remote site. + /// A user-friendly error which indicates why fetching the mod info failed (if applicable). + IModPage SetError(RemoteModStatus status, string error); + } +} diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs new file mode 100644 index 00000000..7845b8c5 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModInfoModel.cs @@ -0,0 +1,81 @@ +using StardewModdingAPI.Web.Framework.Clients; + +namespace StardewModdingAPI.Web.Framework +{ + /// Generic metadata about a mod. + internal class ModInfoModel + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's latest version. + public ISemanticVersion Version { get; set; } + + /// The mod's latest optional or prerelease version, if newer than . + public ISemanticVersion PreviewVersion { get; set; } + + /// The mod's web URL. + public string Url { get; set; } + + /// The mod availability status on the remote site. + public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; + + /// The error message indicating why the mod is invalid (if applicable). + public string Error { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModInfoModel() { } + + /// Construct an instance. + /// The mod name. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + /// The mod's web URL. + public ModInfoModel(string name, ISemanticVersion version, string url, ISemanticVersion previewVersion = null) + { + this + .SetBasicInfo(name, url) + .SetVersions(version, previewVersion); + } + + /// Set the basic mod info. + /// The mod name. + /// The mod's web URL. + public ModInfoModel SetBasicInfo(string name, string url) + { + this.Name = name; + this.Url = url; + + return this; + } + + /// Set the mod version info. + /// The semantic version for the mod's latest release. + /// The semantic version for the mod's latest preview release, if available and different from . + public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion previewVersion = null) + { + this.Version = version; + this.PreviewVersion = previewVersion; + + return this; + } + + /// Set a mod error. + /// The mod availability status on the remote site. + /// The error message indicating why the mod is invalid (if applicable). + public ModInfoModel SetError(RemoteModStatus status, string error) + { + this.Status = status; + this.Error = error; + + return this; + } + } +} diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs deleted file mode 100644 index f9f9f47d..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - internal abstract class RepositoryBase : IModRepository - { - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - public ModRepositoryKey VendorKey { get; } - - - /********* - ** Public methods - *********/ - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public abstract void Dispose(); - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public abstract Task GetModInfoAsync(string id); - - - /********* - ** Protected methods - *********/ - /// Construct an instance. - /// The unique key for this vendor. - protected RepositoryBase(ModRepositoryKey vendorKey) - { - this.VendorKey = vendorKey; - } - - /// Normalize a version string. - /// The version to normalize. - protected string NormalizeVersion(string version) - { - if (string.IsNullOrWhiteSpace(version)) - return null; - - version = version.Trim(); - if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix - version = version.Substring(1); - - return version; - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs deleted file mode 100644 index 0945735a..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients.Chucklefish; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from the Chucklefish mod site. - internal class ChucklefishRepository : RepositoryBase - { - /********* - ** Fields - *********/ - /// The underlying HTTP client. - private readonly IChucklefishClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying HTTP client. - public ChucklefishRepository(IChucklefishClient client) - : base(ModRepositoryKey.Chucklefish) - { - this.Client = client; - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!uint.TryParse(id, out uint realID)) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID."); - - // fetch info - try - { - var mod = await this.Client.GetModAsync(realID); - return mod != null - ? new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), url: mod.Url) - : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID."); - } - catch (Exception ex) - { - return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs deleted file mode 100644 index 93ddc1eb..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/CurseForgeRepository.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients.CurseForge; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from CurseForge. - internal class CurseForgeRepository : RepositoryBase - { - /********* - ** Fields - *********/ - /// The underlying CurseForge API client. - private readonly ICurseForgeClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying CurseForge API client. - public CurseForgeRepository(ICurseForgeClient client) - : base(ModRepositoryKey.CurseForge) - { - this.Client = client; - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!uint.TryParse(id, out uint curseID)) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid CurseForge mod ID, must be an integer ID."); - - // fetch info - try - { - CurseForgeMod mod = await this.Client.GetModAsync(curseID); - if (mod == null) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no CurseForge mod with this ID."); - if (mod.Error != null) - { - RemoteModStatus remoteStatus = RemoteModStatus.InvalidData; - return new ModInfoModel().SetError(remoteStatus, mod.Error); - } - - return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.LatestVersion), url: mod.Url); - } - catch (Exception ex) - { - return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs deleted file mode 100644 index c62cb73f..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients.GitHub; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from GitHub project releases. - internal class GitHubRepository : RepositoryBase - { - /********* - ** Fields - *********/ - /// The underlying GitHub API client. - private readonly IGitHubClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying GitHub API client. - public GitHubRepository(IGitHubClient client) - : base(ModRepositoryKey.GitHub) - { - this.Client = client; - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - ModInfoModel result = new ModInfoModel().SetBasicInfo(id, $"https://github.com/{id}/releases"); - - // validate ID format - if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase)) - return result.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'."); - - // fetch info - try - { - // fetch repo info - GitRepo repository = await this.Client.GetRepositoryAsync(id); - if (repository == null) - return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID."); - result - .SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases") - .SetLicense(url: repository.License?.Url, name: repository.License?.SpdxId ?? repository.License?.Name); - - // get latest release (whether preview or stable) - GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true); - if (latest == null) - return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID."); - - // split stable/prerelease if applicable - GitRelease preview = null; - if (latest.IsPrerelease) - { - GitRelease release = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false); - if (release != null) - { - preview = latest; - latest = release; - } - } - - // return data - return result.SetVersions(version: this.NormalizeVersion(latest.Tag), previewVersion: this.NormalizeVersion(preview?.Tag)); - } - catch (Exception ex) - { - return result.SetError(RemoteModStatus.TemporaryError, ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs deleted file mode 100644 index 68f754ae..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// A repository which provides mod metadata. - internal interface IModRepository : IDisposable - { - /********* - ** Accessors - *********/ - /// The unique key for this vendor. - ModRepositoryKey VendorKey { get; } - - - /********* - ** Public methods - *********/ - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - Task GetModInfoAsync(string id); - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs deleted file mode 100644 index 62142668..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/ModDropRepository.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients.ModDrop; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from the ModDrop API. - internal class ModDropRepository : RepositoryBase - { - /********* - ** Fields - *********/ - /// The underlying ModDrop API client. - private readonly IModDropClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying Nexus Mods API client. - public ModDropRepository(IModDropClient client) - : base(ModRepositoryKey.ModDrop) - { - this.Client = client; - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!long.TryParse(id, out long modDropID)) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID."); - - // fetch info - try - { - ModDropMod mod = await this.Client.GetModAsync(modDropID); - return mod != null - ? new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url) - : new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID."); - } - catch (Exception ex) - { - return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs deleted file mode 100644 index 46b98860..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// Generic metadata about a mod. - internal class ModInfoModel - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's latest version. - public string Version { get; set; } - - /// The mod's latest optional or prerelease version, if newer than . - public string PreviewVersion { get; set; } - - /// The mod's web URL. - public string Url { get; set; } - - /// The license URL, if available. - public string LicenseUrl { get; set; } - - /// The license name, if available. - public string LicenseName { get; set; } - - /// The mod availability status on the remote site. - public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok; - - /// The error message indicating why the mod is invalid (if applicable). - public string Error { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModInfoModel() { } - - /// Construct an instance. - /// The mod name. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - /// The mod's web URL. - public ModInfoModel(string name, string version, string url, string previewVersion = null) - { - this - .SetBasicInfo(name, url) - .SetVersions(version, previewVersion); - } - - /// Set the basic mod info. - /// The mod name. - /// The mod's web URL. - public ModInfoModel SetBasicInfo(string name, string url) - { - this.Name = name; - this.Url = url; - - return this; - } - - /// Set the mod version info. - /// The semantic version for the mod's latest release. - /// The semantic version for the mod's latest preview release, if available and different from . - public ModInfoModel SetVersions(string version, string previewVersion = null) - { - this.Version = version; - this.PreviewVersion = previewVersion; - - return this; - } - - /// Set the license info, if available. - /// The license URL. - /// The license name. - public ModInfoModel SetLicense(string url, string name) - { - this.LicenseUrl = url; - this.LicenseName = name; - - return this; - } - - /// Set a mod error. - /// The mod availability status on the remote site. - /// The error message indicating why the mod is invalid (if applicable). - public ModInfoModel SetError(RemoteModStatus status, string error) - { - this.Status = status; - this.Error = error; - - return this; - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs deleted file mode 100644 index 9551258c..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Threading.Tasks; -using StardewModdingAPI.Toolkit.Framework.UpdateData; -using StardewModdingAPI.Web.Framework.Clients.Nexus; - -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// An HTTP client for fetching mod metadata from Nexus Mods. - internal class NexusRepository : RepositoryBase - { - /********* - ** Fields - *********/ - /// The underlying Nexus Mods API client. - private readonly INexusClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The underlying Nexus Mods API client. - public NexusRepository(INexusClient client) - : base(ModRepositoryKey.Nexus) - { - this.Client = client; - } - - /// Get metadata about a mod in the repository. - /// The mod ID in this repository. - public override async Task GetModInfoAsync(string id) - { - // validate ID format - if (!uint.TryParse(id, out uint nexusID)) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); - - // fetch info - try - { - NexusMod mod = await this.Client.GetModAsync(nexusID); - if (mod == null) - return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); - if (mod.Error != null) - { - RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished - ? RemoteModStatus.DoesNotExist - : RemoteModStatus.TemporaryError; - return new ModInfoModel().SetError(remoteStatus, mod.Error); - } - - return new ModInfoModel(name: mod.Name, version: this.NormalizeVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url); - } - catch (Exception ex) - { - return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString()); - } - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public override void Dispose() - { - this.Client.Dispose(); - } - } -} diff --git a/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs b/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs deleted file mode 100644 index 02876556..00000000 --- a/src/SMAPI.Web/Framework/ModRepositories/RemoteModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Web.Framework.ModRepositories -{ - /// The mod availability status on a remote site. - internal enum RemoteModStatus - { - /// The mod is valid. - Ok, - - /// The mod data was fetched, but the data is not valid (e.g. version isn't semantic). - InvalidData, - - /// The mod does not exist. - DoesNotExist, - - /// The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred). - TemporaryError - } -} diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs new file mode 100644 index 00000000..eaae7935 --- /dev/null +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients; + +namespace StardewModdingAPI.Web.Framework +{ + /// Handles fetching data from mod sites. + internal class ModSiteManager + { + /********* + ** Fields + *********/ + /// The mod sites which provide mod metadata. + private readonly IDictionary ModSites; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod sites which provide mod metadata. + public ModSiteManager(IModSiteClient[] modSites) + { + this.ModSites = modSites.ToDictionary(p => p.SiteKey); + } + + /// Get the mod info for an update key. + /// The namespaced update key. + public async Task GetModPageAsync(UpdateKey updateKey) + { + // get site + if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient client)) + return new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Site}'. Expected one of [{string.Join(", ", this.ModSites.Keys)}]."); + + // fetch mod + IModPage mod; + try + { + mod = await client.GetModData(updateKey.ID); + } + catch (Exception ex) + { + mod = new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.TemporaryError, ex.ToString()); + } + + // handle errors + return mod ?? new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"Found no {updateKey.Site} mod with ID '{updateKey.ID}'."); + } + + /// Parse version info for the given mod page info. + /// The mod page info. + /// Maps remote versions to a semantic version for update checks. + /// Whether to allow non-standard versions. + public ModInfoModel GetPageVersions(IModPage page, bool allowNonStandardVersions, IDictionary mapRemoteVersions) + { + // get base model + ModInfoModel model = new ModInfoModel() + .SetBasicInfo(page.Name, page.Url) + .SetError(page.Status, page.Error); + if (page.Status != RemoteModStatus.Ok) + return model; + + // fetch versions + if (!this.TryGetLatestVersions(page, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion)) + return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions."); + + // return info + return model.SetVersions(mainVersion, previewVersion); + } + + /// Get a semantic local version for update checks. + /// The version to parse. + /// A map of version replacements. + /// Whether to allow non-standard versions. + public ISemanticVersion GetMappedVersion(string version, IDictionary map, bool allowNonStandard) + { + // try mapped version + string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard); + if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew)) + return parsedNew; + + // return original version + return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld) + ? parsedOld + : null; + } + + + /********* + ** Private methods + *********/ + /// Get the mod version numbers for the given mod. + /// The mod to check. + /// Whether to allow non-standard versions. + /// Maps remote versions to a semantic version for update checks. + /// The main mod version. + /// The latest prerelease version, if newer than . + private bool TryGetLatestVersions(IModPage mod, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) + { + main = null; + preview = null; + + ISemanticVersion ParseVersion(string raw) + { + raw = this.NormalizeVersion(raw); + return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions); + } + + if (mod != null) + { + // get versions + main = ParseVersion(mod.Version); + foreach (string rawVersion in mod.Downloads.Select(p => p.Version)) + { + ISemanticVersion cur = ParseVersion(rawVersion); + if (cur == null) + continue; + + if (main == null || cur.IsNewerThan(main)) + main = cur; + if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview))) + preview = cur; + } + + if (preview != null && !preview.IsNewerThan(main)) + preview = null; + } + + return main != null; + } + + /// Get a semantic local version for update checks. + /// The version to map. + /// A map of version replacements. + /// Whether to allow non-standard versions. + private string GetRawMappedVersion(string version, IDictionary map, bool allowNonStandard) + { + if (version == null || map == null || !map.Any()) + return version; + + // match exact raw version + if (map.ContainsKey(version)) + return map[version]; + + // match parsed version + if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed)) + { + if (map.ContainsKey(parsed.ToString())) + return map[parsed.ToString()]; + + foreach ((string fromRaw, string toRaw) in map) + { + if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion)) + return newVersion.ToString(); + } + } + + return version; + } + + /// Normalize a version string. + /// The version to normalize. + private string NormalizeVersion(string version) + { + if (string.IsNullOrWhiteSpace(version)) + return null; + + version = version.Trim(); + if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix + version = version.Substring(1); + + return version; + } + } +} diff --git a/src/SMAPI.Web/Framework/RemoteModStatus.cs b/src/SMAPI.Web/Framework/RemoteModStatus.cs new file mode 100644 index 00000000..139ecfd3 --- /dev/null +++ b/src/SMAPI.Web/Framework/RemoteModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Web.Framework +{ + /// The mod availability status on a remote site. + internal enum RemoteModStatus + { + /// The mod is valid. + Ok, + + /// The mod data was fetched, but the data is not valid (e.g. version isn't semantic). + InvalidData, + + /// The mod does not exist. + DoesNotExist, + + /// The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred). + TemporaryError + } +} -- cgit From d97b11060c310f1fa6064f02496e6a6162a44573 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 May 2020 00:21:51 -0400 Subject: add update subkeys --- docs/release-notes.md | 1 + .../Framework/UpdateData/UpdateKey.cs | 51 +++++++++++++++++----- src/SMAPI.Web/Controllers/ModsApiController.cs | 48 ++++++++++++-------- src/SMAPI.Web/Framework/ModSiteManager.cs | 28 +++++++++--- src/SMAPI.sln.DotSettings | 1 + 5 files changed, 92 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 894fd562..57d32fd7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,6 +12,7 @@ * For modders: * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index f6044148..7e4d0220 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -17,6 +17,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The mod ID within the repository. public string ID { get; } + /// If specified, a substring in download names/descriptions to match. + public string Subkey { get; } + /// Whether the update key seems to be valid. public bool LooksValid { get; } @@ -28,11 +31,13 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// The raw update key text. /// The mod site containing the mod. /// The mod ID within the site. - public UpdateKey(string rawText, ModSiteKey site, string id) + /// If specified, a substring in download names/descriptions to match. + public UpdateKey(string rawText, ModSiteKey site, string id, string subkey) { this.RawText = rawText?.Trim(); this.Site = site; this.ID = id?.Trim(); + this.Subkey = subkey?.Trim(); this.LooksValid = site != ModSiteKey.Unknown && !string.IsNullOrWhiteSpace(id); @@ -41,8 +46,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// Construct an instance. /// The mod site containing the mod. /// The mod ID within the site. - public UpdateKey(ModSiteKey site, string id) - : this(UpdateKey.GetString(site, id), site, id) { } + /// If specified, a substring in download names/descriptions to match. + public UpdateKey(ModSiteKey site, string id, string subkey) + : this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { } /// Parse a raw update key. /// The raw update key to parse. @@ -54,7 +60,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { string[] parts = raw?.Trim().Split(':'); if (parts == null || parts.Length != 2) - return new UpdateKey(raw, ModSiteKey.Unknown, null); + return new UpdateKey(raw, ModSiteKey.Unknown, null, null); rawSite = parts[0].Trim(); id = parts[1].Trim(); @@ -62,20 +68,32 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData if (string.IsNullOrWhiteSpace(id)) id = null; + // extract subkey + string subkey = null; + if (id != null) + { + string[] parts = id.Split('@'); + if (parts.Length == 2) + { + id = parts[0].Trim(); + subkey = $"@{parts[1]}".Trim(); + } + } + // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) - return new UpdateKey(raw, ModSiteKey.Unknown, id); + return new UpdateKey(raw, ModSiteKey.Unknown, id, subkey); if (id == null) - return new UpdateKey(raw, site, null); + return new UpdateKey(raw, site, null, subkey); - return new UpdateKey(raw, site, id); + return new UpdateKey(raw, site, id, subkey); } /// Get a string that represents the current object. public override string ToString() { return this.LooksValid - ? UpdateKey.GetString(this.Site, this.ID) + ? UpdateKey.GetString(this.Site, this.ID, this.Subkey) : this.RawText; } @@ -83,10 +101,18 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// An object to compare with this object. public bool Equals(UpdateKey other) { + if (!this.LooksValid) + { + return + other?.LooksValid == false + && this.RawText.Equals(other.RawText, StringComparison.OrdinalIgnoreCase); + } + return other != null && this.Site == other.Site - && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase); + && string.Equals(this.ID, other.ID, StringComparison.OrdinalIgnoreCase) + && string.Equals(this.Subkey, other.Subkey, StringComparison.OrdinalIgnoreCase); } /// Determines whether the specified object is equal to the current object. @@ -100,15 +126,16 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// A hash code for the current object. public override int GetHashCode() { - return $"{this.Site}:{this.ID}".ToLower().GetHashCode(); + return this.ToString().ToLower().GetHashCode(); } /// Get the string representation of an update key. /// The mod site containing the mod. /// The mod ID within the repository. - public static string GetString(ModSiteKey site, string id) + /// If specified, a substring in download names/descriptions to match. + public static string GetString(ModSiteKey site, string id, string subkey = null) { - return $"{site}:{id}".Trim(); + return $"{site}:{id}{subkey}".Trim(); } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 14be520d..028fc613 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -135,7 +135,7 @@ namespace StardewModdingAPI.Web.Controllers // validate update key if (!updateKey.LooksValid) { - errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); + errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'."); continue; } @@ -271,7 +271,7 @@ namespace StardewModdingAPI.Web.Controllers } // get version info - return this.ModSites.GetPageVersions(page, allowNonStandardVersions, mapRemoteVersions); + return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions); } /// Get update keys based on the available mod metadata, while maintaining the precedence order. @@ -280,44 +280,56 @@ namespace StardewModdingAPI.Web.Controllers /// The mod's entry in the wiki list. private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { + // get every update key (including duplicates) IEnumerable GetRaw() { // specified update keys if (specifiedKeys != null) { foreach (string key in specifiedKeys) - yield return key?.Trim(); + { + if (!string.IsNullOrWhiteSpace(key)) + yield return key.Trim(); + } } // default update key string defaultKey = record?.GetDefaultUpdateKey(); - if (defaultKey != null) + if (!string.IsNullOrWhiteSpace(defaultKey)) yield return defaultKey; // wiki metadata if (entry != null) { if (entry.NexusID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString()); if (entry.ModDropID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString()); if (entry.CurseForgeID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString()); if (entry.ChucklefishID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID?.ToString()); + yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString()); } } - HashSet seen = new HashSet(); - foreach (string rawKey in GetRaw()) - { - if (string.IsNullOrWhiteSpace(rawKey)) - continue; - - UpdateKey key = UpdateKey.Parse(rawKey); - if (seen.Add(key)) - yield return key; - } + // get unique update keys + var subkeyRoots = new HashSet(); + List updateKeys = GetRaw() + .Select(raw => + { + var key = UpdateKey.Parse(raw); + if (key.Subkey != null) + subkeyRoots.Add(new UpdateKey(key.Site, key.ID, null)); + return key; + }) + .Distinct() + .ToList(); + + // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority + if (subkeyRoots.Any()) + updateKeys.RemoveAll(subkeyRoots.Contains); + + return updateKeys; } } } diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs index eaae7935..68b4c6ac 100644 --- a/src/SMAPI.Web/Framework/ModSiteManager.cs +++ b/src/SMAPI.Web/Framework/ModSiteManager.cs @@ -54,9 +54,10 @@ namespace StardewModdingAPI.Web.Framework /// Parse version info for the given mod page info. /// The mod page info. + /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Maps remote versions to a semantic version for update checks. /// Whether to allow non-standard versions. - public ModInfoModel GetPageVersions(IModPage page, bool allowNonStandardVersions, IDictionary mapRemoteVersions) + public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions) { // get base model ModInfoModel model = new ModInfoModel() @@ -66,7 +67,10 @@ namespace StardewModdingAPI.Web.Framework return model; // fetch versions - if (!this.TryGetLatestVersions(page, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion)) + bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion mainVersion, out ISemanticVersion previewVersion); + if (!hasVersions && subkey != null) + hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion); + if (!hasVersions) return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions."); // return info @@ -96,11 +100,12 @@ namespace StardewModdingAPI.Web.Framework *********/ /// Get the mod version numbers for the given mod. /// The mod to check. + /// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.) /// Whether to allow non-standard versions. /// Maps remote versions to a semantic version for update checks. /// The main mod version. /// The latest prerelease version, if newer than . - private bool TryGetLatestVersions(IModPage mod, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) + private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview) { main = null; preview = null; @@ -113,14 +118,23 @@ namespace StardewModdingAPI.Web.Framework if (mod != null) { - // get versions - main = ParseVersion(mod.Version); - foreach (string rawVersion in mod.Downloads.Select(p => p.Version)) + // get mod version + if (subkey == null) + main = ParseVersion(mod.Version); + + // get file versions + foreach (IModDownload download in mod.Downloads) { - ISemanticVersion cur = ParseVersion(rawVersion); + // check for subkey if specified + if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true) + continue; + + // parse version + ISemanticVersion cur = ParseVersion(download.Version); if (cur == null) continue; + // track highest versions if (main == null || cur.IsNewerThan(main)) main = cur; if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview))) diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.sln.DotSettings index 556f1ec0..05caa938 100644 --- a/src/SMAPI.sln.DotSettings +++ b/src/SMAPI.sln.DotSettings @@ -58,6 +58,7 @@ True True True + True True True True -- cgit From bb9cde8f2e4737627668b75d55660816d7386f06 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 May 2020 00:31:15 -0400 Subject: ignore MacOS files starting with ._ --- docs/release-notes.md | 1 + src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 57d32fd7..10c31143 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming released * For players: * Mod warnings are now listed alphabetically. + * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * For the web UI: * Updated web framework to improve site performance and reliability. diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs index f11cc1a7..f4857c7d 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning { // OS metadata files new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager - new Regex(@"^(?:__MACOSX|\._\.DS_Store|\.DS_Store|mcs)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS + new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files -- cgit From ea96fdf54167608502885a64e844e8b67f1c1d72 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 May 2020 14:35:16 -0400 Subject: update ModDrop URLs --- docs/release-notes.md | 3 ++- src/SMAPI.Web/ViewModels/ModModel.cs | 2 +- src/SMAPI.Web/appsettings.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 10c31143..5813298d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,8 +7,9 @@ * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * For the web UI: - * Updated web framework to improve site performance and reliability. * Added GitHub licenses to mod compatibility list. + * Updated web framework to improve site performance and reliability. + * Updated ModDrop URLs. * Internal changes to improve performance and reliability. * For modders: diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 45b12397..575d596a 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -106,7 +106,7 @@ namespace StardewModdingAPI.Web.ViewModels if (entry.ModDropID.HasValue) { anyFound = true; - yield return new ModLinkModel($"https://www.moddrop.com/sdv/mod/{entry.ModDropID}", "ModDrop"); + yield return new ModLinkModel($"https://www.moddrop.com/stardew-valley/mod/{entry.ModDropID}", "ModDrop"); } if (!string.IsNullOrWhiteSpace(entry.CurseForgeKey)) { diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index b1d39a6f..22fd7396 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -39,7 +39,7 @@ "GitHubPassword": null, "ModDropApiUrl": "https://www.moddrop.com/api/mods/data", - "ModDropModPageUrl": "https://www.moddrop.com/sdv/mod/{0}", + "ModDropModPageUrl": "https://www.moddrop.com/stardew-valley/mod/{0}", "NexusApiKey": null, "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/", -- cgit From d9c2d242b9457a6517ec348c945ae1a324582492 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 24 May 2020 16:39:56 -0400 Subject: add update key overrides --- docs/release-notes.md | 1 + .../Framework/Clients/Wiki/WikiClient.cs | 2 + .../Framework/Clients/Wiki/WikiModEntry.cs | 3 + src/SMAPI.Web/Controllers/ModsApiController.cs | 103 +++++++++++++-------- 4 files changed, 69 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5813298d..90835fa3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,6 +16,7 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. + * Added ability to override update keys from the compatibility list. * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index c829c0f4..34e2e1b8 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -105,6 +105,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string pullRequestUrl = this.GetAttribute(node, "data-pr"); IDictionary mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions"); IDictionary mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions"); + string[] changeUpdateKeys = this.GetAttributeAsCsv(node, "data-change-update-keys"); // parse stable compatibility WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo @@ -153,6 +154,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Warnings = warnings, PullRequestUrl = pullRequestUrl, DevNote = devNote, + ChangeUpdateKeys = changeUpdateKeys, MapLocalVersions = mapLocalVersions, MapRemoteVersions = mapRemoteVersions, Anchor = anchor diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 474dce3d..21466c6a 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -63,6 +63,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } + /// Update keys to add (optionally prefixed by '+') or remove (prefixed by '-'). + public string[] ChangeUpdateKeys { get; set; } + /// Maps local versions to a semantic version for update checks. public IDictionary MapLocalVersions { get; set; } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 028fc613..db669bf9 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -280,56 +280,79 @@ namespace StardewModdingAPI.Web.Controllers /// The mod's entry in the wiki list. private IEnumerable GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) { - // get every update key (including duplicates) - IEnumerable GetRaw() + // get unique update keys + List updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry) + .Select(UpdateKey.Parse) + .Distinct() + .ToList(); + + // apply remove overrides from wiki + { + var removeKeys = new HashSet( + from key in entry?.ChangeUpdateKeys ?? new string[0] + where key.StartsWith('-') + select UpdateKey.Parse(key.Substring(1)) + ); + if (removeKeys.Any()) + updateKeys.RemoveAll(removeKeys.Contains); + } + + // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority { - // specified update keys - if (specifiedKeys != null) + var removeKeys = new HashSet(); + foreach (var key in updateKeys) { - foreach (string key in specifiedKeys) - { - if (!string.IsNullOrWhiteSpace(key)) - yield return key.Trim(); - } + if (key.Subkey != null) + removeKeys.Add(new UpdateKey(key.Site, key.ID, null)); } + if (removeKeys.Any()) + updateKeys.RemoveAll(removeKeys.Contains); + } + + return updateKeys; + } + + /// Get every available update key based on the available mod metadata, including duplicates and keys which should be filtered. + /// The specified update keys. + /// The mod's entry in SMAPI's internal database. + /// The mod's entry in the wiki list. + private IEnumerable GetUnfilteredUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry) + { + // specified update keys + foreach (string key in specifiedKeys ?? Array.Empty()) + { + if (!string.IsNullOrWhiteSpace(key)) + yield return key.Trim(); + } - // default update key + // default update key + { string defaultKey = record?.GetDefaultUpdateKey(); if (!string.IsNullOrWhiteSpace(defaultKey)) yield return defaultKey; - - // wiki metadata - if (entry != null) - { - if (entry.NexusID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString()); - if (entry.ModDropID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString()); - if (entry.CurseForgeID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString()); - if (entry.ChucklefishID.HasValue) - yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString()); - } } - // get unique update keys - var subkeyRoots = new HashSet(); - List updateKeys = GetRaw() - .Select(raw => - { - var key = UpdateKey.Parse(raw); - if (key.Subkey != null) - subkeyRoots.Add(new UpdateKey(key.Site, key.ID, null)); - return key; - }) - .Distinct() - .ToList(); - - // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority - if (subkeyRoots.Any()) - updateKeys.RemoveAll(subkeyRoots.Contains); + // wiki metadata + if (entry != null) + { + if (entry.NexusID.HasValue) + yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString()); + if (entry.ModDropID.HasValue) + yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString()); + if (entry.CurseForgeID.HasValue) + yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString()); + if (entry.ChucklefishID.HasValue) + yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString()); + } - return updateKeys; + // overrides from wiki + foreach (string key in entry?.ChangeUpdateKeys ?? Array.Empty()) + { + if (key.StartsWith('+')) + yield return key.Substring(1); + else if (!key.StartsWith("-")) + yield return key; + } } } } -- cgit From 01b6e4ac3252a3ac8aa8046b5b97000fce13d576 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 May 2020 00:44:26 -0400 Subject: fix BadImageFormatException error handling Thanks to mouse for pointing it out! --- docs/release-notes.md | 4 ++-- src/SMAPI/Program.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 90835fa3..c9e16db8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,10 +5,11 @@ * For players: * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. + * Simplified paranoid warning logs and reduced their log level. + * Fixed `BadImageFormatException` error detection. * For the web UI: * Added GitHub licenses to mod compatibility list. - * Updated web framework to improve site performance and reliability. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. @@ -22,7 +23,6 @@ * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. * Fixed rewriting generic types to method references. - * Simplified paranoid warnings in the log and reduced their log level. * Fixed asset propagation for Gil's portraits. * Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 715c8553..9438f11e 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI Program.AssertGameVersion(); Program.Start(args); } - catch (BadImageFormatException ex) when (ex.FileName == "StardewValley") + catch (BadImageFormatException ex) when (ex.FileName == "StardewValley" || ex.FileName == "Stardew Valley") // NOTE: don't use StardewModdingAPI.Constants here, assembly resolution isn't hooked up at this point { string executableName = Program.GetExecutableAssemblyName(); Console.WriteLine($"SMAPI failed to initialize because your game's {executableName}.exe seems to be invalid.\nThis may be a pirated version which modified the executable in an incompatible way; if so, you can try a different download or buy a legitimate version.\n\nTechnical details:\n{ex}"); -- cgit From f817039a3a71e6e67c3bebb5e7fbd189c9a5da87 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Fri, 29 May 2020 14:25:01 +0800 Subject: Bug fix: make it possible for multi patch(When one patch replace an instruction, another patch didn't aware it due to the variable capture) --- src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 3 ++- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index eadb2997..dbb5f696 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using Mono.Cecil; +using Mono.Cecil.Cil; using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.ModLoading.Framework; using StardewModdingAPI.Metadata; @@ -304,7 +305,7 @@ namespace StardewModdingAPI.Framework.ModLoading rewritten |= handler.Handle(module, type, replaceWith); return rewritten; }, - rewriteInstruction: (instruction, cil, replaceWith) => + rewriteInstruction: (ref Instruction instruction, ILProcessor cil, Action replaceWith) => { bool rewritten = false; foreach (IInstructionHandler handler in handlers) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index a0f075bd..c774c038 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// The CIL instruction processor. /// Replaces the CIL instruction with the given instruction. /// Returns whether the instruction was changed. - public delegate bool RewriteInstructionDelegate(Instruction instruction, ILProcessor cil, Action replaceWith); + public delegate bool RewriteInstructionDelegate(ref Instruction instruction, ILProcessor cil, Action replaceWith); /********* @@ -144,7 +144,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework // instruction itself // (should be done after the above type rewrites to ensure valid types) - rewritten |= this.RewriteInstructionImpl(instruction, cil, newInstruction => + rewritten |= this.RewriteInstructionImpl(ref instruction, cil, newInstruction => { rewritten = true; cil.Replace(instruction, newInstruction); -- cgit From 7bbbef7687d958a351b5491e2bb20116c23c31bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 30 May 2020 22:45:43 -0400 Subject: simplify harmony_search output, tweak SMAPI's Harmony ID --- .../Framework/Commands/HarmonySummaryCommand.cs | 33 +++++++++++----------- src/SMAPI/Framework/Patching/GamePatcher.cs | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index bc8f4aa2..08233feb 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Framework.Commands /// Writes messages to the console. public void HandleCommand(string[] args, IMonitor monitor) { - SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.Method).ToArray(); + SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).ToArray(); StringBuilder result = new StringBuilder(); @@ -40,18 +40,19 @@ namespace StardewModdingAPI.Framework.Commands result.AppendLine(); foreach (var match in matches) { - result.AppendLine($" {match.Method}"); - foreach (var ownerGroup in match.PatchTypesByOwner) + 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(), LogLevel.Info); + monitor.Log(result.ToString().TrimEnd(), LogLevel.Info); } @@ -63,15 +64,12 @@ namespace StardewModdingAPI.Framework.Commands private IEnumerable FilterPatches(string[] searchTerms) { bool hasSearch = searchTerms.Any(); - bool IsMatch(string target) => searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); + bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); foreach (var patch in this.GetAllPatches()) { - if (!hasSearch) - yield return patch; - // matches entire patch - if (IsMatch(patch.Method)) + if (IsMatch(patch.MethodDescription)) { yield return patch; continue; @@ -95,7 +93,6 @@ namespace StardewModdingAPI.Framework.Commands foreach (MethodBase method in Harmony.GetAllPatchedMethods()) { // get metadata for method - string methodLabel = method.FullDescription(); HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); IDictionary> patchGroups = new Dictionary> { @@ -118,7 +115,7 @@ namespace StardewModdingAPI.Framework.Commands } // create search result - yield return new SearchResult(methodLabel, typesByOwner); + yield return new SearchResult(method, typesByOwner); } } @@ -144,8 +141,11 @@ namespace StardewModdingAPI.Framework.Commands /********* ** Accessors *********/ - /// A detailed human-readable label for the patched method. - public string Method { get; } + /// A simple human-readable name for the patched method. + public string MethodName { get; } + + /// A detailed description for the patched method. + public string MethodDescription { get; } /// The patch types by the Harmony instance ID that added them. public IDictionary> PatchTypesByOwner { get; } @@ -155,11 +155,12 @@ namespace StardewModdingAPI.Framework.Commands ** Public methods *********/ /// Construct an instance. - /// A detailed human-readable label for the patched method. + /// The patched method. /// The patch types by the Harmony instance ID that added them. - public SearchResult(string method, IDictionary> patchTypesByOwner) + public SearchResult(MethodBase method, IDictionary> patchTypesByOwner) { - this.Method = method; + this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}"; + this.MethodDescription = method.FullDescription(); this.PatchTypesByOwner = patchTypesByOwner; } } diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index cdb54453..dcad285a 100644 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -27,7 +27,7 @@ namespace StardewModdingAPI.Framework.Patching /// The patches to apply. public void Apply(params IHarmonyPatch[] patches) { - Harmony harmony = new Harmony("io.smapi"); + Harmony harmony = new Harmony("SMAPI"); foreach (IHarmonyPatch patch in patches) { try -- cgit From 10367a3eae27ccd668986d07522102a141c9ac31 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 31 May 2020 17:41:17 -0400 Subject: update TMXTile to fix void map issue on Android --- docs/release-notes.md | 1 + src/SMAPI/SMAPI.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index c9e16db8..dcb4a485 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * Simplified paranoid warning logs and reduced their log level. * Fixed `BadImageFormatException` error detection. + * Fixed black maps on Android for mods which use `.tmx` files. * For the web UI: * Added GitHub licenses to mod compatibility list. diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 1755b9e7..d36d7b4c 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit From 4fad1f8c0c0fc7ed313c2832ca89cd24fa276569 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Tue, 2 Jun 2020 18:53:09 +0800 Subject: 1.Multithread rewrite --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index c774c038..aafdefc6 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -57,12 +57,11 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - bool anyRewritten = false; - - foreach (TypeDefinition type in this.Module.GetTypes()) + return this.Module.GetTypes().AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism).Select(type => { + bool anyRewritten = false; if (type.BaseType == null) - continue; // special type like + return false; // special type like anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); @@ -108,9 +107,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } } } - } - return anyRewritten; + return anyRewritten; + }).Max(); } -- cgit From 73e3735dcd3b5789d4541e22988db9db2854a69f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 2 Jun 2020 22:05:00 -0400 Subject: undo parallel loop (#716) This caused errors during rewriting to be obfuscated with null reference exceptions. --- docs/release-notes.md | 1 - src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b2ef5a8d..dcb4a485 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,7 +6,6 @@ * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * Simplified paranoid warning logs and reduced their log level. - * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). * Fixed `BadImageFormatException` error detection. * Fixed black maps on Android for mods which use `.tmx` files. diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index aafdefc6..c774c038 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -57,11 +57,12 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - return this.Module.GetTypes().AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism).Select(type => + bool anyRewritten = false; + + foreach (TypeDefinition type in this.Module.GetTypes()) { - bool anyRewritten = false; if (type.BaseType == null) - return false; // special type like + continue; // special type like anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); @@ -107,9 +108,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } } } + } - return anyRewritten; - }).Max(); + return anyRewritten; } -- cgit From 6b4e52febbd69cc6b25735c96d89910d41e09d93 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Wed, 3 Jun 2020 10:56:31 +0800 Subject: Parallel exception aggregate fix --- .../ModLoading/Framework/RecursiveRewriter.cs | 101 ++++++++++++--------- 1 file changed, 58 insertions(+), 43 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index c774c038..c03376d6 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -57,60 +57,75 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - bool anyRewritten = false; - - foreach (TypeDefinition type in this.Module.GetTypes()) - { - if (type.BaseType == null) - continue; // special type like - - anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); - anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); - - foreach (InterfaceImplementation @interface in type.Interfaces) - anyRewritten |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); - - if (type.BaseType.FullName != "System.Object") - anyRewritten |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); - - foreach (MethodDefinition method in type.Methods) + Tuple aggregateResult = this.Module.GetTypes() + .AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism) + .Select(type => { - anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); - anyRewritten |= this.RewriteGenericParameters(method.GenericParameters); - anyRewritten |= this.RewriteCustomAttributes(method.CustomAttributes); + try + { + bool anyRewritten = false; + if (type.BaseType == null) + return new Tuple(anyRewritten, null); // special type like - foreach (ParameterDefinition parameter in method.Parameters) - anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); + anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); - foreach (var methodOverride in method.Overrides) - anyRewritten |= this.RewriteMethodReference(methodOverride); + foreach (InterfaceImplementation @interface in type.Interfaces) + anyRewritten |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); - if (method.HasBody) - { - foreach (VariableDefinition variable in method.Body.Variables) - anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + if (type.BaseType.FullName != "System.Object") + anyRewritten |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - Collection instructions = cil.Body.Instructions; - for (int i = 0; i < instructions.Count; i++) + foreach (MethodDefinition method in type.Methods) { - var instruction = instructions[i]; - if (instruction.OpCode.Code == Code.Nop) - continue; + anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); + anyRewritten |= this.RewriteGenericParameters(method.GenericParameters); + anyRewritten |= this.RewriteCustomAttributes(method.CustomAttributes); - anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + foreach (ParameterDefinition parameter in method.Parameters) + anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + + foreach (var methodOverride in method.Overrides) + anyRewritten |= this.RewriteMethodReference(methodOverride); + + if (method.HasBody) { - anyRewritten = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); + foreach (VariableDefinition variable in method.Body.Variables) + anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + Collection instructions = cil.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + if (instruction.OpCode.Code == Code.Nop) + continue; + + anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + { + anyRewritten = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + } + } } + + return new Tuple(anyRewritten, null); } - } + catch (Exception e) + { + return new Tuple(false, e.InnerException ?? e); + } + }) + .TakeWhile(tuple => tuple.Item2 == null) // Stop on any exception occurs + .Aggregate((tupleA, tupleB) => new Tuple(tupleA.Item1 | tupleB.Item1, tupleA.Item2 ?? tupleB.Item2)); // Aggregate result and exception + if (aggregateResult.Item2 != null) + { + throw aggregateResult.Item2; // rethrow inner Exception } - - return anyRewritten; + return aggregateResult.Item1; } -- cgit From 9b41397a019e6cc32b5c1ab502fa4abdb9a9ef49 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Wed, 3 Jun 2020 11:43:13 +0800 Subject: Remove take while op(does not return first none matched item) --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index c03376d6..579c3a08 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -119,7 +119,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework return new Tuple(false, e.InnerException ?? e); } }) - .TakeWhile(tuple => tuple.Item2 == null) // Stop on any exception occurs .Aggregate((tupleA, tupleB) => new Tuple(tupleA.Item1 | tupleB.Item1, tupleA.Item2 ?? tupleB.Item2)); // Aggregate result and exception if (aggregateResult.Item2 != null) { -- cgit From 8c4edc27656b7b60b4036a158c30f4fc1caccdd7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 3 Jun 2020 18:52:16 -0400 Subject: tweak new code, add release note (#718) --- docs/release-notes.md | 1 + .../ModLoading/Framework/RecursiveRewriter.cs | 31 ++++++++++++---------- 2 files changed, 18 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index dcb4a485..b2ef5a8d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * Simplified paranoid warning logs and reduced their log level. + * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). * Fixed `BadImageFormatException` error detection. * Fixed black maps on Android for mods which use `.tmx` files. diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 579c3a08..c4e6013e 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -57,16 +57,18 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - Tuple aggregateResult = this.Module.GetTypes() - .AsParallel().WithExecutionMode(ParallelExecutionMode.ForceParallelism) + // rewrite each type in the assembly, tracking whether any type was rewritten (Item1) + // and any exception that occurred during rewriting (Item2). + Tuple result = this.Module + .GetTypes() + .Where(type => type.BaseType != null) // skip special types like + .AsParallel() + .WithExecutionMode(ParallelExecutionMode.ForceParallelism) .Select(type => { + bool anyRewritten = false; try { - bool anyRewritten = false; - if (type.BaseType == null) - return new Tuple(anyRewritten, null); // special type like - anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); @@ -112,19 +114,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } } - return new Tuple(anyRewritten, null); + return Tuple.Create(anyRewritten, null as Exception); } catch (Exception e) { - return new Tuple(false, e.InnerException ?? e); + return Tuple.Create(anyRewritten, e); } }) - .Aggregate((tupleA, tupleB) => new Tuple(tupleA.Item1 | tupleB.Item1, tupleA.Item2 ?? tupleB.Item2)); // Aggregate result and exception - if (aggregateResult.Item2 != null) - { - throw aggregateResult.Item2; // rethrow inner Exception - } - return aggregateResult.Item1; + .Aggregate((a, b) => Tuple.Create(a.Item1 || b.Item1, a.Item2 ?? b.Item2)); + + bool rewritten = result.Item1; + Exception exception = result.Item2; + return exception == null + ? rewritten + : throw exception; } -- cgit From 80f882baf3f1854e32df3546fc4d4485c2aab68f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 3 Jun 2020 18:58:04 -0400 Subject: stop rewriting module at first error --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index c4e6013e..898d7fad 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -59,6 +60,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework { // rewrite each type in the assembly, tracking whether any type was rewritten (Item1) // and any exception that occurred during rewriting (Item2). + var cancellationToken = new CancellationTokenSource(); Tuple result = this.Module .GetTypes() .Where(type => type.BaseType != null) // skip special types like @@ -66,6 +68,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework .WithExecutionMode(ParallelExecutionMode.ForceParallelism) .Select(type => { + if (cancellationToken.IsCancellationRequested) + return Tuple.Create(false, null as Exception); + bool anyRewritten = false; try { @@ -118,6 +123,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } catch (Exception e) { + cancellationToken.Cancel(); return Tuple.Create(anyRewritten, e); } }) -- cgit From 43a9ee42aac1eaf2ff630d164bc425718a531a64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 3 Jun 2020 19:02:24 -0400 Subject: don't prevent mods from accessing game methods/properties extended by SMAPI --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ReflectionHelper.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b2ef5a8d..55339d57 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -25,6 +25,7 @@ * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. * Fixed rewriting generic types to method references. + * Fixed `helper.Reflection` blocking access to game methods/properties that were extended by SMAPI. * Fixed asset propagation for Gil's portraits. * Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. 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 the same property instance for convenience. private IReflectedProperty AssertAccessAllowed(IReflectedProperty 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 the same method instance for convenience. private IReflectedMethod AssertAccessAllowed(IReflectedMethod method) { - this.AssertAccessAllowed(method?.MethodInfo); + this.AssertAccessAllowed(method?.MethodInfo.GetBaseDefinition()); return method; } -- cgit From 2c9c4fbc65136e30ee5daa28a99bcaa665908d60 Mon Sep 17 00:00:00 2001 From: ZaneYork Date: Thu, 4 Jun 2020 18:29:27 +0800 Subject: Bug fix: Provide seed to Aggregate op(for Sequence contains no elements exception, all type was filtered by Where(type => type.BaseType != null) op) --- src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 898d7fad..47cc6508 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -127,7 +127,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework return Tuple.Create(anyRewritten, e); } }) - .Aggregate((a, b) => Tuple.Create(a.Item1 || b.Item1, a.Item2 ?? b.Item2)); + .Aggregate(Tuple.Create(false, null as Exception), (a, b) => Tuple.Create(a.Item1 || b.Item1, a.Item2 ?? b.Item2)); bool rewritten = result.Item1; Exception exception = result.Item2; -- cgit From 92aaf3fb8ac55ac36ba2a84854a26b5d25c7a135 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 4 Jun 2020 19:00:48 -0400 Subject: simplify parallel rewriting logic --- .../ModLoading/Framework/RecursiveRewriter.cs | 38 ++++++++-------------- 1 file changed, 14 insertions(+), 24 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 47cc6508..b304a732 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,6 +1,6 @@ using System; using System.Linq; -using System.Threading; +using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; @@ -58,20 +58,15 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - // rewrite each type in the assembly, tracking whether any type was rewritten (Item1) - // and any exception that occurred during rewriting (Item2). - var cancellationToken = new CancellationTokenSource(); - Tuple result = this.Module - .GetTypes() - .Where(type => type.BaseType != null) // skip special types like - .AsParallel() - .WithExecutionMode(ParallelExecutionMode.ForceParallelism) - .Select(type => + bool anyRewritten = false; + Exception exception = null; + Parallel.ForEach( + source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like + body: type => { - if (cancellationToken.IsCancellationRequested) - return Tuple.Create(false, null as Exception); + if (exception != null) + return; - bool anyRewritten = false; try { anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); @@ -118,22 +113,17 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } } } - - return Tuple.Create(anyRewritten, null as Exception); } - catch (Exception e) + catch (Exception ex) { - cancellationToken.Cancel(); - return Tuple.Create(anyRewritten, e); + exception ??= ex; } - }) - .Aggregate(Tuple.Create(false, null as Exception), (a, b) => Tuple.Create(a.Item1 || b.Item1, a.Item2 ?? b.Item2)); + } + ); - bool rewritten = result.Item1; - Exception exception = result.Item2; return exception == null - ? rewritten - : throw exception; + ? anyRewritten + : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); } -- cgit From 868eefb9a7d30c194a4d3e2b95565cebdcf053be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 4 Jun 2020 21:08:12 -0400 Subject: fix thread safety issue in new parallel rewriting --- .../ModLoading/Framework/RecursiveRewriter.cs | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index b304a732..9dc3680f 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Mono.Cecil; using Mono.Cecil.Cil; @@ -58,8 +59,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework /// Returns whether the module was modified. public bool RewriteModule() { - bool anyRewritten = false; + int typesChanged = 0; Exception exception = null; + Parallel.ForEach( source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like body: type => @@ -67,33 +69,34 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework if (exception != null) return; + bool changed = false; try { - anyRewritten |= this.RewriteCustomAttributes(type.CustomAttributes); - anyRewritten |= this.RewriteGenericParameters(type.GenericParameters); + changed |= this.RewriteCustomAttributes(type.CustomAttributes); + changed |= this.RewriteGenericParameters(type.GenericParameters); foreach (InterfaceImplementation @interface in type.Interfaces) - anyRewritten |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); + changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); if (type.BaseType.FullName != "System.Object") - anyRewritten |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); + changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); foreach (MethodDefinition method in type.Methods) { - anyRewritten |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); - anyRewritten |= this.RewriteGenericParameters(method.GenericParameters); - anyRewritten |= this.RewriteCustomAttributes(method.CustomAttributes); + 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) - anyRewritten |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); foreach (var methodOverride in method.Overrides) - anyRewritten |= this.RewriteMethodReference(methodOverride); + changed |= this.RewriteMethodReference(methodOverride); if (method.HasBody) { foreach (VariableDefinition variable in method.Body.Variables) - anyRewritten |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); // check CIL instructions ILProcessor cil = method.Body.GetILProcessor(); @@ -104,9 +107,9 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework if (instruction.OpCode.Code == Code.Nop) continue; - anyRewritten |= this.RewriteInstruction(instruction, cil, newInstruction => + changed |= this.RewriteInstruction(instruction, cil, newInstruction => { - anyRewritten = true; + changed = true; cil.Replace(instruction, newInstruction); instruction = newInstruction; }); @@ -118,11 +121,14 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework { exception ??= ex; } + + if (changed) + Interlocked.Increment(ref typesChanged); } ); return exception == null - ? anyRewritten + ? typesChanged > 0 : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); } -- cgit From 8036bcdf007ef4c3e8b39778a2a202ba0b22edec Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Jun 2020 12:33:48 -0400 Subject: improve premultiplication performance --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 55339d57..6c9a9649 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. * Simplified paranoid warning logs and reduced their log level. * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). + * Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!). * Fixed `BadImageFormatException` error detection. * Fixed black maps on Android for mods which use `.tmx` files. 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); -- cgit From 79181012ee01e93c1af7c4bf8bd1a3a717274ded Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jun 2020 10:55:52 -0400 Subject: tweak build files --- build/common.targets | 1 + src/SMAPI.Installer/SMAPI.Installer.csproj | 8 +-- .../SMAPI.ModBuildConfig.Analyzer.csproj | 4 -- .../SMAPI.ModBuildConfig.csproj | 25 ++----- src/SMAPI.ModBuildConfig/build/smapi.targets | 56 ++++----------- .../SMAPI.Mods.ConsoleCommands.csproj | 47 +++---------- .../SMAPI.Mods.SaveBackup.csproj | 16 +---- .../SMAPI.Toolkit.CoreInterfaces.csproj | 6 +- src/SMAPI.Toolkit/ModToolkit.cs | 3 - src/SMAPI.Toolkit/Properties/AssemblyInfo.cs | 4 ++ src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 7 +- src/SMAPI/SMAPI.csproj | 81 +++++----------------- 12 files changed, 56 insertions(+), 202 deletions(-) create mode 100644 src/SMAPI.Toolkit/Properties/AssemblyInfo.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 41bea8af..ddfbd71a 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,6 +7,7 @@ 3.5.0 SMAPI + latest $(AssemblySearchPaths);{GAC} $(DefineConstants);SMAPI_FOR_WINDOWS diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj index 79e19d89..44ed3bd1 100644 --- a/src/SMAPI.Installer/SMAPI.Installer.csproj +++ b/src/SMAPI.Installer/SMAPI.Installer.csproj @@ -1,11 +1,8 @@  - - SMAPI.Installer StardewModdingAPI.Installer The SMAPI installer for players. net45 - latest Exe x86 false @@ -16,13 +13,10 @@ - - PreserveNewest - + - diff --git a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj index 3659e25a..0d109b83 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer/SMAPI.ModBuildConfig.Analyzer.csproj @@ -1,11 +1,8 @@  - - SMAPI.ModBuildConfig.Analyzer StardewModdingAPI.ModBuildConfig.Analyzer 3.0.0 netstandard2.0 - latest false bin latest @@ -19,5 +16,4 @@ - diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index ccbd9a85..5061b01b 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -1,24 +1,12 @@  - - SMAPI.ModBuildConfig StardewModdingAPI.ModBuildConfig - 3.0.0 + 3.1.0 net45 - latest x86 false - - - - - - - - - @@ -28,19 +16,16 @@ - - mod-package.md - + - - PreserveNewest - + + + - diff --git a/src/SMAPI.ModBuildConfig/build/smapi.targets b/src/SMAPI.ModBuildConfig/build/smapi.targets index 5ca9f032..bfee3b33 100644 --- a/src/SMAPI.ModBuildConfig/build/smapi.targets +++ b/src/SMAPI.ModBuildConfig/build/smapi.targets @@ -38,58 +38,26 @@ **********************************************--> - - $(GamePath)\$(GameExecutableName).exe - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\StardewValley.GameData.dll - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\StardewModdingAPI.exe - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\smapi-internal\SMAPI.Toolkit.CoreInterfaces.dll - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\xTile.dll - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\smapi-internal\0Harmony.dll - $(CopyModReferencesToBuildOutput) - + + + + + + - - $(CopyModReferencesToBuildOutput) - - - $(CopyModReferencesToBuildOutput) - - - $(CopyModReferencesToBuildOutput) - - - $(CopyModReferencesToBuildOutput) - - - $(GamePath)\Netcode.dll - $(CopyModReferencesToBuildOutput) - + + + + + - - $(GamePath)\MonoGame.Framework.dll - $(CopyModReferencesToBuildOutput) - + diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index 526d406b..1e3208de 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -1,72 +1,45 @@  - ConsoleCommands StardewModdingAPI.Mods.ConsoleCommands net45 - latest false x86 - - False - + - - $(GamePath)\$(GameExecutableName).exe - False - - - $(GamePath)\StardewValley.GameData.dll - False - + + - - $(GamePath)\Netcode.dll - False - - - False - - - False - - - False - - - False - + + + + + - - $(GamePath)\MonoGame.Framework.dll - False - + - - PreserveNewest - + - diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj index 970ccea8..98a3f0cc 100644 --- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -1,34 +1,24 @@  - SaveBackup StardewModdingAPI.Mods.SaveBackup net45 - latest false x86 - - False - + - - $(GamePath)\$(GameExecutableName).exe - False - + - - PreserveNewest - + - diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj index accc9175..2bddc46a 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -1,15 +1,11 @@  - - SMAPI.Toolkit.CoreInterfaces StardewModdingAPI Provides toolkit interfaces which are available to SMAPI mods. net4.5;netstandard2.0 - latest - bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml + true x86 - diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs index 80b14659..08fe0fed 100644 --- a/src/SMAPI.Toolkit/ModToolkit.cs +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -11,8 +10,6 @@ using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.ModScanning; using StardewModdingAPI.Toolkit.Serialization; -[assembly: InternalsVisibleTo("StardewModdingAPI")] -[assembly: InternalsVisibleTo("SMAPI.Web")] namespace StardewModdingAPI.Toolkit { /// A convenience wrapper for the various tools. diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..233e680b --- /dev/null +++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("StardewModdingAPI")] +[assembly: InternalsVisibleTo("SMAPI.Web")] diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 4e6918ad..71ea0f12 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -1,14 +1,10 @@  - - SMAPI.Toolkit StardewModdingAPI.Toolkit A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods. net4.5;netstandard2.0 - latest - bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.xml + true x86 - StardewModdingAPI.Toolkit @@ -24,5 +20,4 @@ - diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index d36d7b4c..02f1763f 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -1,14 +1,12 @@  - StardewModdingAPI StardewModdingAPI The modding API for Stardew Valley. net45 - latest x86 Exe - bin\$(Configuration)\StardewModdingAPI.xml + true false true icon.ico @@ -23,54 +21,24 @@ - - $(GamePath)\$(GameExecutableName).exe - False - - - $(GamePath)\StardewValley.GameData.dll - False - - - True - - - True - - - $(GamePath)\GalaxyCSharp.dll - False - - - $(GamePath)\Lidgren.Network.dll - False - - - $(GamePath)\xTile.dll - False - + + + + + + + - - $(GamePath)\Netcode.dll - False - - - False - - - False - - - False - - - False - + + + + + @@ -78,10 +46,7 @@ - - $(GamePath)\MonoGame.Framework.dll - False - + @@ -92,22 +57,12 @@ - - PreserveNewest - - - SMAPI.metadata.json - PreserveNewest - - - PreserveNewest - - - PreserveNewest - + + + + - -- cgit From 2d19095169019a1cb07da5802dd83fb13550a051 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jun 2020 11:29:07 -0400 Subject: add support for using a custom Harmony build (#711) --- .gitignore | 4 ++-- docs/technical/smapi.md | 33 ++++++++++++++++++++------------- src/SMAPI/SMAPI.csproj | 3 ++- 3 files changed, 24 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/.gitignore b/.gitignore index 5450a2f5..02522716 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,8 @@ _ReSharper*/ # sensitive files appsettings.Development.json -# AWS generated files -src/SMAPI.Web.LegacyRedirects/aws-beanstalk-tools-defaults.json +# generated build files +build/0Harmony.* # Azure generated files src/SMAPI.Web/Properties/PublishProfiles/*.pubxml diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index c9d5c07e..ca8a9c70 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -15,6 +15,7 @@ This document is about SMAPI itself; see also [mod build package](mod-package.md * [Compiling from source](#compiling-from-source) * [Debugging a local build](#debugging-a-local-build) * [Preparing a release](#preparing-a-release) + * [Using a custom Harmony build](#using-a-custom-harmony-build) * [Release notes](#release-notes) ## Customisation @@ -60,21 +61,18 @@ flag | purpose ## For SMAPI developers ### Compiling from source -Using an official SMAPI release is recommended for most users. +Using an official SMAPI release is recommended for most users, but you can compile from source +directly if needed. There are no special steps (just open the project and compile), but SMAPI often +uses the latest C# syntax. You may need the latest version of your IDE to compile it. -SMAPI often uses the latest C# syntax. You may need the latest version of -[Visual Studio](https://www.visualstudio.com/vs/community/) on Windows, -[MonoDevelop](https://www.monodevelop.com/) on Linux, -[Visual Studio for Mac](https://www.visualstudio.com/vs/visual-studio-mac/), or an equivalent IDE -to compile it. It uses build configuration derived from the -[crossplatform mod config](https://smapi.io/package/readme) to detect your current OS automatically -and load the correct references. Compile output will be placed in a `bin` folder at the root of the -git repository. +SMAPI uses build configuration derived from the [crossplatform mod config](https://smapi.io/package/readme) +to detect your current OS automatically and load the correct references. Compile output will be +placed in a `bin` folder at the root of the Git repository. ### Debugging a local build Rebuilding the solution in debug mode will copy the SMAPI files into your game folder. Starting the `SMAPI` project with debugging from Visual Studio (on Mac or Windows) will launch SMAPI with -the debugger attached, so you can intercept errors and step through the code being executed. This +the debugger attached, so you can intercept errors and step through the code being executed. That doesn't work in MonoDevelop on Linux, unfortunately. ### Preparing a release @@ -87,9 +85,9 @@ on the wiki for the first-time setup. build type | format | example :--------- | :----------------------- | :------ - dev build | `-alpha.` | `3.0-alpha.20171230` - prerelease | `-beta.` | `3.0-beta.2` - release | `` | `3.0` + dev build | `-alpha.` | `3.0.0-alpha.20171230` + prerelease | `-beta.` | `3.0.0-beta.2` + release | `` | `3.0.0` 2. In Windows: 1. Rebuild the solution in Release mode. @@ -103,5 +101,14 @@ on the wiki for the first-time setup. 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. 4. Zip the two folders. +### Using a custom Harmony build +The official SMAPI releases include [a custom build of Harmony](https://github.com/Pathoschild/Harmony), +but compiling from source will use the official build. To use a custom build, put `0Harmony.dll` in +the `build` folder and it'll be referenced automatically. + +Note that Harmony merges its dependencies into `0Harmony.dll` when compiled in release mode. To use +a debug build of Harmony, you'll need to manually copy those dependencies into your game's +`smapi-internal` folder. + ## Release notes See [release notes](../release-notes.md). diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 02f1763f..443d5baa 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,13 +14,14 @@ - + + -- cgit From ff7b9a0251484bfb9737f9c6c05637f63efa9551 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Jun 2020 23:30:35 -0400 Subject: update TMXTile --- src/SMAPI/SMAPI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 443d5baa..603b6fb5 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -17,7 +17,7 @@ - + -- cgit From b395e92faae0a197a2d1c2e10e835e38fcc6502c Mon Sep 17 00:00:00 2001 From: Chase W Date: Mon, 15 Jun 2020 15:28:03 -0400 Subject: Implemented event priority attribute --- src/SMAPI/Events/EventPriority.cs | 29 +++++++++++++++++++ src/SMAPI/Events/EventPriorityAttribute.cs | 29 +++++++++++++++++++ src/SMAPI/Framework/Events/ManagedEvent.cs | 45 ++++++++++++++++++++++++------ 3 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 src/SMAPI/Events/EventPriority.cs create mode 100644 src/SMAPI/Events/EventPriorityAttribute.cs (limited to 'src') diff --git a/src/SMAPI/Events/EventPriority.cs b/src/SMAPI/Events/EventPriority.cs new file mode 100644 index 00000000..17f5fbb7 --- /dev/null +++ b/src/SMAPI/Events/EventPriority.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Events +{ + /// + /// Event priority for method handlers. + /// + public enum EventPriority + { + /// + /// Low priority. + /// + Low = 3, + + /// + /// Normal priority. This is the default. + /// + Normal = 2, + + /// + /// High priority. + /// + High = 1, + } +} diff --git a/src/SMAPI/Events/EventPriorityAttribute.cs b/src/SMAPI/Events/EventPriorityAttribute.cs new file mode 100644 index 00000000..c5683931 --- /dev/null +++ b/src/SMAPI/Events/EventPriorityAttribute.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StardewModdingAPI.Events +{ + /// + /// An attribute for controlling event priority of an event handler. + /// + [AttributeUsage(AttributeTargets.Method)] + public class EventPriorityAttribute : System.Attribute + { + /// + /// The priority for the method marked by this attribute. + /// + public EventPriority Priority { get; } + + /// + /// Constructor. + /// + /// The priority for method marked by this attribute. + public EventPriorityAttribute( EventPriority priority ) + { + this.Priority = priority; + } + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 118b73ac..172b25c0 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,6 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events @@ -13,7 +16,7 @@ namespace StardewModdingAPI.Framework.Events ** Fields *********/ /// The underlying event. - private event EventHandler Event; + private IList> EventHandlers = new List>(); /// Writes messages to the log. private readonly IMonitor Monitor; @@ -77,23 +80,23 @@ namespace StardewModdingAPI.Framework.Events /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { - this.Event += handler; - this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); + this.EventHandlers.Add(handler); + this.AddTracking(mod, handler, this.EventHandlers); } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - this.Event -= handler; - this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); + this.EventHandlers.Remove(handler); + this.RemoveTracking(handler, this.EventHandlers); } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { - if (this.Event == null) + if (this.EventHandlers.Count == 0) return; @@ -118,7 +121,7 @@ namespace StardewModdingAPI.Framework.Events /// A lambda which returns true if the event should be raised for the given mod. public void RaiseForMods(TEventArgs args, Func match) { - if (this.Event == null) + if (this.EventHandlers.Count == 0) return; foreach (EventHandler handler in this.CachedInvocationList) @@ -154,6 +157,30 @@ namespace StardewModdingAPI.Framework.Events : mod.DisplayName; } + /// + /// Get the event priority of an event handler. + /// + /// The event handler to get the priority of. + /// The event priority of the event handler. + private EventPriority GetPriorityOfHandler(EventHandler handler) + { + CustomAttributeData attr = handler.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(EventPriorityAttribute)); + if (attr == null) + return EventPriority.Normal; + return (EventPriority) attr.ConstructorArguments[0].Value; + } + + /// + /// Sort an invocation list by its priority. + /// + /// The invocation list. + /// An array of the event handlers sorted by their priority. + private EventHandler[] GetCachedInvocationList(IEnumerable> invocationList ) + { + EventHandler[] handlers = invocationList?.ToArray() ?? new EventHandler[0]; + return handlers.OrderBy((h1) => this.GetPriorityOfHandler(h1)).ToArray(); + } + /// Track an event handler. /// The mod which added the handler. /// The event handler. @@ -161,7 +188,7 @@ namespace StardewModdingAPI.Framework.Events protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) { this.SourceMods[handler] = mod; - this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; + this.CachedInvocationList = this.GetCachedInvocationList(invocationList); } /// Remove tracking for an event handler. @@ -169,7 +196,7 @@ namespace StardewModdingAPI.Framework.Events /// The updated event invocation list. protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) { - this.CachedInvocationList = invocationList?.ToArray() ?? new EventHandler[0]; + this.CachedInvocationList = this.GetCachedInvocationList(invocationList); if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) this.SourceMods.Remove(handler); } -- cgit From fc29fe918a89623544b011c76217aa1ea1975d00 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 18:58:05 -0400 Subject: refactor & optimize event code a bit, drop old support for unknown event handlers --- src/SMAPI/Events/EventPriority.cs | 24 +--- src/SMAPI/Events/EventPriorityAttribute.cs | 31 +++-- src/SMAPI/Framework/Events/EventManager.cs | 5 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 138 ++++++--------------- src/SMAPI/Framework/Events/ManagedEventHandler.cs | 62 +++++++++ src/SMAPI/Framework/Events/ModDisplayEvents.cs | 20 +-- src/SMAPI/Framework/Events/ModGameLoopEvents.cs | 28 ++--- src/SMAPI/Framework/Events/ModInputEvents.cs | 8 +- src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 6 +- src/SMAPI/Framework/Events/ModPlayerEvents.cs | 6 +- src/SMAPI/Framework/Events/ModSpecialisedEvents.cs | 6 +- src/SMAPI/Framework/Events/ModWorldEvents.cs | 8 +- src/SMAPI/Framework/SCore.cs | 2 +- 13 files changed, 166 insertions(+), 178 deletions(-) create mode 100644 src/SMAPI/Framework/Events/ManagedEventHandler.cs (limited to 'src') diff --git a/src/SMAPI/Events/EventPriority.cs b/src/SMAPI/Events/EventPriority.cs index 17f5fbb7..e1fb00ac 100644 --- a/src/SMAPI/Events/EventPriority.cs +++ b/src/SMAPI/Events/EventPriority.cs @@ -1,29 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace StardewModdingAPI.Events { - /// - /// Event priority for method handlers. - /// + /// The event priorities for method handlers. public enum EventPriority { - /// - /// Low priority. - /// + /// Low priority. Low = 3, - /// - /// Normal priority. This is the default. - /// + /// The default priority. Normal = 2, - /// - /// High priority. - /// - High = 1, + /// High priority. + High = 1 } } diff --git a/src/SMAPI/Events/EventPriorityAttribute.cs b/src/SMAPI/Events/EventPriorityAttribute.cs index c5683931..207e7862 100644 --- a/src/SMAPI/Events/EventPriorityAttribute.cs +++ b/src/SMAPI/Events/EventPriorityAttribute.cs @@ -1,27 +1,24 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace StardewModdingAPI.Events { - /// - /// An attribute for controlling event priority of an event handler. - /// + /// An attribute which specifies the priority for an event handler. [AttributeUsage(AttributeTargets.Method)] - public class EventPriorityAttribute : System.Attribute + public class EventPriorityAttribute : Attribute { - /// - /// The priority for the method marked by this attribute. - /// - public EventPriority Priority { get; } + /********* + ** Accessors + *********/ + /// The event handler priority, relative to other handlers across all mods registered for this event. + internal EventPriority Priority { get; } - /// - /// Constructor. - /// - /// The priority for method marked by this attribute. - public EventPriorityAttribute( EventPriority priority ) + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The event handler priority, relative to other handlers across all mods registered for this event. Higher-priority handlers are notified before lower-priority handlers. + public EventPriorityAttribute(EventPriority priority) { this.Priority = priority; } diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index a9dfda97..538fde59 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -174,15 +174,14 @@ namespace StardewModdingAPI.Framework.Events ** Public methods *********/ /// Construct an instance. - /// Writes messages to the log. /// The mod registry with which to identify mods. /// Tracks performance metrics. - public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor) + public EventManager(ModRegistry modRegistry, PerformanceMonitor performanceMonitor) { // create shortcut initializers ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) { - return new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical); + return new ManagedEvent($"{typeName}.{eventName}", modRegistry, performanceMonitor, isPerformanceCritical); } // init events (new) diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 172b25c0..b0f0ae71 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -15,24 +14,24 @@ namespace StardewModdingAPI.Framework.Events /********* ** Fields *********/ - /// The underlying event. - private IList> EventHandlers = new List>(); - - /// Writes messages to the log. - private readonly IMonitor Monitor; + /// The underlying event handlers. + private readonly List> EventHandlers = new List>(); /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; - /// The display names for the mods which added each delegate. - private readonly IDictionary, IModMetadata> SourceMods = new Dictionary, IModMetadata>(); - - /// The cached invocation list. - private EventHandler[] CachedInvocationList; - /// Tracks performance metrics. private readonly PerformanceMonitor PerformanceMonitor; + /// The total number of event handlers registered for this events, regardless of whether they're still registered. + private int RegistrationIndex; + + /// Whether any registered event handlers have a custom priority value. + private bool HasCustomPriorities; + + /// Whether event handlers should be sorted before the next invocation. + private bool NeedsSort; + /********* ** Accessors @@ -49,14 +48,12 @@ namespace StardewModdingAPI.Framework.Events *********/ /// Construct an instance. /// A human-readable name for the event. - /// Writes messages to the log. /// The mod registry with which to identify mods. /// Tracks performance metrics. /// Whether the event is typically called at least once per second. - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; - this.Monitor = monitor; this.ModRegistry = modRegistry; this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; @@ -65,14 +62,7 @@ namespace StardewModdingAPI.Framework.Events /// Get whether anything is listening to the event. public bool HasListeners() { - return this.CachedInvocationList?.Length > 0; - } - - /// Add an event handler. - /// The event handler. - public void Add(EventHandler handler) - { - this.Add(handler, this.ModRegistry.GetFromStack()); + return this.EventHandlers.Count > 0; } /// Add an event handler. @@ -80,33 +70,46 @@ namespace StardewModdingAPI.Framework.Events /// The mod which added the event handler. public void Add(EventHandler handler, IModMetadata mod) { - this.EventHandlers.Add(handler); - this.AddTracking(mod, handler, this.EventHandlers); + EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; + var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); + + this.EventHandlers.Add(managedHandler); + this.HasCustomPriorities = this.HasCustomPriorities || managedHandler.HasCustomPriority(); + + if (this.HasCustomPriorities) + this.NeedsSort = true; } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - this.EventHandlers.Remove(handler); - this.RemoveTracking(handler, this.EventHandlers); + this.EventHandlers.RemoveAll(p => p.Handler == handler); + this.HasCustomPriorities = this.HasCustomPriorities && this.EventHandlers.Any(p => p.HasCustomPriority()); } /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) { + // sort event handlers by priority + // (This is done here to avoid repeatedly sorting when handlers are added/removed.) + if (this.NeedsSort) + { + this.NeedsSort = false; + this.EventHandlers.Sort(); + } + + // raise if (this.EventHandlers.Count == 0) return; - - this.PerformanceMonitor.Track(this.EventName, () => { - foreach (EventHandler handler in this.CachedInvocationList) + foreach (ManagedEventHandler handler in this.EventHandlers) { try { - this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Invoke(null, args)); + this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); } catch (Exception ex) { @@ -124,13 +127,13 @@ namespace StardewModdingAPI.Framework.Events if (this.EventHandlers.Count == 0) return; - foreach (EventHandler handler in this.CachedInvocationList) + foreach (ManagedEventHandler handler in this.EventHandlers) { - if (match(this.GetSourceMod(handler))) + if (match(handler.SourceMod)) { try { - handler.Invoke(null, args); + handler.Handler.Invoke(null, args); } catch (Exception ex) { @@ -146,80 +149,21 @@ namespace StardewModdingAPI.Framework.Events *********/ /// Get the mod name for a given event handler to display in performance monitoring reports. /// The event handler. - private string GetModNameForPerformanceCounters(EventHandler handler) + private string GetModNameForPerformanceCounters(ManagedEventHandler handler) { - IModMetadata mod = this.GetSourceMod(handler); - if (mod == null) - return Constants.GamePerformanceCounterName; + IModMetadata mod = handler.SourceMod; return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; } - /// - /// Get the event priority of an event handler. - /// - /// The event handler to get the priority of. - /// The event priority of the event handler. - private EventPriority GetPriorityOfHandler(EventHandler handler) - { - CustomAttributeData attr = handler.Method.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(EventPriorityAttribute)); - if (attr == null) - return EventPriority.Normal; - return (EventPriority) attr.ConstructorArguments[0].Value; - } - - /// - /// Sort an invocation list by its priority. - /// - /// The invocation list. - /// An array of the event handlers sorted by their priority. - private EventHandler[] GetCachedInvocationList(IEnumerable> invocationList ) - { - EventHandler[] handlers = invocationList?.ToArray() ?? new EventHandler[0]; - return handlers.OrderBy((h1) => this.GetPriorityOfHandler(h1)).ToArray(); - } - - /// Track an event handler. - /// The mod which added the handler. - /// The event handler. - /// The updated event invocation list. - protected void AddTracking(IModMetadata mod, EventHandler handler, IEnumerable> invocationList) - { - this.SourceMods[handler] = mod; - this.CachedInvocationList = this.GetCachedInvocationList(invocationList); - } - - /// Remove tracking for an event handler. - /// The event handler. - /// The updated event invocation list. - protected void RemoveTracking(EventHandler handler, IEnumerable> invocationList) - { - this.CachedInvocationList = this.GetCachedInvocationList(invocationList); - if (!this.CachedInvocationList.Contains(handler)) // don't remove if there's still a reference to the removed handler (e.g. it was added twice and removed once) - this.SourceMods.Remove(handler); - } - - /// Get the mod which registered the given event handler, if available. - /// The event handler. - protected IModMetadata GetSourceMod(EventHandler handler) - { - return this.SourceMods.TryGetValue(handler, out IModMetadata mod) - ? mod - : null; - } - /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. - protected void LogError(EventHandler handler, Exception ex) + protected void LogError(ManagedEventHandler handler, Exception ex) { - IModMetadata mod = this.GetSourceMod(handler); - if (mod != null) - mod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); - else - this.Monitor.Log($"A mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); + handler.SourceMod.LogAsMod($"This mod failed in the {this.EventName} event. Technical details: \n{ex.GetLogSummary()}", LogLevel.Error); } } } diff --git a/src/SMAPI/Framework/Events/ManagedEventHandler.cs b/src/SMAPI/Framework/Events/ManagedEventHandler.cs new file mode 100644 index 00000000..87591f63 --- /dev/null +++ b/src/SMAPI/Framework/Events/ManagedEventHandler.cs @@ -0,0 +1,62 @@ +using System; +using StardewModdingAPI.Events; + +namespace StardewModdingAPI.Framework.Events +{ + /// An event handler wrapper which tracks metadata about an event handler. + /// The event arguments type. + internal class ManagedEventHandler : IComparable + { + /********* + ** Accessors + *********/ + /// The event handler method. + public EventHandler Handler { get; } + + /// The order in which the event handler was registered, relative to other handlers for this event. + public int RegistrationOrder { get; } + + /// The event handler priority, relative to other handlers for this event. + public EventPriority Priority { get; } + + /// The mod which registered the handler. + public IModMetadata SourceMod { get; set; } + + + /********* + ** Accessors + *********/ + /// Construct an instance. + /// The event handler method. + /// The order in which the event handler was registered, relative to other handlers for this event. + /// The event handler priority, relative to other handlers for this event. + /// The mod which registered the handler. + public ManagedEventHandler(EventHandler handler, int registrationOrder, EventPriority priority, IModMetadata sourceMod) + { + this.Handler = handler; + this.RegistrationOrder = registrationOrder; + this.Priority = priority; + this.SourceMod = sourceMod; + } + + /// Get whether the event handler has a custom priority value. + public bool HasCustomPriority() + { + return this.Priority != EventPriority.Normal; + } + + /// Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. + /// An object to compare with this instance. + /// is not the same type as this instance. + public int CompareTo(object obj) + { + if (!(obj is ManagedEventHandler other)) + throw new ArgumentException("Can't compare to an unrelated object type."); + + int priorityCompare = this.Priority.CompareTo(other.Priority); + return priorityCompare != 0 + ? priorityCompare + : this.RegistrationOrder.CompareTo(other.RegistrationOrder); + } + } +} diff --git a/src/SMAPI/Framework/Events/ModDisplayEvents.cs b/src/SMAPI/Framework/Events/ModDisplayEvents.cs index e383eec6..54d40dee 100644 --- a/src/SMAPI/Framework/Events/ModDisplayEvents.cs +++ b/src/SMAPI/Framework/Events/ModDisplayEvents.cs @@ -13,70 +13,70 @@ namespace StardewModdingAPI.Framework.Events /// Raised after a game menu is opened, closed, or replaced. public event EventHandler MenuChanged { - add => this.EventManager.MenuChanged.Add(value); + add => this.EventManager.MenuChanged.Add(value, this.Mod); remove => this.EventManager.MenuChanged.Remove(value); } /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. The sprite batch may be closed and reopened multiple times after this event is called, but it's only raised once per draw tick. This event isn't useful for drawing to the screen, since the game will draw over it. public event EventHandler Rendering { - add => this.EventManager.Rendering.Add(value); + add => this.EventManager.Rendering.Add(value, this.Mod); remove => this.EventManager.Rendering.Remove(value); } /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. Since the game may open/close the sprite batch multiple times in a draw tick, the sprite batch may not contain everything being drawn and some things may already be rendered to the screen. Content drawn to the sprite batch at this point will be drawn over all vanilla content (including menus, HUD, and cursor). public event EventHandler Rendered { - add => this.EventManager.Rendered.Add(value); + add => this.EventManager.Rendered.Add(value, this.Mod); remove => this.EventManager.Rendered.Remove(value); } /// Raised before the game world is drawn to the screen. This event isn't useful for drawing to the screen, since the game will draw over it. public event EventHandler RenderingWorld { - add => this.EventManager.RenderingWorld.Add(value); + add => this.EventManager.RenderingWorld.Add(value, this.Mod); remove => this.EventManager.RenderingWorld.Remove(value); } /// Raised after the game world is drawn to the sprite patch, before it's rendered to the screen. Content drawn to the sprite batch at this point will be drawn over the world, but under any active menu, HUD elements, or cursor. public event EventHandler RenderedWorld { - add => this.EventManager.RenderedWorld.Add(value); + add => this.EventManager.RenderedWorld.Add(value, this.Mod); remove => this.EventManager.RenderedWorld.Remove(value); } /// When a menu is open ( isn't null), raised before that menu is drawn to the screen. This includes the game's internal menus like the title screen. Content drawn to the sprite batch at this point will appear under the menu. public event EventHandler RenderingActiveMenu { - add => this.EventManager.RenderingActiveMenu.Add(value); + add => this.EventManager.RenderingActiveMenu.Add(value, this.Mod); remove => this.EventManager.RenderingActiveMenu.Remove(value); } /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. Content drawn to the sprite batch at this point will appear over the menu and menu cursor. public event EventHandler RenderedActiveMenu { - add => this.EventManager.RenderedActiveMenu.Add(value); + add => this.EventManager.RenderedActiveMenu.Add(value, this.Mod); remove => this.EventManager.RenderedActiveMenu.Remove(value); } /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear under the HUD. public event EventHandler RenderingHud { - add => this.EventManager.RenderingHud.Add(value); + add => this.EventManager.RenderingHud.Add(value, this.Mod); remove => this.EventManager.RenderingHud.Remove(value); } /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. public event EventHandler RenderedHud { - add => this.EventManager.RenderedHud.Add(value); + add => this.EventManager.RenderedHud.Add(value, this.Mod); remove => this.EventManager.RenderedHud.Remove(value); } /// Raised after the game window is resized. public event EventHandler WindowResized { - add => this.EventManager.WindowResized.Add(value); + add => this.EventManager.WindowResized.Add(value, this.Mod); remove => this.EventManager.WindowResized.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs index c15460fa..a0119bf8 100644 --- a/src/SMAPI/Framework/Events/ModGameLoopEvents.cs +++ b/src/SMAPI/Framework/Events/ModGameLoopEvents.cs @@ -12,84 +12,84 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the game is launched, right before the first update tick. public event EventHandler GameLaunched { - add => this.EventManager.GameLaunched.Add(value); + add => this.EventManager.GameLaunched.Add(value, this.Mod); remove => this.EventManager.GameLaunched.Remove(value); } /// Raised before the game performs its overall update tick (≈60 times per second). public event EventHandler UpdateTicking { - add => this.EventManager.UpdateTicking.Add(value); + add => this.EventManager.UpdateTicking.Add(value, this.Mod); remove => this.EventManager.UpdateTicking.Remove(value); } /// Raised after the game performs its overall update tick (≈60 times per second). public event EventHandler UpdateTicked { - add => this.EventManager.UpdateTicked.Add(value); + add => this.EventManager.UpdateTicked.Add(value, this.Mod); remove => this.EventManager.UpdateTicked.Remove(value); } /// Raised once per second before the game state is updated. public event EventHandler OneSecondUpdateTicking { - add => this.EventManager.OneSecondUpdateTicking.Add(value); + add => this.EventManager.OneSecondUpdateTicking.Add(value, this.Mod); remove => this.EventManager.OneSecondUpdateTicking.Remove(value); } /// Raised once per second after the game state is updated. public event EventHandler OneSecondUpdateTicked { - add => this.EventManager.OneSecondUpdateTicked.Add(value); + add => this.EventManager.OneSecondUpdateTicked.Add(value, this.Mod); remove => this.EventManager.OneSecondUpdateTicked.Remove(value); } /// Raised before the game creates a new save file. public event EventHandler SaveCreating { - add => this.EventManager.SaveCreating.Add(value); + add => this.EventManager.SaveCreating.Add(value, this.Mod); remove => this.EventManager.SaveCreating.Remove(value); } /// Raised after the game finishes creating the save file. public event EventHandler SaveCreated { - add => this.EventManager.SaveCreated.Add(value); + add => this.EventManager.SaveCreated.Add(value, this.Mod); remove => this.EventManager.SaveCreated.Remove(value); } /// Raised before the game begins writes data to the save file. public event EventHandler Saving { - add => this.EventManager.Saving.Add(value); + add => this.EventManager.Saving.Add(value, this.Mod); remove => this.EventManager.Saving.Remove(value); } /// Raised after the game finishes writing data to the save file. public event EventHandler Saved { - add => this.EventManager.Saved.Add(value); + add => this.EventManager.Saved.Add(value, this.Mod); remove => this.EventManager.Saved.Remove(value); } /// Raised after the player loads a save slot and the world is initialized. public event EventHandler SaveLoaded { - add => this.EventManager.SaveLoaded.Add(value); + add => this.EventManager.SaveLoaded.Add(value, this.Mod); remove => this.EventManager.SaveLoaded.Remove(value); } /// Raised after the game begins a new day (including when the player loads a save). public event EventHandler DayStarted { - add => this.EventManager.DayStarted.Add(value); + add => this.EventManager.DayStarted.Add(value, this.Mod); remove => this.EventManager.DayStarted.Remove(value); } /// Raised before the game ends the current day. This happens before it starts setting up the next day and before . public event EventHandler DayEnding { - add => this.EventManager.DayEnding.Add(value); + add => this.EventManager.DayEnding.Add(value, this.Mod); remove => this.EventManager.DayEnding.Remove(value); } @@ -97,14 +97,14 @@ namespace StardewModdingAPI.Framework.Events public event EventHandler TimeChanged { - add => this.EventManager.TimeChanged.Add(value); + add => this.EventManager.TimeChanged.Add(value, this.Mod); remove => this.EventManager.TimeChanged.Remove(value); } /// Raised after the game returns to the title screen. public event EventHandler ReturnedToTitle { - add => this.EventManager.ReturnedToTitle.Add(value); + add => this.EventManager.ReturnedToTitle.Add(value, this.Mod); remove => this.EventManager.ReturnedToTitle.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModInputEvents.cs b/src/SMAPI/Framework/Events/ModInputEvents.cs index 6a4298b4..ab26ab3e 100644 --- a/src/SMAPI/Framework/Events/ModInputEvents.cs +++ b/src/SMAPI/Framework/Events/ModInputEvents.cs @@ -12,28 +12,28 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the player presses a button on the keyboard, controller, or mouse. public event EventHandler ButtonPressed { - add => this.EventManager.ButtonPressed.Add(value); + add => this.EventManager.ButtonPressed.Add(value, this.Mod); remove => this.EventManager.ButtonPressed.Remove(value); } /// Raised after the player releases a button on the keyboard, controller, or mouse. public event EventHandler ButtonReleased { - add => this.EventManager.ButtonReleased.Add(value); + add => this.EventManager.ButtonReleased.Add(value, this.Mod); remove => this.EventManager.ButtonReleased.Remove(value); } /// Raised after the player moves the in-game cursor. public event EventHandler CursorMoved { - add => this.EventManager.CursorMoved.Add(value); + add => this.EventManager.CursorMoved.Add(value, this.Mod); remove => this.EventManager.CursorMoved.Remove(value); } /// Raised after the player scrolls the mouse wheel. public event EventHandler MouseWheelScrolled { - add => this.EventManager.MouseWheelScrolled.Add(value); + add => this.EventManager.MouseWheelScrolled.Add(value, this.Mod); remove => this.EventManager.MouseWheelScrolled.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 152c4e0c..2006b2b5 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI. public event EventHandler PeerContextReceived { - add => this.EventManager.PeerContextReceived.Add(value); + add => this.EventManager.PeerContextReceived.Add(value, this.Mod); remove => this.EventManager.PeerContextReceived.Remove(value); } /// Raised after a mod message is received over the network. public event EventHandler ModMessageReceived { - add => this.EventManager.ModMessageReceived.Add(value); + add => this.EventManager.ModMessageReceived.Add(value, this.Mod); remove => this.EventManager.ModMessageReceived.Remove(value); } /// Raised after the connection with a peer is severed. public event EventHandler PeerDisconnected { - add => this.EventManager.PeerDisconnected.Add(value); + add => this.EventManager.PeerDisconnected.Add(value, this.Mod); remove => this.EventManager.PeerDisconnected.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModPlayerEvents.cs b/src/SMAPI/Framework/Events/ModPlayerEvents.cs index ca7cfd96..240beb8d 100644 --- a/src/SMAPI/Framework/Events/ModPlayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModPlayerEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the local player. public event EventHandler InventoryChanged { - add => this.EventManager.InventoryChanged.Add(value); + add => this.EventManager.InventoryChanged.Add(value, this.Mod); remove => this.EventManager.InventoryChanged.Remove(value); } /// Raised after a player skill level changes. This happens as soon as they level up, not when the game notifies the player after their character goes to bed. NOTE: this event is currently only raised for the local player. public event EventHandler LevelChanged { - add => this.EventManager.LevelChanged.Add(value); + add => this.EventManager.LevelChanged.Add(value, this.Mod); remove => this.EventManager.LevelChanged.Remove(value); } /// Raised after a player warps to a new location. NOTE: this event is currently only raised for the local player. public event EventHandler Warped { - add => this.EventManager.Warped.Add(value); + add => this.EventManager.Warped.Add(value, this.Mod); remove => this.EventManager.Warped.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs index 9388bdb2..1d6788e1 100644 --- a/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs +++ b/src/SMAPI/Framework/Events/ModSpecialisedEvents.cs @@ -12,21 +12,21 @@ namespace StardewModdingAPI.Framework.Events /// Raised when the low-level stage in the game's loading process has changed. This is an advanced event for mods which need to run code at specific points in the loading process. The available stages or when they happen might change without warning in future versions (e.g. due to changes in the game's load process), so mods using this event are more likely to break or have bugs. Most mods should use instead. public event EventHandler LoadStageChanged { - add => this.EventManager.LoadStageChanged.Add(value); + add => this.EventManager.LoadStageChanged.Add(value, this.Mod); remove => this.EventManager.LoadStageChanged.Remove(value); } /// Raised before the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. public event EventHandler UnvalidatedUpdateTicking { - add => this.EventManager.UnvalidatedUpdateTicking.Add(value); + add => this.EventManager.UnvalidatedUpdateTicking.Add(value, this.Mod); remove => this.EventManager.UnvalidatedUpdateTicking.Remove(value); } /// Raised after the game state is updated (≈60 times per second), regardless of normal SMAPI validation. This event is not thread-safe and may be invoked while game logic is running asynchronously. Changes to game state in this method may crash the game or corrupt an in-progress save. Do not use this event unless you're fully aware of the context in which your code will be run. Mods using this event will trigger a stability warning in the SMAPI console. public event EventHandler UnvalidatedUpdateTicked { - add => this.EventManager.UnvalidatedUpdateTicked.Add(value); + add => this.EventManager.UnvalidatedUpdateTicked.Add(value, this.Mod); remove => this.EventManager.UnvalidatedUpdateTicked.Remove(value); } diff --git a/src/SMAPI/Framework/Events/ModWorldEvents.cs b/src/SMAPI/Framework/Events/ModWorldEvents.cs index 2ae69669..21b1b664 100644 --- a/src/SMAPI/Framework/Events/ModWorldEvents.cs +++ b/src/SMAPI/Framework/Events/ModWorldEvents.cs @@ -40,28 +40,28 @@ namespace StardewModdingAPI.Framework.Events /// Raised after NPCs are added or removed in a location. public event EventHandler NpcListChanged { - add => this.EventManager.NpcListChanged.Add(value); + add => this.EventManager.NpcListChanged.Add(value, this.Mod); remove => this.EventManager.NpcListChanged.Remove(value); } /// Raised after objects are added or removed in a location. public event EventHandler ObjectListChanged { - add => this.EventManager.ObjectListChanged.Add(value); + add => this.EventManager.ObjectListChanged.Add(value, this.Mod); remove => this.EventManager.ObjectListChanged.Remove(value); } /// Raised after items are added or removed from a chest. public event EventHandler ChestInventoryChanged { - add => this.EventManager.ChestInventoryChanged.Add(value); + add => this.EventManager.ChestInventoryChanged.Add(value, this.Mod); remove => this.EventManager.ChestInventoryChanged.Remove(value); } /// Raised after terrain features (like floors and trees) are added or removed in a location. public event EventHandler TerrainFeatureListChanged { - add => this.EventManager.TerrainFeatureListChanged.Add(value); + add => this.EventManager.TerrainFeatureListChanged.Add(value, this.Mod); remove => this.EventManager.TerrainFeatureListChanged.Remove(value); } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index de9c955d..c6e69d4e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -172,7 +172,7 @@ namespace StardewModdingAPI.Framework this.MonitorForGame = this.GetSecondaryMonitor("game"); SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceMonitor); + this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor); SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); -- cgit From da95a906bf8e812ddcd99a90a4d49942f02f5623 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 18:59:05 -0400 Subject: increase event priority range This can be used in cases where more granular priority is needed. --- src/SMAPI/Events/EventPriority.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Events/EventPriority.cs b/src/SMAPI/Events/EventPriority.cs index e1fb00ac..1efb4e2a 100644 --- a/src/SMAPI/Events/EventPriority.cs +++ b/src/SMAPI/Events/EventPriority.cs @@ -4,12 +4,12 @@ namespace StardewModdingAPI.Events public enum EventPriority { /// Low priority. - Low = 3, + Low = -1000, /// The default priority. - Normal = 2, + Normal = 0, /// High priority. - High = 1 + High = 1000 } } -- cgit From 02e7318d2b99d311a328746b23a359364575f0c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 19:08:02 -0400 Subject: merge inconsistent event raise methods --- src/SMAPI/Framework/Events/ManagedEvent.cs | 38 ++++++++---------------------- src/SMAPI/Framework/SGame.cs | 2 +- 2 files changed, 11 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index b0f0ae71..b37fb376 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -90,8 +90,13 @@ namespace StardewModdingAPI.Framework.Events /// Raise the event and notify all handlers. /// The event arguments to pass. - public void Raise(TEventArgs args) + /// A lambda which returns true if the event should be raised for the given mod. + public void Raise(TEventArgs args, Func match = null) { + // skip if no handlers + if (this.EventHandlers.Count == 0) + return; + // sort event handlers by priority // (This is done here to avoid repeatedly sorting when handlers are added/removed.) if (this.NeedsSort) @@ -100,13 +105,14 @@ namespace StardewModdingAPI.Framework.Events this.EventHandlers.Sort(); } - // raise - if (this.EventHandlers.Count == 0) - return; + // raise event this.PerformanceMonitor.Track(this.EventName, () => { foreach (ManagedEventHandler handler in this.EventHandlers) { + if (match != null && !match(handler.SourceMod)) + continue; + try { this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); @@ -119,30 +125,6 @@ namespace StardewModdingAPI.Framework.Events }); } - /// Raise the event and notify all handlers. - /// The event arguments to pass. - /// A lambda which returns true if the event should be raised for the given mod. - public void RaiseForMods(TEventArgs args, Func match) - { - if (this.EventHandlers.Count == 0) - return; - - foreach (ManagedEventHandler handler in this.EventHandlers) - { - if (match(handler.SourceMod)) - { - try - { - handler.Handler.Invoke(null, args); - } - catch (Exception ex) - { - this.LogError(handler, ex); - } - } - } - } - /********* ** Private methods diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 2a30b595..23358afb 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -240,7 +240,7 @@ namespace StardewModdingAPI.Framework modIDs.Remove(message.FromModID); // don't send a broadcast back to the sender // raise events - this.Events.ModMessageReceived.RaiseForMods(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); + this.Events.ModMessageReceived.Raise(new ModMessageReceivedEventArgs(message), mod => mod != null && modIDs.Contains(mod.Manifest.UniqueID)); } /// A callback invoked when custom content is removed from the save data to avoid a crash. -- cgit From 6d1cd7d9b884bddd00675dfdca9f63dc7db1bd1f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 22:14:17 -0400 Subject: fix merge, update release notes --- docs/release-notes.md | 2 ++ src/SMAPI/Framework/Events/ModMultiplayerEvents.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6c9a9649..dd87c1fc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -18,6 +18,7 @@ * For modders: * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. * Added ability to override update keys from the compatibility list. @@ -29,6 +30,7 @@ * Fixed `helper.Reflection` blocking access to game methods/properties that were extended by SMAPI. * Fixed asset propagation for Gil's portraits. * Fixed `.pdb` files ignored for error stack traces for mods rewritten by SMAPI. + * Fixed `ModMessageReceived` event handlers not tracked for performance monitoring. * For SMAPI developers: * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. diff --git a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs index 64cf0355..2f9b9482 100644 --- a/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs +++ b/src/SMAPI/Framework/Events/ModMultiplayerEvents.cs @@ -19,7 +19,7 @@ namespace StardewModdingAPI.Framework.Events /// Raised after a peer connection is approved by the game. public event EventHandler PeerConnected { - add => this.EventManager.PeerConnected.Add(value); + add => this.EventManager.PeerConnected.Add(value, this.Mod); remove => this.EventManager.PeerConnected.Remove(value); } -- cgit From dcd2c647a2abd836e8ee20f8ddad6568c9b4fbf2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 15 Jun 2020 22:17:32 -0400 Subject: temporarily restore Harmony 1.x support with compile flag (#711) --- docs/release-notes.md | 10 ++- docs/technical/smapi.md | 1 + .../Framework/Commands/HarmonySummaryCommand.cs | 2 + .../ModLoading/RewriteFacades/AccessToolsFacade.cs | 2 + .../RewriteFacades/HarmonyInstanceFacade.cs | 2 + .../RewriteFacades/HarmonyMethodFacade.cs | 2 + .../Rewriters/Harmony1AssemblyRewriter.cs | 2 + src/SMAPI/Framework/Patching/GamePatcher.cs | 8 ++ src/SMAPI/Framework/Patching/IHarmonyPatch.cs | 8 ++ src/SMAPI/Framework/Patching/PatchHelper.cs | 36 +++++++++ src/SMAPI/Framework/SCore.cs | 2 + src/SMAPI/Metadata/InstructionMetadata.cs | 6 ++ src/SMAPI/Patches/DialogueErrorPatch.cs | 94 +++++++++++++++++++++- src/SMAPI/Patches/EventErrorPatch.cs | 48 ++++++++++- src/SMAPI/Patches/LoadContextPatch.cs | 8 ++ src/SMAPI/Patches/LoadErrorPatch.cs | 8 ++ src/SMAPI/Patches/ObjectErrorPatch.cs | 50 +++++++++++- src/SMAPI/Patches/ScheduleErrorPatch.cs | 50 +++++++++++- src/SMAPI/SMAPI.csproj | 2 +- 19 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 src/SMAPI/Framework/Patching/PatchHelper.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index dd87c1fc..c47ee835 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,12 @@ ← [README](README.md) # Release notes -## Upcoming released +## Upcoming release + 1 +* For modders: + * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. + +## Upcoming release * For players: * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. @@ -17,12 +22,10 @@ * Internal changes to improve performance and reliability. * For modders: - * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. * Added ability to override update keys from the compatibility list. - * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. @@ -33,6 +36,7 @@ * Fixed `ModMessageReceived` event handlers not tracked for performance monitoring. * For SMAPI developers: + * Added support for bundling a custom Harmony build for upcoming use. * Eliminated MongoDB storage in the web services, which complicated the code unnecessarily. The app still uses an abstract interface for storage, so we can wrap a distributed cache in the future if needed. * Overhauled update checks to simplify individual clients, centralize common logic, and enable upcoming features. * Merged the separate legacy redirects app on AWS into the main app on Azure. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index ca8a9c70..3b2d6e56 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -58,6 +58,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. +`HARMONY_2` | Whether to enable experimental Harmony 2.0 support. Existing Harmony 1._x_ mods will be rewritten automatically for compatibility. ## For SMAPI developers ### Compiling from source diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 08233feb..8c20fbdd 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Linq; @@ -166,3 +167,4 @@ namespace StardewModdingAPI.Framework.Commands } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs index 8e4320b3..102f3364 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/AccessToolsFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -40,3 +41,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs index 54b91679..ad6d5e4f 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -80,3 +81,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs index 44c97401..f3975558 100644 --- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs +++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyMethodFacade.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; @@ -43,3 +44,4 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades } } } +#endif diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs index 8fed170a..b30d686e 100644 --- a/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Rewriters/Harmony1AssemblyRewriter.cs @@ -1,3 +1,4 @@ +#if HARMONY_2 using System; using HarmonyLib; using Mono.Cecil; @@ -125,3 +126,4 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters } } } +#endif diff --git a/src/SMAPI/Framework/Patching/GamePatcher.cs b/src/SMAPI/Framework/Patching/GamePatcher.cs index dcad285a..82d7b9c8 100644 --- a/src/SMAPI/Framework/Patching/GamePatcher.cs +++ b/src/SMAPI/Framework/Patching/GamePatcher.cs @@ -1,5 +1,9 @@ using System; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Patching { @@ -27,7 +31,11 @@ namespace StardewModdingAPI.Framework.Patching /// The patches to apply. public void Apply(params IHarmonyPatch[] patches) { +#if HARMONY_2 Harmony harmony = new Harmony("SMAPI"); +#else + HarmonyInstance harmony = HarmonyInstance.Create("SMAPI"); +#endif foreach (IHarmonyPatch patch in patches) { try diff --git a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs index 7d5eb3d4..922243fa 100644 --- a/src/SMAPI/Framework/Patching/IHarmonyPatch.cs +++ b/src/SMAPI/Framework/Patching/IHarmonyPatch.cs @@ -1,4 +1,8 @@ +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Patching { @@ -10,6 +14,10 @@ namespace StardewModdingAPI.Framework.Patching /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 void Apply(Harmony harmony); +#else + void Apply(HarmonyInstance harmony); +#endif } } diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs new file mode 100644 index 00000000..d1aa0185 --- /dev/null +++ b/src/SMAPI/Framework/Patching/PatchHelper.cs @@ -0,0 +1,36 @@ +#if !HARMONY_2 +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Patching +{ + /// Provides generic methods for implementing Harmony patches. + internal class PatchHelper + { + /********* + ** Fields + *********/ + /// The interception keys currently being intercepted. + private static readonly HashSet InterceptingKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Track a method that will be intercepted. + /// The intercept key. + /// Returns false if the method was already marked for interception, else true. + public static bool StartIntercept(string key) + { + return PatchHelper.InterceptingKeys.Add(key); + } + + /// Track a method as no longer being intercepted. + /// The intercept key. + public static void StopIntercept(string key) + { + PatchHelper.InterceptingKeys.Remove(key); + } + } +} +#endif diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 530b6754..1a2c97f4 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,7 +511,9 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) +#if HARMONY_2 .Add(new HarmonySummaryCommand(), this.Monitor) +#endif .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs index 89430a11..79d7a7a8 100644 --- a/src/SMAPI/Metadata/InstructionMetadata.cs +++ b/src/SMAPI/Metadata/InstructionMetadata.cs @@ -38,8 +38,10 @@ namespace StardewModdingAPI.Metadata // rewrite for Stardew Valley 1.3 yield return new StaticFieldToConstantRewriter(typeof(Game1), "tileSize", Game1.tileSize); +#if HARMONY_2 // rewrite for SMAPI 3.6 (Harmony 1.x => 2.0 update) yield return new Harmony1AssemblyRewriter(); +#endif /**** ** detect mod issues @@ -51,7 +53,11 @@ namespace StardewModdingAPI.Metadata /**** ** detect code which may impact game stability ****/ +#if HARMONY_2 yield return new TypeFinder(typeof(HarmonyLib.Harmony).FullName, InstructionHandleResult.DetectedGamePatch); +#else + yield return new TypeFinder(typeof(Harmony.HarmonyInstance).FullName, InstructionHandleResult.DetectedGamePatch); +#endif yield return new TypeFinder("System.Runtime.CompilerServices.CallSite", InstructionHandleResult.DetectedDynamic); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.serializer), InstructionHandleResult.DetectedSaveSerializer); yield return new FieldFinder(typeof(SaveGame).FullName, nameof(SaveGame.farmerSerializer), InstructionHandleResult.DetectedSaveSerializer); diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index cddf29d6..8043eda3 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewValley; +#if HARMONY_2 +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -47,6 +52,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) { harmony.Patch( @@ -58,11 +64,24 @@ namespace StardewModdingAPI.Patches finalizer: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Finalize_NPC_CurrentDialogue)) ); } - +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_Dialogue_Constructor)) + ); + harmony.Patch( + original: AccessTools.Property(typeof(NPC), nameof(NPC.CurrentDialogue)).GetMethod, + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Before_NPC_CurrentDialogue)) + ); + } +#endif /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call after the Dialogue constructor. /// The instance being patched. /// The dialogue being parsed. @@ -102,5 +121,74 @@ namespace StardewModdingAPI.Patches return null; } +#else + + /// The method to call instead of the Dialogue constructor. + /// The instance being patched. + /// The dialogue being parsed. + /// The NPC for which the dialogue is being parsed. + /// Returns whether to execute the original method. + private static bool Before_Dialogue_Constructor(Dialogue __instance, string masterDialogue, NPC speaker) + { + // get private members + bool nameArraysTranslated = DialogueErrorPatch.Reflection.GetField(typeof(Dialogue), "nameArraysTranslated").GetValue(); + IReflectedMethod translateArraysOfStrings = DialogueErrorPatch.Reflection.GetMethod(typeof(Dialogue), "TranslateArraysOfStrings"); + IReflectedMethod parseDialogueString = DialogueErrorPatch.Reflection.GetMethod(__instance, "parseDialogueString"); + IReflectedMethod checkForSpecialDialogueAttributes = DialogueErrorPatch.Reflection.GetMethod(__instance, "checkForSpecialDialogueAttributes"); + IReflectedField> dialogues = DialogueErrorPatch.Reflection.GetField>(__instance, "dialogues"); + + // replicate base constructor + if (dialogues.GetValue() == null) + dialogues.SetValue(new List()); + + // duplicate code with try..catch + try + { + if (!nameArraysTranslated) + translateArraysOfStrings.Invoke(); + __instance.speaker = speaker; + parseDialogueString.Invoke(masterDialogue); + checkForSpecialDialogueAttributes.Invoke(); + } + catch (Exception baseEx) when (baseEx.InnerException is TargetInvocationException invocationEx && invocationEx.InnerException is Exception ex) + { + string name = !string.IsNullOrWhiteSpace(speaker?.Name) ? speaker.Name : null; + DialogueErrorPatch.MonitorForGame.Log($"Failed parsing dialogue string{(name != null ? $" for {name}" : "")}:\n{masterDialogue}\n{ex}", LogLevel.Error); + + parseDialogueString.Invoke("..."); + checkForSpecialDialogueAttributes.Invoke(); + } + + return false; + } + + /// The method to call instead of . + /// The instance being patched. + /// The return value of the original method. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_NPC_CurrentDialogue); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Stack)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) + { + DialogueErrorPatch.MonitorForGame.Log($"Failed loading current dialogue for NPC {__instance.Name}:\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Stack(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index de9dea29..4dbb25f3 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -1,6 +1,11 @@ -using System; using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 +using System; using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -38,6 +43,7 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) { harmony.Patch( @@ -45,11 +51,21 @@ namespace StardewModdingAPI.Patches finalizer: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Finalize_GameLocation_CheckEventPrecondition)) ); } +#else + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition)) + ); + } +#endif /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call instead of the GameLocation.CheckEventPrecondition. /// The return value of the original method. /// The precondition to be parsed. @@ -65,5 +81,35 @@ namespace StardewModdingAPI.Patches return null; } +#else + /// The method to call instead of the GameLocation.CheckEventPrecondition. + /// The instance being patched. + /// The return value of the original method. + /// The precondition to be parsed. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) + { + const string key = nameof(Before_GameLocation_CheckEventPrecondition); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs index 9c707676..768ddd6b 100644 --- a/src/SMAPI/Patches/LoadContextPatch.cs +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -1,6 +1,10 @@ using System; using System.Diagnostics.CodeAnalysis; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; @@ -47,7 +51,11 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { // detect CreatedBasicInfo harmony.Patch( diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index f8ad6693..5e67b169 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -2,7 +2,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -49,7 +53,11 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { harmony.Patch( original: AccessTools.Method(typeof(SaveGame), nameof(SaveGame.loadDataToLocations)), diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 189a14a0..4edcc64e 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,11 +1,16 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; using StardewModdingAPI.Framework.Patching; using StardewValley; using StardewValley.Menus; using SObject = StardewValley.Object; +#if HARMONY_2 +using System; +using HarmonyLib; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -27,7 +32,11 @@ namespace StardewModdingAPI.Patches *********/ /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { // object.getDescription harmony.Patch( @@ -38,7 +47,11 @@ namespace StardewModdingAPI.Patches // object.getDisplayName harmony.Patch( original: AccessTools.Method(typeof(SObject), "loadDisplayName"), +#if HARMONY_2 finalizer: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Finalize_Object_loadDisplayName)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName)) +#endif ); // IClickableMenu.drawToolTip @@ -68,6 +81,7 @@ namespace StardewModdingAPI.Patches return true; } +#if HARMONY_2 /// The method to call after . /// The patched method's return value. /// The exception thrown by the wrapped method, if any. @@ -82,6 +96,38 @@ namespace StardewModdingAPI.Patches return __exception; } +#else + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_Object_loadDisplayName); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (string)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException) + { + __result = "???"; + return false; + } + catch + { + return true; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif /// The method to call instead of . /// The item for which to draw a tooltip. diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index df6ffab3..cc2238b0 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -1,10 +1,15 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using HarmonyLib; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Patching; using StardewValley; +#if HARMONY_2 +using System; +using HarmonyLib; +using StardewModdingAPI.Framework; +#else +using System.Reflection; +using Harmony; +#endif namespace StardewModdingAPI.Patches { @@ -40,11 +45,19 @@ namespace StardewModdingAPI.Patches /// Apply the Harmony patch. /// The Harmony instance. +#if HARMONY_2 public void Apply(Harmony harmony) +#else + public void Apply(HarmonyInstance harmony) +#endif { harmony.Patch( original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), +#if HARMONY_2 finalizer: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Finalize_NPC_parseMasterSchedule)) +#else + prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) +#endif ); } @@ -52,6 +65,7 @@ namespace StardewModdingAPI.Patches /********* ** Private methods *********/ +#if HARMONY_2 /// The method to call instead of . /// The raw schedule data to parse. /// The instance being patched. @@ -68,5 +82,35 @@ namespace StardewModdingAPI.Patches return null; } +#else + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_NPC_parseMasterSchedule); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); + return false; + } + catch (TargetInvocationException ex) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Dictionary(); + return false; + } + finally + { + PatchHelper.StopIntercept(key); + } + } +#endif } } diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 603b6fb5..c17de6d0 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,7 +14,7 @@ - + -- cgit From f63f14c70369541311bb5034894409a5170d56e9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 Jun 2020 18:53:29 -0400 Subject: fix typo --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 1a2c97f4..2794002c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -994,7 +994,7 @@ namespace StardewModdingAPI.Framework } catch (SAssemblyLoadFailedException ex) { - errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}"; + errorReasonPhrase = $"its DLL couldn't be loaded: {ex.Message}"; return false; } catch (Exception ex) -- cgit From c41b92f721bc61f3dd21e56f86557d7cb185197a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 16 Jun 2020 20:14:27 -0400 Subject: improve new event code This commit... * debounces the has-custom-priorities check; * fixes collection-modified-during-enumeration errors if an event handler is added or removed while the event is being raised; * fixes Remove(handler) removing all instances of the handler instead of the last one. --- src/SMAPI/Framework/Events/ManagedEvent.cs | 56 +++++++++++++---------- src/SMAPI/Framework/Events/ManagedEventHandler.cs | 6 --- 2 files changed, 33 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index b37fb376..08ac1131 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -14,23 +14,23 @@ namespace StardewModdingAPI.Framework.Events /********* ** Fields *********/ - /// The underlying event handlers. - private readonly List> EventHandlers = new List>(); - /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; /// Tracks performance metrics. private readonly PerformanceMonitor PerformanceMonitor; + /// The underlying event handlers. + private readonly List> Handlers = new List>(); + + /// A cached snapshot of , or null to rebuild it next raise. + private ManagedEventHandler[] CachedHandlers = new ManagedEventHandler[0]; + /// The total number of event handlers registered for this events, regardless of whether they're still registered. private int RegistrationIndex; - /// Whether any registered event handlers have a custom priority value. - private bool HasCustomPriorities; - - /// Whether event handlers should be sorted before the next invocation. - private bool NeedsSort; + /// Whether new handlers were added since the last raise. + private bool HasNewHandlers; /********* @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Framework.Events /// Get whether anything is listening to the event. public bool HasListeners() { - return this.EventHandlers.Count > 0; + return this.Handlers.Count > 0; } /// Add an event handler. @@ -73,19 +73,25 @@ namespace StardewModdingAPI.Framework.Events EventPriority priority = handler.Method.GetCustomAttribute()?.Priority ?? EventPriority.Normal; var managedHandler = new ManagedEventHandler(handler, this.RegistrationIndex++, priority, mod); - this.EventHandlers.Add(managedHandler); - this.HasCustomPriorities = this.HasCustomPriorities || managedHandler.HasCustomPriority(); - - if (this.HasCustomPriorities) - this.NeedsSort = true; + this.Handlers.Add(managedHandler); + this.CachedHandlers = null; + this.HasNewHandlers = true; } /// Remove an event handler. /// The event handler. public void Remove(EventHandler handler) { - this.EventHandlers.RemoveAll(p => p.Handler == handler); - this.HasCustomPriorities = this.HasCustomPriorities && this.EventHandlers.Any(p => p.HasCustomPriority()); + // match C# events: if a handler is listed multiple times, remove the last one added + for (int i = this.Handlers.Count - 1; i >= 0; i--) + { + if (this.Handlers[i].Handler != handler) + continue; + + this.Handlers.RemoveAt(i); + this.CachedHandlers = null; + break; + } } /// Raise the event and notify all handlers. @@ -94,21 +100,25 @@ namespace StardewModdingAPI.Framework.Events public void Raise(TEventArgs args, Func match = null) { // skip if no handlers - if (this.EventHandlers.Count == 0) + if (this.Handlers.Count == 0) return; - // sort event handlers by priority - // (This is done here to avoid repeatedly sorting when handlers are added/removed.) - if (this.NeedsSort) + // update cached data + // (This is debounced here to avoid repeatedly sorting when handlers are added/removed, + // and keeping a separate cached list allows changes during enumeration.) + if (this.CachedHandlers == null) { - this.NeedsSort = false; - this.EventHandlers.Sort(); + if (this.HasNewHandlers && this.Handlers.Any(p => p.Priority != EventPriority.Normal)) + this.Handlers.Sort(); + + this.CachedHandlers = this.Handlers.ToArray(); + this.HasNewHandlers = false; } // raise event this.PerformanceMonitor.Track(this.EventName, () => { - foreach (ManagedEventHandler handler in this.EventHandlers) + foreach (ManagedEventHandler handler in this.CachedHandlers) { if (match != null && !match(handler.SourceMod)) continue; diff --git a/src/SMAPI/Framework/Events/ManagedEventHandler.cs b/src/SMAPI/Framework/Events/ManagedEventHandler.cs index 87591f63..cf470c1e 100644 --- a/src/SMAPI/Framework/Events/ManagedEventHandler.cs +++ b/src/SMAPI/Framework/Events/ManagedEventHandler.cs @@ -39,12 +39,6 @@ namespace StardewModdingAPI.Framework.Events this.SourceMod = sourceMod; } - /// Get whether the event handler has a custom priority value. - public bool HasCustomPriority() - { - return this.Priority != EventPriority.Normal; - } - /// Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes, follows, or occurs in the same position in the sort order as the other object. /// An object to compare with this instance. /// is not the same type as this instance. -- cgit From aeab19f4aca366be3446b9f1ee097d51d21b5fde Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 19 Jun 2020 21:28:44 -0400 Subject: backport harmony_summary command to Harmony 1.x (#711) --- docs/release-notes.md | 4 ++- .../Framework/Commands/HarmonySummaryCommand.cs | 38 ++++++++++++++++++++-- src/SMAPI/Framework/SCore.cs | 2 -- 3 files changed, 38 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index c47ee835..9ea3e445 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,10 +1,11 @@ ← [README](README.md) # Release notes + ## Upcoming release * For players: @@ -25,6 +26,7 @@ * Added [event priorities](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events#Custom_priority) (thanks to spacechase0!). * Added [update subkeys](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Update_checks#Update_subkeys). * Added `Multiplayer.PeerConnected` event. + * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Added ability to override update keys from the compatibility list. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 8c20fbdd..8fdd4282 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -1,16 +1,27 @@ -#if HARMONY_2 using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +#if HARMONY_2 using HarmonyLib; +#else +using Harmony; +#endif namespace StardewModdingAPI.Framework.Commands { /// The 'harmony_summary' SMAPI console command. internal class HarmonySummaryCommand : IInternalCommand { +#if !HARMONY_2 + /********* + ** Fields + *********/ + /// The Harmony instance through which to fetch patch info. + private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}"); +#endif + /********* ** Accessors *********/ @@ -45,7 +56,16 @@ namespace StardewModdingAPI.Framework.Commands foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key)) { var sortedTypes = ownerGroup.Value - .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); + .OrderBy(p => p switch + { + PatchType.Prefix => 0, + PatchType.Postfix => 1, +#if HARMONY_2 + PatchType.Finalizer => 2, +#endif + PatchType.Transpiler => 3, + _ => 4 + }); result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); } @@ -91,15 +111,26 @@ namespace StardewModdingAPI.Framework.Commands /// Get all current Harmony patches. private IEnumerable GetAllPatches() { +#if HARMONY_2 foreach (MethodBase method in Harmony.GetAllPatchedMethods()) +#else + foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods()) +#endif { // get metadata for method +#if HARMONY_2 HarmonyLib.Patches patchInfo = Harmony.GetPatchInfo(method); +#else + Harmony.Patches patchInfo = this.HarmonyInstance.GetPatchInfo(method); +#endif + IDictionary> patchGroups = new Dictionary> { [PatchType.Prefix] = patchInfo.Prefixes, [PatchType.Postfix] = patchInfo.Postfixes, +#if HARMONY_2 [PatchType.Finalizer] = patchInfo.Finalizers, +#endif [PatchType.Transpiler] = patchInfo.Transpilers }; @@ -129,8 +160,10 @@ namespace StardewModdingAPI.Framework.Commands /// A postfix patch. Postfix, +#if HARMONY_2 /// A finalizer patch. Finalizer, +#endif /// A transpiler patch. Transpiler @@ -167,4 +200,3 @@ namespace StardewModdingAPI.Framework.Commands } } } -#endif diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 2794002c..e1db563c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -511,9 +511,7 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager .Add(new HelpCommand(this.GameInstance.CommandManager), this.Monitor) -#if HARMONY_2 .Add(new HarmonySummaryCommand(), this.Monitor) -#endif .Add(new ReloadI18nCommand(this.ReloadTranslations), this.Monitor); // start handling command line input -- cgit From a7cf886b7145e94dbcb1157f73aa0f2922fa183f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:13:23 -0400 Subject: switch to custom build of Harmony 1.2.0.1 --- .gitignore | 3 --- build/0Harmony.dll | Bin 0 -> 115200 bytes docs/release-notes.md | 1 + docs/technical/smapi.md | 14 +++++--------- src/SMAPI/SMAPI.csproj | 3 +-- 5 files changed, 7 insertions(+), 14 deletions(-) create mode 100644 build/0Harmony.dll (limited to 'src') diff --git a/.gitignore b/.gitignore index 02522716..b7f0d3e6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,5 @@ _ReSharper*/ # sensitive files appsettings.Development.json -# generated build files -build/0Harmony.* - # Azure generated files src/SMAPI.Web/Properties/PublishProfiles/*.pubxml diff --git a/build/0Harmony.dll b/build/0Harmony.dll new file mode 100644 index 00000000..2e893d0e Binary files /dev/null and b/build/0Harmony.dll differ diff --git a/docs/release-notes.md b/docs/release-notes.md index 9ea3e445..eadd45eb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -28,6 +28,7 @@ * Added `Multiplayer.PeerConnected` event. * Added `harmony_summary` console command which lists all current Harmony patches, optionally with a search filter. * Added ability to override update keys from the compatibility list. + * SMAPI now uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme) to provide more useful stack traces in error logs. * Harmony mods which use the `[HarmonyPatch(type)]` attribute now work crossplatform. Previously SMAPI couldn't rewrite types in custom attributes for compatibility. * Improved mod rewriting for compatibility: * Fixed rewriting types in custom attributes. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index 3b2d6e56..5d550cfc 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -58,7 +58,7 @@ SMAPI uses a small number of conditional compilation constants, which you can se flag | purpose ---- | ------- `SMAPI_FOR_WINDOWS` | Whether SMAPI is being compiled on Windows for players on Windows. Set automatically in `crossplatform.targets`. -`HARMONY_2` | Whether to enable experimental Harmony 2.0 support. Existing Harmony 1._x_ mods will be rewritten automatically for compatibility. +`HARMONY_2` | Whether to enable experimental Harmony 2.0 support and rewrite existing Harmony 1._x_ mods for compatibility. Note that you need to replace `build/0Harmony.dll` with a Harmony 2.0 build (or switch to a package reference) to use this flag. ## For SMAPI developers ### Compiling from source @@ -102,14 +102,10 @@ on the wiki for the first-time setup. 3. Rename the folders to `SMAPI installer` and `SMAPI installer for developers`. 4. Zip the two folders. -### Using a custom Harmony build -The official SMAPI releases include [a custom build of Harmony](https://github.com/Pathoschild/Harmony), -but compiling from source will use the official build. To use a custom build, put `0Harmony.dll` in -the `build` folder and it'll be referenced automatically. - -Note that Harmony merges its dependencies into `0Harmony.dll` when compiled in release mode. To use -a debug build of Harmony, you'll need to manually copy those dependencies into your game's -`smapi-internal` folder. +### Custom Harmony build +SMAPI uses [a custom build of Harmony](https://github.com/Pathoschild/Harmony#readme), which is +included in the `build` folder. To use a different build, just replace `0Harmony.dll` in that +folder. ## Release notes See [release notes](../release-notes.md). diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c17de6d0..4af4527b 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -14,14 +14,13 @@ - - + -- cgit From 067163da02c5a5993d88d80f04d379c22bc32cba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 00:50:23 -0400 Subject: make parallel rewriting optional --- docs/release-notes.md | 2 +- src/SMAPI/Constants.cs | 8 ++ src/SMAPI/Framework/ModLoading/AssemblyLoader.cs | 10 +- .../ModLoading/Framework/RecursiveRewriter.cs | 143 +++++++++++++-------- src/SMAPI/Framework/Models/SConfig.cs | 11 +- src/SMAPI/Framework/SCore.cs | 4 +- src/SMAPI/SMAPI.config.json | 20 ++- 7 files changed, 128 insertions(+), 70 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ed86f73f..3c3f0796 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,7 @@ ## Upcoming release * For players: - * Reduced startup time when loading mod DLLs (thanks to ZaneYork!). + * Added experimental option to reduce startup time when loading mod DLLs (thanks to ZaneYork!). Enable `RewriteInParallel` in the `smapi-internal/config.json` to try it. * Reduced processing time when a mod loads many unpacked images (thanks to Entoarox!). * Mod warnings are now listed alphabetically. * MacOS files starting with `._` are now ignored and can no longer cause skipped mods. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..9d510d2d 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -52,6 +52,14 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// Whether SMAPI was compiled in debug mode. + internal const bool IsDebugBuild = +#if DEBUG + true; +#else + false; +#endif + /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index dbb5f696..f8c901e0 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -76,9 +76,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The mod for which the assembly is being loaded. /// The assembly file path. /// Assume the mod is compatible, even if incompatible code is detected. + /// Whether to enable experimental parallel rewriting. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible) + public Assembly Load(IModMetadata mod, string assemblyPath, bool assumeCompatible, bool rewriteInParallel) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -108,7 +109,7 @@ namespace StardewModdingAPI.Framework.ModLoading continue; // rewrite assembly - bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " "); + bool changed = this.RewriteAssembly(mod, assembly.Definition, loggedMessages, logPrefix: " ", rewriteInParallel); // detect broken assembly reference foreach (AssemblyNameReference reference in assembly.Definition.MainModule.AssemblyReferences) @@ -262,9 +263,10 @@ namespace StardewModdingAPI.Framework.ModLoading /// The assembly to rewrite. /// The messages that have already been logged for this mod. /// A string to prefix to log messages. + /// Whether to enable experimental parallel rewriting. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix) + private bool RewriteAssembly(IModMetadata mod, AssemblyDefinition assembly, HashSet loggedMessages, string logPrefix, bool rewriteInParallel) { ModuleDefinition module = assembly.MainModule; string filename = $"{assembly.Name.Name}.dll"; @@ -313,7 +315,7 @@ namespace StardewModdingAPI.Framework.ModLoading return rewritten; } ); - bool anyRewritten = rewriter.RewriteModule(); + bool anyRewritten = rewriter.RewriteModule(rewriteInParallel); // handle rewrite flags foreach (IInstructionHandler handler in handlers) diff --git a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs index 9dc3680f..34c78c7d 100644 --- a/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs +++ b/src/SMAPI/Framework/ModLoading/Framework/RecursiveRewriter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -56,15 +57,20 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework } /// Rewrite the loaded module code. + /// Whether to enable experimental parallel rewriting. /// Returns whether the module was modified. - public bool RewriteModule() + public bool RewriteModule(bool rewriteInParallel) { - int typesChanged = 0; - Exception exception = null; + IEnumerable types = this.Module.GetTypes().Where(type => type.BaseType != null); // skip special types like - Parallel.ForEach( - source: this.Module.GetTypes().Where(type => type.BaseType != null), // skip special types like - body: type => + // experimental parallel rewriting + // This may cause intermittent startup errors and is disabled by default: https://github.com/Pathoschild/SMAPI/issues/721 + if (rewriteInParallel) + { + int typesChanged = 0; + Exception exception = null; + + Parallel.ForEach(types, type => { if (exception != null) return; @@ -72,50 +78,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework bool changed = false; try { - changed |= this.RewriteCustomAttributes(type.CustomAttributes); - changed |= this.RewriteGenericParameters(type.GenericParameters); - - foreach (InterfaceImplementation @interface in type.Interfaces) - changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); - - if (type.BaseType.FullName != "System.Object") - changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); - - foreach (MethodDefinition method in type.Methods) - { - changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); - changed |= this.RewriteGenericParameters(method.GenericParameters); - changed |= this.RewriteCustomAttributes(method.CustomAttributes); - - foreach (ParameterDefinition parameter in method.Parameters) - changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); - - foreach (var methodOverride in method.Overrides) - changed |= this.RewriteMethodReference(methodOverride); - - if (method.HasBody) - { - foreach (VariableDefinition variable in method.Body.Variables) - changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); - - // check CIL instructions - ILProcessor cil = method.Body.GetILProcessor(); - Collection instructions = cil.Body.Instructions; - for (int i = 0; i < instructions.Count; i++) - { - var instruction = instructions[i]; - if (instruction.OpCode.Code == Code.Nop) - continue; - - changed |= this.RewriteInstruction(instruction, cil, newInstruction => - { - changed = true; - cil.Replace(instruction, newInstruction); - instruction = newInstruction; - }); - } - } - } + changed = this.RewriteTypeDefinition(type); } catch (Exception ex) { @@ -124,18 +87,90 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework if (changed) Interlocked.Increment(ref typesChanged); + }); + + return exception == null + ? typesChanged > 0 + : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + } + + // non-parallel rewriting + { + bool changed = false; + + try + { + foreach (var type in types) + changed |= this.RewriteTypeDefinition(type); + } + catch (Exception ex) + { + throw new Exception($"Rewriting {this.Module.Name} failed.", ex); } - ); - return exception == null - ? typesChanged > 0 - : throw new Exception($"Rewriting {this.Module.Name} failed.", exception); + return changed; + } } /********* ** Private methods *********/ + /// Rewrite a loaded type definition. + /// The type definition to rewrite. + /// Returns whether the type was modified. + private bool RewriteTypeDefinition(TypeDefinition type) + { + bool changed = false; + + changed |= this.RewriteCustomAttributes(type.CustomAttributes); + changed |= this.RewriteGenericParameters(type.GenericParameters); + + foreach (InterfaceImplementation @interface in type.Interfaces) + changed |= this.RewriteTypeReference(@interface.InterfaceType, newType => @interface.InterfaceType = newType); + + if (type.BaseType.FullName != "System.Object") + changed |= this.RewriteTypeReference(type.BaseType, newType => type.BaseType = newType); + + foreach (MethodDefinition method in type.Methods) + { + changed |= this.RewriteTypeReference(method.ReturnType, newType => method.ReturnType = newType); + changed |= this.RewriteGenericParameters(method.GenericParameters); + changed |= this.RewriteCustomAttributes(method.CustomAttributes); + + foreach (ParameterDefinition parameter in method.Parameters) + changed |= this.RewriteTypeReference(parameter.ParameterType, newType => parameter.ParameterType = newType); + + foreach (var methodOverride in method.Overrides) + changed |= this.RewriteMethodReference(methodOverride); + + if (method.HasBody) + { + foreach (VariableDefinition variable in method.Body.Variables) + changed |= this.RewriteTypeReference(variable.VariableType, newType => variable.VariableType = newType); + + // check CIL instructions + ILProcessor cil = method.Body.GetILProcessor(); + Collection instructions = cil.Body.Instructions; + for (int i = 0; i < instructions.Count; i++) + { + var instruction = instructions[i]; + if (instruction.OpCode.Code == Code.Nop) + continue; + + changed |= this.RewriteInstruction(instruction, cil, newInstruction => + { + changed = true; + cil.Replace(instruction, newInstruction); + instruction = newInstruction; + }); + } + } + } + + return changed; + } + /// Rewrite a CIL instruction if needed. /// The current CIL instruction. /// The CIL instruction processor. diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs index b1612aa4..a98d8c54 100644 --- a/src/SMAPI/Framework/Models/SConfig.cs +++ b/src/SMAPI/Framework/Models/SConfig.cs @@ -15,12 +15,8 @@ namespace StardewModdingAPI.Framework.Models private static readonly IDictionary DefaultValues = new Dictionary { [nameof(CheckForUpdates)] = true, - [nameof(ParanoidWarnings)] = -#if DEBUG - true, -#else - false, -#endif + [nameof(ParanoidWarnings)] = Constants.IsDebugBuild, + [nameof(RewriteInParallel)] = Constants.IsDebugBuild, [nameof(UseBetaChannel)] = Constants.ApiVersion.IsPrerelease(), [nameof(GitHubProjectName)] = "Pathoschild/SMAPI", [nameof(WebApiBaseUrl)] = "https://smapi.io/api/", @@ -45,6 +41,9 @@ namespace StardewModdingAPI.Framework.Models /// Whether to check for newer versions of SMAPI and mods on startup. public bool CheckForUpdates { get; set; } + /// Whether to enable experimental parallel rewriting. + public bool RewriteInParallel { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteInParallel)]; + /// Whether to add a section to the 'mod issues' list for mods which which directly use potentially sensitive .NET APIs like file or shell access. public bool ParanoidWarnings { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.ParanoidWarnings)]; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index e1db563c..90435f54 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -337,6 +337,8 @@ namespace StardewModdingAPI.Framework // add headers if (this.Settings.DeveloperMode) this.Monitor.Log($"You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info); + if (this.Settings.RewriteInParallel) + this.Monitor.Log($"You enabled experimental parallel rewriting. This may result in faster startup times, but intermittent startup errors. You can disable it by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Info); if (!this.Settings.CheckForUpdates) this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) @@ -981,7 +983,7 @@ namespace StardewModdingAPI.Framework Assembly modAssembly; try { - modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible); + modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible, rewriteInParallel: this.Settings.RewriteInParallel); this.ModRegistry.TrackAssemblies(mod, modAssembly); } catch (IncompatibleInstructionException) // details already in trace logs diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index a426b0ef..0a6d8372 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -33,18 +33,30 @@ copy all the settings, or you may cause bugs due to overridden changes in future */ "DeveloperMode": true, + /** + * Whether to enable experimental parallel rewriting when SMAPI is loading mods. This can + * reduce startup time when you have many mods installed, but is experimental and may cause + * intermittent startup errors. + * + * When this is commented out, it'll be true for local debug builds and false otherwise. + */ + //"RewriteInParallel": false, + /** * Whether to add a section to the 'mod issues' list for mods which directly use potentially * sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as * part of their normal functionality, so these warnings are meaningless without further - * investigation. When this is commented out, it'll be true for local debug builds and false - * otherwise. + * investigation. + * + * When this is commented out, it'll be true for local debug builds and false otherwise. */ //"ParanoidWarnings": true, /** - * Whether SMAPI should show newer beta versions as an available update. When this is commented - * out, it'll be true if the current SMAPI version is beta, and false otherwise. + * Whether SMAPI should show newer beta versions as an available update. + * + * When this is commented out, it'll be true if the current SMAPI version is beta, and false + * otherwise. */ //"UseBetaChannel": true, -- cgit From b32cad4046916344665c67923482db09cacb366f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:13:23 -0400 Subject: add i18n schema to JSON validator --- docs/release-notes.md | 1 + docs/technical/web.md | 5 +++-- .../Controllers/JsonValidatorController.cs | 3 ++- src/SMAPI.Web/wwwroot/schemas/i18n.json | 24 ++++++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI.Web/wwwroot/schemas/i18n.json (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 3c3f0796..43e1011c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ * For the web UI: * Added GitHub licenses to mod compatibility list. + * Added SMAPI `i18n` schema to JSON validator. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. diff --git a/docs/technical/web.md b/docs/technical/web.md index d21b87ac..50237bfe 100644 --- a/docs/technical/web.md +++ b/docs/technical/web.md @@ -110,8 +110,9 @@ Available schemas: format | schema URL ------ | ---------- -[SMAPI `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json -[Content Patcher `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json +[SMAPI: `manifest.json`](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Manifest) | https://smapi.io/schemas/manifest.json +[SMAPI: translations (`i18n` folder)](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation) | https://smapi.io/schemas/i18n.json +[Content Patcher: `content.json`](https://github.com/Pathoschild/StardewMods/tree/develop/ContentPatcher#readme) | https://smapi.io/schemas/content-patcher.json ## Web API ### Overview diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index c43fb929..8fb9e06a 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -27,7 +27,8 @@ namespace StardewModdingAPI.Web.Controllers private readonly IDictionary SchemaFormats = new Dictionary { ["none"] = "None", - ["manifest"] = "Manifest", + ["manifest"] = "SMAPI: manifest", + ["i18n"] = "SMAPI: translations (i18n)", ["content-patcher"] = "Content Patcher" }; diff --git a/src/SMAPI.Web/wwwroot/schemas/i18n.json b/src/SMAPI.Web/wwwroot/schemas/i18n.json new file mode 100644 index 00000000..493ad213 --- /dev/null +++ b/src/SMAPI.Web/wwwroot/schemas/i18n.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://smapi.io/schemas/i18n.json", + "title": "SMAPI i18n file", + "description": "A translation file for a SMAPI mod or content pack.", + "@documentationUrl": "https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Translation", + "type": "object", + + "properties": { + "$schema": { + "title": "Schema", + "description": "A reference to this JSON schema. Not part of the actual format, but useful for validation tools.", + "type": "string", + "const": "https://smapi.io/schemas/manifest.json" + } + }, + + "additionalProperties": { + "type": "string", + "@errorMessages": { + "type": "Invalid property. Translation files can only contain text property values." + } + } +} -- cgit From d02a40de997edf493354e85eb018ded84eb8f782 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:31:01 -0400 Subject: change default JSON validator schema to none --- docs/release-notes.md | 4 +++- src/SMAPI.Web/Controllers/JsonValidatorController.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 43e1011c..2780e5ad 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,7 +20,9 @@ * For the web UI: * Added GitHub licenses to mod compatibility list. - * Added SMAPI `i18n` schema to JSON validator. + * Improved JSON validator: + * added SMAPI `i18n` schema; + * changed default schema to plain JSON. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index 8fb9e06a..b76d41a3 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI.Web.Controllers }; /// The schema ID to use if none was specified. - private string DefaultSchemaID = "manifest"; + private string DefaultSchemaID = "none"; /// A token in an error message which indicates that the child errors should be displayed instead. private readonly string TransparentToken = "$transparent"; -- cgit From ed3309e7bb8d5f3f6c3d08df3475bd811d5b16d0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 Jun 2020 11:36:22 -0400 Subject: remember selected schema when editing a file --- docs/release-notes.md | 1 + .../Controllers/JsonValidatorController.cs | 25 ++++++++++++++-------- .../ViewModels/JsonValidator/JsonValidatorModel.cs | 7 +++++- src/SMAPI.Web/Views/JsonValidator/Index.cshtml | 9 ++++---- 4 files changed, 27 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 2780e5ad..2bb4dc84 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Added GitHub licenses to mod compatibility list. * Improved JSON validator: * added SMAPI `i18n` schema; + * editing an uploaded file now remembers the selected schema; * changed default schema to plain JSON. * Updated ModDrop URLs. * Internal changes to improve performance and reliability. diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index b76d41a3..5f83eafd 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -58,16 +58,22 @@ namespace StardewModdingAPI.Web.Controllers /// Render the schema validator UI. /// The schema name with which to validate the JSON, or 'edit' to return to the edit screen. /// The stored file ID. + /// The operation to perform for the selected log ID. This can be 'edit', or any other value to view. [HttpGet] [Route("json")] [Route("json/{schemaName}")] [Route("json/{schemaName}/{id}")] - public async Task Index(string schemaName = null, string id = null) + [Route("json/{schemaName}/{id}/{operation}")] + public async Task Index(string schemaName = null, string id = null, string operation = null) { + // parse arguments schemaName = this.NormalizeSchemaName(schemaName); + bool hasId = !string.IsNullOrWhiteSpace(id); + bool isEditView = !hasId || operation?.Trim().ToLower() == "edit"; - var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats); - if (string.IsNullOrWhiteSpace(id)) + // build result model + var result = this.GetModel(id, schemaName, isEditView); + if (!hasId) return this.View("Index", result); // fetch raw JSON @@ -77,7 +83,7 @@ namespace StardewModdingAPI.Web.Controllers result.SetContent(file.Content, expiry: file.Expiry, uploadWarning: file.Warning); // skip parsing if we're going to the edit screen - if (schemaName?.ToLower() == "edit") + if (isEditView) return this.View("Index", result); // parse JSON @@ -131,7 +137,7 @@ namespace StardewModdingAPI.Web.Controllers public async Task PostAsync(JsonValidatorRequestModel request) { if (request == null) - return this.View("Index", this.GetModel(null, null).SetUploadError("The request seems to be invalid.")); + return this.View("Index", this.GetModel(null, null, isEditView: true).SetUploadError("The request seems to be invalid.")); // normalize schema name string schemaName = this.NormalizeSchemaName(request.SchemaName); @@ -139,12 +145,12 @@ namespace StardewModdingAPI.Web.Controllers // get raw text string input = request.Content; if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", this.GetModel(null, schemaName).SetUploadError("The JSON file seems to be empty.")); + return this.View("Index", this.GetModel(null, schemaName, isEditView: true).SetUploadError("The JSON file seems to be empty.")); // upload file UploadResult result = await this.Storage.SaveAsync(input); if (!result.Succeeded) - return this.View("Index", this.GetModel(result.ID, schemaName).SetUploadError(result.UploadError)); + return this.View("Index", this.GetModel(result.ID, schemaName, isEditView: true).SetContent(input, null).SetUploadError(result.UploadError)); // redirect to view return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID })); @@ -157,9 +163,10 @@ namespace StardewModdingAPI.Web.Controllers /// Build a JSON validator model. /// The stored file ID. /// The schema name with which the JSON was validated. - private JsonValidatorModel GetModel(string pasteID, string schemaName) + /// Whether to show the edit view. + private JsonValidatorModel GetModel(string pasteID, string schemaName, bool isEditView) { - return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats); + return new JsonValidatorModel(pasteID, schemaName, this.SchemaFormats, isEditView); } /// Get a normalized schema name, or the if blank. diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index c0dd7184..0ea69911 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -10,6 +10,9 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /********* ** Accessors *********/ + /// Whether to show the edit view. + public bool IsEditView { get; set; } + /// The paste ID. public string PasteID { get; set; } @@ -51,11 +54,13 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /// The stored file ID. /// The schema name with which the JSON was validated. /// The supported JSON schemas (names indexed by ID). - public JsonValidatorModel(string pasteID, string schemaName, IDictionary schemaFormats) + /// Whether to show the edit view. + public JsonValidatorModel(string pasteID, string schemaName, IDictionary schemaFormats, bool isEditView) { this.PasteID = pasteID; this.SchemaName = schemaName; this.SchemaFormats = schemaFormats; + this.IsEditView = isEditView; } /// Set the validated content. diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index f23bd150..7b89a23d 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -9,7 +9,6 @@ string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName }); string schemaDisplayName = null; bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName?.ToLower() != "none"; - bool isEditView = Model.Content == null || Model.SchemaName?.ToLower() == "edit"; // build title ViewData["Title"] = "JSON validator"; @@ -63,7 +62,7 @@ else if (Model.ParseError != null) Error details: @Model.ParseError
} -else if (!isEditView && Model.PasteID != null) +else if (!Model.IsEditView && Model.PasteID != null) {