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;
}
}
}
}