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.MethodName).ToArray(); StringBuilder result = new(); if (!matches.Any()) result.AppendLine("No current patches match your search."); else { result.AppendLine(args.Any() ? "Harmony patches which match your search terms:" : "Current Harmony patches:"); result.AppendLine(); foreach (var match in matches) { result.AppendLine($" {match.MethodName}"); foreach (var ownerGroup in match.PatchTypesByOwner.OrderBy(p => p.Key)) { var sortedTypes = ownerGroup.Value .OrderBy(p => p switch { PatchType.Prefix => 0, PatchType.Postfix => 1, PatchType.Finalizer => 2, PatchType.Transpiler => 3, _ => 4 }); result.AppendLine($" - {ownerGroup.Key} ({string.Join(", ", sortedTypes).ToLower()})"); } result.AppendLine(); } } monitor.Log(result.ToString().TrimEnd(), LogLevel.Info); } /********* ** Private methods *********/ /// 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) => !hasSearch || searchTerms.Any(search => target != null && target.IndexOf(search, StringComparison.OrdinalIgnoreCase) > -1); foreach (SearchResult patch in this.GetAllPatches()) { // matches entire patch if (IsMatch(patch.MethodDescription)) { yield return patch; continue; } // matches individual patchers foreach ((string patcherId, ISet patchTypes) in patch.PatchTypesByOwner.ToArray()) { if (!IsMatch(patcherId) && !patchTypes.Any(type => IsMatch(type.ToString()))) patch.PatchTypesByOwner.Remove(patcherId); } if (patch.PatchTypesByOwner.Any()) yield return patch; } } /// Get all current Harmony patches. private IEnumerable GetAllPatches() { foreach (MethodBase method in Harmony.GetAllPatchedMethods().ToArray()) { // get metadata for method 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 ((PatchType type, IReadOnlyCollection patches) in patchGroups) { foreach (Patch patch in patches) { if (!typesByOwner.TryGetValue(patch.owner, out ISet? patchTypes)) typesByOwner[patch.owner] = patchTypes = new HashSet(); patchTypes.Add(type); } } // create search result yield return new SearchResult(method, 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 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; } /********* ** Public methods *********/ /// Construct an instance. /// The patched method. /// The patch types by the Harmony instance ID that added them. public SearchResult(MethodBase method, IDictionary> patchTypesByOwner) { this.MethodName = $"{method.DeclaringType?.FullName}.{method.Name}"; this.MethodDescription = method.FullDescription(); this.PatchTypesByOwner = patchTypesByOwner; } } } }