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