diff options
| author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-06-20 12:43:08 -0400 |
|---|---|---|
| committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-06-20 12:43:08 -0400 |
| commit | e64ecc89f94641d4054162eff4943f660f43030f (patch) | |
| tree | c43ca79f9947b3e16f946e1dc5fd1d02f70ce571 /src/SMAPI | |
| parent | df6e745c6b842290338317ed1d3e969ee222998c (diff) | |
| parent | cb9ff7019995eff92104703f097856d2523e02ce (diff) | |
| download | SMAPI-e64ecc89f94641d4054162eff4943f660f43030f.tar.gz SMAPI-e64ecc89f94641d4054162eff4943f660f43030f.tar.bz2 SMAPI-e64ecc89f94641d4054162eff4943f660f43030f.zip | |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI')
80 files changed, 2236 insertions, 984 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index a898fccd..66971709 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI ** Public ****/ /// <summary>SMAPI's current semantic version.</summary> - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.5.0"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.6.0"); /// <summary>The minimum supported version of Stardew Valley.</summary> public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); @@ -52,6 +52,14 @@ namespace StardewModdingAPI /**** ** Internal ****/ + /// <summary>Whether SMAPI was compiled in debug mode.</summary> + internal const bool IsDebugBuild = +#if DEBUG + true; +#else + false; +#endif + /// <summary>The URL of the SMAPI home page.</summary> internal const string HomePageUrl = "https://smapi.io"; diff --git a/src/SMAPI/Events/EventPriority.cs b/src/SMAPI/Events/EventPriority.cs new file mode 100644 index 00000000..1efb4e2a --- /dev/null +++ b/src/SMAPI/Events/EventPriority.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Events +{ + /// <summary>The event priorities for method handlers.</summary> + public enum EventPriority + { + /// <summary>Low priority.</summary> + Low = -1000, + + /// <summary>The default priority.</summary> + Normal = 0, + + /// <summary>High priority.</summary> + High = 1000 + } +} diff --git a/src/SMAPI/Events/EventPriorityAttribute.cs b/src/SMAPI/Events/EventPriorityAttribute.cs new file mode 100644 index 00000000..207e7862 --- /dev/null +++ b/src/SMAPI/Events/EventPriorityAttribute.cs @@ -0,0 +1,26 @@ +using System; + +namespace StardewModdingAPI.Events +{ + /// <summary>An attribute which specifies the priority for an event handler.</summary> + [AttributeUsage(AttributeTargets.Method)] + public class EventPriorityAttribute : Attribute + { + /********* + ** Accessors + *********/ + /// <summary>The event handler priority, relative to other handlers across all mods registered for this event.</summary> + internal EventPriority Priority { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="priority">The event handler priority, relative to other handlers across all mods registered for this event. Higher-priority handlers are notified before lower-priority handlers.</param> + public EventPriorityAttribute(EventPriority priority) + { + this.Priority = priority; + } + } +} 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 /// <summary>Events raised for multiplayer messages and connections.</summary> public interface IMultiplayerEvents { - /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection, so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary> + /// <summary>Raised after the mod context for a peer is received. This happens before the game approves the connection (<see cref="PeerConnected"/>), so the player doesn't yet exist in the game. This is the earliest point where messages can be sent to the peer via SMAPI.</summary> event EventHandler<PeerContextReceivedEventArgs> PeerContextReceived; + /// <summary>Raised after a peer connection is approved by the game.</summary> + event EventHandler<PeerConnectedEventArgs> PeerConnected; + /// <summary>Raised after a mod message is received over the network.</summary> event EventHandler<ModMessageReceivedEventArgs> 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 +{ + /// <summary>Event arguments for an <see cref="IMultiplayerEvents.PeerConnected"/> event.</summary> + public class PeerConnectedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// <summary>The peer whose metadata was received.</summary> + public IMultiplayerPeer Peer { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="peer">The peer whose metadata was received.</param> + internal PeerConnectedEventArgs(IMultiplayerPeer peer) + { + this.Peer = peer; + } + } +} diff --git a/src/SMAPI/Framework/CommandManager.cs b/src/SMAPI/Framework/CommandManager.cs index ceeb6f93..eaa91c86 100644 --- a/src/SMAPI/Framework/CommandManager.cs +++ b/src/SMAPI/Framework/CommandManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using StardewModdingAPI.Framework.Commands; namespace StardewModdingAPI.Framework { @@ -27,7 +28,7 @@ namespace StardewModdingAPI.Framework /// <exception cref="ArgumentNullException">The <paramref name="name"/> or <paramref name="callback"/> is null or empty.</exception> /// <exception cref="FormatException">The <paramref name="name"/> is not a valid format.</exception> /// <exception cref="ArgumentException">There's already a command with that name.</exception> - public void Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false) + public CommandManager Add(IModMetadata mod, string name, string documentation, Action<string, string[]> callback, bool allowNullCallback = false) { name = this.GetNormalizedName(name); @@ -45,6 +46,16 @@ namespace StardewModdingAPI.Framework // add command this.Commands.Add(name, new Command(mod, name, documentation, callback)); + return this; + } + + /// <summary>Add a console command.</summary> + /// <param name="command">the SMAPI console command to add.</param> + /// <param name="monitor">Writes messages to the console.</param> + /// <exception cref="ArgumentException">There's already a command with that name.</exception> + public CommandManager Add(IInternalCommand command, IMonitor monitor) + { + return this.Add(null, command.Name, command.Description, (name, args) => command.HandleCommand(args, monitor)); } /// <summary>Get a command by its unique name.</summary> diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs new file mode 100644 index 00000000..8fdd4282 --- /dev/null +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -0,0 +1,202 @@ +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 +{ + /// <summary>The 'harmony_summary' SMAPI console command.</summary> + internal class HarmonySummaryCommand : IInternalCommand + { +#if !HARMONY_2 + /********* + ** Fields + *********/ + /// <summary>The Harmony instance through which to fetch patch info.</summary> + private readonly HarmonyInstance HarmonyInstance = HarmonyInstance.Create($"SMAPI.{nameof(HarmonySummaryCommand)}"); +#endif + + /********* + ** Accessors + *********/ + /// <summary>The command name, which the user must type to trigger it.</summary> + public string Name { get; } = "harmony_summary"; + + /// <summary>The human-readable documentation shown when the player runs the built-in 'help' command.</summary> + public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary <search>\n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method."; + + + /********* + ** Public methods + *********/ + /// <summary>Handle the console command when it's entered by the user.</summary> + /// <param name="args">The command arguments.</param> + /// <param name="monitor">Writes messages to the console.</param> + public void HandleCommand(string[] args, IMonitor monitor) + { + SearchResult[] matches = this.FilterPatches(args).OrderBy(p => p.MethodName).ToArray(); + + StringBuilder result = new StringBuilder(); + + if (!matches.Any()) + result.AppendLine("No current patches match your search."); + else + { + result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:"); + result.AppendLine(); + foreach (var match in matches) + { + result.AppendLine($" {match.MethodName}"); + foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key)) + { + var sortedTypes = ownerGroup.Value + .OrderBy(p => p switch + { + PatchType.Prefix => 0, + PatchType.Postfix => 1, +#if HARMONY_2 + PatchType.Finalizer => 2, +#endif + PatchType.Transpiler => 3, + _ => 4 + }); + + result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); + } + result.AppendLine(); + } + } + + monitor.Log(result.ToString().TrimEnd(), LogLevel.Info); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get all current Harmony patches matching any of the given search terms.</summary> + /// <param name="searchTerms">The search terms to match.</param> + private IEnumerable<SearchResult> FilterPatches(string[] searchTerms) + { + bool hasSearch = searchTerms.Any(); + bool IsMatch(string target) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); + + foreach (var patch in this.GetAllPatches()) + { + // matches entire patch + if (IsMatch(patch.MethodDescription)) + { + yield return patch; + continue; + } + + // matches individual patchers + foreach (var pair in patch.PatchTypesByOwner.ToArray()) + { + if (!IsMatch(pair.Key) && !pair.Value.Any(type => IsMatch(type.ToString()))) + patch.PatchTypesByOwner.Remove(pair.Key); + } + + if (patch.PatchTypesByOwner.Any()) + yield return patch; + } + } + + /// <summary>Get all current Harmony patches.</summary> + private IEnumerable<SearchResult> GetAllPatches() + { +#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<PatchType, IReadOnlyCollection<Patch>> patchGroups = new Dictionary<PatchType, IReadOnlyCollection<Patch>> + { + [PatchType.Prefix] = patchInfo.Prefixes, + [PatchType.Postfix] = patchInfo.Postfixes, +#if HARMONY_2 + [PatchType.Finalizer] = patchInfo.Finalizers, +#endif + [PatchType.Transpiler] = patchInfo.Transpilers + }; + + // get p |
