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 *********/ /// 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 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 *********/ /// 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 (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; } } /// 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 }; // 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(method, typesByOwner); } } /// A Harmony patch type. private enum PatchType { /// A prefix patch. Prefix, /// A postfix patch. Postfix, #if HARMONY_2 /// A finalizer patch. Finalizer, #endif /// 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; } } } }