1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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().ToArray())
#else
foreach (MethodBase method in this.HarmonyInstance.GetPatchedMethods().ToArray())
#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;
}
}
}
}
|