diff options
Diffstat (limited to 'src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs')
-rw-r--r-- | src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs | 202 |
1 files changed, 202 insertions, 0 deletions
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 patch types by owner + var typesByOwner = new Dictionary<string, ISet<PatchType>>(); + foreach (var group in patchGroups) + { + foreach (var patch in group.Value) + { + if (!typesByOwner.TryGetValue(patch.owner, out ISet<PatchType> patchTypes)) + typesByOwner[patch.owner] = patchTypes = new HashSet<PatchType>(); + patchTypes.Add(group.Key); + } + } + + // create search result + yield return new SearchResult(method, typesByOwner); + } + } + + /// <summary>A Harmony patch type.</summary> + private enum PatchType + { + /// <summary>A prefix patch.</summary> + Prefix, + + /// <summary>A postfix patch.</summary> + Postfix, + +#if HARMONY_2 + /// <summary>A finalizer patch.</summary> + Finalizer, +#endif + + /// <summary>A transpiler patch.</summary> + Transpiler + } + + /// <summary>A patch search result for a method.</summary> + private class SearchResult + { + /********* + ** Accessors + *********/ + /// <summary>A simple human-readable name for the patched method.</summary> + public string MethodName { get; } + + /// <summary>A detailed description for the patched method.</summary> + public string MethodDescription { get; } + + /// <summary>The patch types by the Harmony instance ID that added them.</summary> + public IDictionary<string, ISet<PatchType>> PatchTypesByOwner { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="method">The patched method.</param> + /// <param name="patchTypesByOwner">The patch types by the Harmony instance ID that added them.</param> + public SearchResult(MethodBase method, IDictionary<string, ISet<PatchType>> patchTypesByOwner) + { + this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}"; + this.MethodDescription = method.FullDescription(); + this.PatchTypesByOwner = patchTypesByOwner; + } + } + } +} |