-
Notifications
You must be signed in to change notification settings - Fork 43
/
Harmony.cs
452 lines (400 loc) · 21.1 KB
/
Harmony.cs
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using HarmonyLib.Internal.RuntimeFixes;
using HarmonyLib.Public.Patching;
using HarmonyLib.Tools;
namespace HarmonyLib
{
/// <summary>The Harmony instance is the main entry to Harmony. After creating one with an unique identifier, it is used to patch and query the current application domain</summary>
///
public class Harmony : IDisposable
{
/// <summary>The unique identifier</summary>
///
public string Id { get; }
/// <summary>Set to true before instantiating Harmony to debug Harmony or use an environment variable to set HARMONY_DEBUG to '1' like this: cmd /C "set HARMONY_DEBUG=1 && game.exe"</summary>
/// <remarks>This is for full debugging. To debug only specific patches, use the <see cref="HarmonyDebug"/> attribute</remarks>
///
[Obsolete("Use HarmonyFileLog.Enabled instead")]
// ReSharper disable once InconsistentNaming
public static bool DEBUG;
static Harmony()
{
StackTraceFixes.Install();
}
/// <summary>Creates a new Harmony instance</summary>
/// <param name="id">A unique identifier (you choose your own)</param>
/// <returns>A Harmony instance</returns>
///
public Harmony(string id)
{
#pragma warning disable 618
if (string.IsNullOrEmpty(id)) throw new ArgumentException($"{nameof(id)} cannot be null or empty");
try
{
var envDebug = Environment.GetEnvironmentVariable("HARMONY_DEBUG");
if (envDebug is not null && envDebug.Length > 0)
{
envDebug = envDebug.Trim();
DEBUG = envDebug == "1" || bool.Parse(envDebug);
}
}
catch
{
}
if (DEBUG)
HarmonyFileLog.Enabled = true;
// Get caller before log call to ensure it's captured correctly
var callingMethod = Logger.IsEnabledFor(Logger.LogChannel.Info) ? AccessTools.GetOutsideCaller() : null;
Logger.Log(Logger.LogChannel.Info, () =>
{
var sb = new StringBuilder();
var assembly = typeof(Harmony).Assembly;
var version = assembly.GetName().Version;
var location = assembly.Location;
var environment = Environment.Version.ToString();
var platform = Environment.OSVersion.Platform.ToString();
#if NETFRAMEWORK
if (string.IsNullOrEmpty(location)) location = new Uri(assembly.CodeBase).LocalPath;
#endif
var ptrRuntime = IntPtr.Size;
sb.AppendLine($"### Harmony id={id}, version={version}, location={location}, env/clr={environment}, platform={platform}, ptrsize:runtime={ptrRuntime}");
if (callingMethod?.DeclaringType is object)
{
var callingAssembly = callingMethod.DeclaringType.Assembly;
location = callingAssembly.Location;
#if NETFRAMEWORK
if (string.IsNullOrEmpty(location)) location = new Uri(callingAssembly.CodeBase).LocalPath;
#endif
sb.AppendLine($"### Started from {callingMethod.FullDescription()}, location {location}");
sb.Append($"### At {DateTime.Now:yyyy-MM-dd hh.mm.ss}");
}
return sb.ToString();
});
Id = id;
#pragma warning restore 618
// TODO: enable to switch to building methods with CECIL
// Switches.SetSwitchValue(Switches.DMDType, "cecil");
}
/// <summary>Searches the current assembly for Harmony annotations and uses them to create patches</summary>
/// <remarks>This method can fail to use the correct assembly when being inlined. It calls StackTrace.GetFrame(1) which can point to the wrong method/assembly. If you are unsure or run into problems, use <code>PatchAll(Assembly.GetExecutingAssembly())</code> instead.</remarks>
///
public void PatchAll()
{
var method = new StackTrace().GetFrame(1).GetMethod();
var assembly = method.ReflectedType.Assembly;
PatchAll(assembly);
}
/// <summary>Creates a empty patch processor for an original method</summary>
/// <param name="original">The original method/constructor</param>
/// <returns>A new <see cref="PatchProcessor"/> instance</returns>
///
public PatchProcessor CreateProcessor(MethodBase original) => new(this, original);
/// <summary>Creates a patch class processor from an annotated class</summary>
/// <param name="type">The class/type</param>
/// <returns>A new <see cref="PatchClassProcessor"/> instance</returns>
///
public PatchClassProcessor CreateClassProcessor(Type type) => new(this, type);
/// <summary>Creates a patch class processor from an annotated class</summary>
/// <param name="type">The class/type</param>
/// <param name="allowUnannotatedType">If <b>true</b>, the type doesn't need to have any <see cref="HarmonyPatch"/> attributes present for processing</param>
/// <returns>A new <see cref="PatchClassProcessor"/> instance</returns>
///
public PatchClassProcessor CreateClassProcessor(Type type, bool allowUnannotatedType) => new(this, type, allowUnannotatedType);
/// <summary>Creates a reverse patcher for one of your stub methods</summary>
/// <param name="original">The original method/constructor</param>
/// <param name="standin">The stand-in stub method as <see cref="HarmonyMethod"/></param>
/// <returns>A new <see cref="ReversePatcher"/> instance</returns>
///
public ReversePatcher CreateReversePatcher(MethodBase original, HarmonyMethod standin) => new(this, original, standin);
/// <summary>Searches an assembly for Harmony annotations and uses them to create patches</summary>
/// <param name="assembly">The assembly</param>
///
public void PatchAll(Assembly assembly) => AccessTools.GetTypesFromAssembly(assembly).Do(type => CreateClassProcessor(type).Patch());
/// <summary>Searches the given type for Harmony annotation and uses them to create patches</summary>
/// <param name="type">The type to search</param>
///
public void PatchAll(Type type) => CreateClassProcessor(type, true).Patch();
/// <summary>Searches an assembly for Harmony-annotated classes without category annotations and uses them to create patches</summary>
///
public void PatchAllUncategorized()
{
var method = new StackTrace().GetFrame(1).GetMethod();
var assembly = method.ReflectedType.Assembly;
PatchAllUncategorized(assembly);
}
/// <summary>Searches an assembly for Harmony-annotated classes without category annotations and uses them to create patches</summary>
/// <param name="assembly">The assembly</param>
///
public void PatchAllUncategorized(Assembly assembly)
{
var patchClasses = AccessTools.GetTypesFromAssembly(assembly).Select(CreateClassProcessor).ToArray();
patchClasses.DoIf((patchClass => string.IsNullOrEmpty(patchClass.Category)), (patchClass => patchClass.Patch()));
}
/// <summary>Searches the current assembly for Harmony annotations with a specific category and uses them to create patches</summary>
/// <param name="category">Name of patch category</param>
///
public void PatchCategory(string category)
{
var method = new StackTrace().GetFrame(1).GetMethod();
var assembly = method.ReflectedType.Assembly;
PatchCategory(assembly, category);
}
/// <summary>Searches an assembly for Harmony annotations with a specific category and uses them to create patches</summary>
/// <param name="assembly">The assembly</param>
/// <param name="category">Name of patch category</param>
///
public void PatchCategory(Assembly assembly, string category)
{
AccessTools.GetTypesFromAssembly(assembly)
.Where(type =>
{
var harmonyAttributes = HarmonyMethodExtensions.GetFromType(type);
var containerAttributes = HarmonyMethod.Merge(harmonyAttributes);
return containerAttributes.category == category;
})
.Do(type => CreateClassProcessor(type).Patch());
}
/// <summary>Creates patches by manually specifying the methods</summary>
/// <param name="original">The original method/constructor</param>
/// <param name="prefix">An optional prefix method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="postfix">An optional postfix method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="transpiler">An optional transpiler method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="finalizer">An optional finalizer method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="ilmanipulator">An optional ilmanipulator method wrapped in a <see cref="HarmonyMethod"/></param>
/// <returns>The replacement method that was created to patch the original method</returns>
///
public MethodInfo Patch(MethodBase original, HarmonyMethod prefix = null, HarmonyMethod postfix = null, HarmonyMethod transpiler = null, HarmonyMethod finalizer = null,
HarmonyMethod ilmanipulator = null)
{
var processor = CreateProcessor(original);
_ = processor.AddPrefix(prefix);
_ = processor.AddPostfix(postfix);
_ = processor.AddTranspiler(transpiler);
_ = processor.AddFinalizer(finalizer);
_ = processor.AddILManipulator(ilmanipulator);
return processor.Patch();
}
/// <summary>Creates patches by manually specifying the methods</summary>
/// <param name="original">The original method/constructor</param>
/// <param name="prefix">An optional prefix method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="postfix">An optional postfix method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="transpiler">An optional transpiler method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <param name="finalizer">An optional finalizer method wrapped in a <see cref="HarmonyMethod"/> object</param>
/// <returns>The replacement method that was created to patch the original method</returns>
///
[Obsolete("Use newer Patch() instead", true)]
public MethodInfo Patch(MethodBase original, HarmonyMethod prefix, HarmonyMethod postfix, HarmonyMethod transpiler, HarmonyMethod finalizer)
{
return Patch(original, prefix, postfix, transpiler, finalizer, null);
}
/// <summary>Patches a foreign method onto a stub method of yours and optionally applies transpilers during the process</summary>
/// <param name="original">The original method/constructor you want to duplicate</param>
/// <param name="standin">Your stub method as <see cref="HarmonyMethod"/> that will become the original. Needs to have the correct signature (either original or whatever your transpilers generates)</param>
/// <param name="transpiler">An optional transpiler as method that will be applied during the process</param>
/// <param name="ilmanipulator">An optional ilmanipulator as method that will be applied during the process</param>
/// <returns>The replacement method that was created to patch the stub method</returns>
///
public static MethodInfo ReversePatch(MethodBase original, HarmonyMethod standin, MethodInfo transpiler = null, MethodInfo ilmanipulator = null)
{
return PatchFunctions.ReversePatch(standin, original, transpiler, ilmanipulator);
}
/// <summary>Patches a foreign method onto a stub method of yours and optionally applies transpilers during the process</summary>
/// <param name="original">The original method/constructor you want to duplicate</param>
/// <param name="standin">Your stub method as <see cref="HarmonyMethod"/> that will become the original. Needs to have the correct signature (either original or whatever your transpilers generates)</param>
/// <param name="transpiler">An optional transpiler as method that will be applied during the process</param>
/// <returns>The replacement method that was created to patch the stub method</returns>
///
[Obsolete("Use newer ReversePatch() instead", true)]
public static MethodInfo ReversePatch(MethodBase original, HarmonyMethod standin, MethodInfo transpiler)
{
return PatchFunctions.ReversePatch(standin, original, transpiler, null);
}
/// <summary>Unpatches all methods that were patched by the specified <paramref name="harmonyID"/>. Unpatching is done by repatching methods without patches of this instance.</summary>
/// <param name="harmonyID">The Harmony ID to restrict unpatching to a specific Harmony instance.</param>
/// <exception cref="ArgumentNullException">Gets thrown when a null or empty HarmonyID gets passed in.</exception>
///
public static void UnpatchID(string harmonyID)
{
if (string.IsNullOrEmpty(harmonyID))
{
throw new ArgumentNullException(nameof(harmonyID) , "UnpatchID was called with a null or empty harmonyID.");
}
PatchFunctions.UnpatchConditional(patchInfo => patchInfo.owner == harmonyID);
}
void IDisposable.Dispose()
{
UnpatchSelf();
}
/// <summary>Unpatches all methods that were patched by this Harmony instance's ID. Unpatching is done by repatching methods without patches of this instance.</summary>
///
public void UnpatchSelf()
{
UnpatchID(Id);
}
/// <summary>Globally unpatches ALL methods by patching them with zero patches. Complete unpatching is not supported.</summary>
///
public static void UnpatchAll()
{
Logger.Log(Logger.LogChannel.Warn, () => "UnpatchAll has been called - This will remove ALL HARMONY PATCHES.");
PatchFunctions.UnpatchConditional(_ => true);
}
/// <summary>Unpatches methods by patching them with zero patches. Fully unpatching is not supported. Be careful, unpatching is global</summary>
/// <param name="harmonyID">The Harmony ID to restrict unpatching to a specific Harmony instance. Whether this parameter is actually optional is determined by the <see cref="HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll"/> global flag</param>
/// <remarks>When <see cref="HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll"/> is set to true, the execution of this method will be skipped when no <paramref name="harmonyID"/> is specified.</remarks>
///
[Obsolete("Use UnpatchSelf() to unpatch the current instance. The functionality to unpatch either other ids or EVERYTHING has been moved the static methods UnpatchID() and UnpatchAll() respectively", true)]
public void UnpatchAll(string harmonyID = null)
{
if (harmonyID == null)
{
if (HarmonyGlobalSettings.DisallowLegacyGlobalUnpatchAll)
{
Logger.Log(Logger.LogChannel.Warn, () => "Legacy UnpatchAll has been called AND DisallowLegacyGlobalUnpatchAll=true. " +
"Skipping execution of UnpatchAll");
return;
}
UnpatchAll();
}
else
{
if (harmonyID.Length == 0)
{
Logger.Log(Logger.LogChannel.Warn, () => "Legacy UnpatchAll was called with harmonyID=\"\" which is an invalid id. " +
"Skipping execution of UnpatchAll");
return;
}
UnpatchID(harmonyID);
}
}
/// <summary>Unpatches a method by patching it with zero patches. Fully unpatching is not supported. Be careful, unpatching is global</summary>
/// <param name="original">The original method/constructor</param>
/// <param name="type">The <see cref="HarmonyPatchType"/></param>
/// <param name="harmonyID">The optional Harmony ID to restrict unpatching to a specific Harmony instance</param>
///
public void Unpatch(MethodBase original, HarmonyPatchType type, string harmonyID = "*")
{
var processor = CreateProcessor(original);
_ = processor.Unpatch(type, harmonyID);
}
/// <summary>Unpatches a method by patching it with zero patches. Fully unpatching is not supported. Be careful, unpatching is global</summary>
/// <param name="original">The original method/constructor</param>
/// <param name="patch">The patch method as method to remove</param>
///
public void Unpatch(MethodBase original, MethodInfo patch)
{
var processor = CreateProcessor(original);
_ = processor.Unpatch(patch);
}
/// <summary>Searches the current assembly for types with a specific category annotation and uses them to unpatch existing patches. Fully unpatching is not supported. Be careful, unpatching is global</summary>
/// <param name="category">Name of patch category</param>
///
public void UnpatchCategory(string category)
{
var method = new StackTrace().GetFrame(1).GetMethod();
var assembly = method.ReflectedType.Assembly;
UnpatchCategory(assembly, category);
}
/// <summary>Searches an assembly for types with a specific category annotation and uses them to unpatch existing patches. Fully unpatching is not supported. Be careful, unpatching is global</summary>
/// <param name="assembly">The assembly</param>
/// <param name="category">Name of patch category</param>
///
public void UnpatchCategory(Assembly assembly, string category)
{
AccessTools.GetTypesFromAssembly(assembly)
.Where(type =>
{
var harmonyAttributes = HarmonyMethodExtensions.GetFromType(type);
var containerAttributes = HarmonyMethod.Merge(harmonyAttributes);
return containerAttributes.category == category;
})
.Do(type => CreateClassProcessor(type).Unpatch());
}
/// <summary>Test for patches from a specific Harmony ID</summary>
/// <param name="harmonyID">The Harmony ID</param>
/// <returns>True if patches for this ID exist</returns>
///
public static bool HasAnyPatches(string harmonyID)
{
return GetAllPatchedMethods()
.Select(GetPatchInfo)
.Any(info => info.Owners.Contains(harmonyID));
}
/// <summary>Gets patch information for a given original method</summary>
/// <param name="method">The original method/constructor</param>
/// <returns>The patch information as <see cref="Patches"/></returns>
///
public static Patches GetPatchInfo(MethodBase method) => PatchProcessor.GetPatchInfo(method);
/// <summary>Gets the methods this instance has patched</summary>
/// <returns>An enumeration of original methods/constructors</returns>
///
public IEnumerable<MethodBase> GetPatchedMethods()
{
return GetAllPatchedMethods()
.Where(original => GetPatchInfo(original).Owners.Contains(Id));
}
/// <summary>Gets all patched original methods in the appdomain</summary>
/// <returns>An enumeration of patched original methods/constructors</returns>
///
public static IEnumerable<MethodBase> GetAllPatchedMethods() => PatchProcessor.GetAllPatchedMethods();
/// <summary>Gets the original method from a given replacement method</summary>
/// <param name="replacement">A replacement method (patched original method)</param>
/// <returns>The original method/constructor or <c>null</c> if not found</returns>
///
public static MethodBase GetOriginalMethod(MethodInfo replacement)
{
if (replacement == null) throw new ArgumentNullException(nameof(replacement));
return PatchManager.GetRealMethod(replacement, useReplacement: false);
}
/// <summary>Tries to get the method from a stackframe including dynamic replacement methods</summary>
/// <param name="frame">The <see cref="StackFrame"/></param>
/// <returns>For normal frames, <c>frame.GetMethod()</c> is returned. For frames containing patched methods, the replacement method is returned or <c>null</c> if no method can be found</returns>
///
public static MethodBase GetMethodFromStackframe(StackFrame frame)
{
if (frame == null) throw new ArgumentNullException(nameof(frame));
return PatchManager.GetStackFrameMethod(frame, useReplacement: true);
}
/// <summary>Gets the original method from the stackframe and uses original if method is a dynamic replacement</summary>
/// <param name="frame">The <see cref="StackFrame"/></param>
/// <returns>The original method from that stackframe</returns>
public static MethodBase GetOriginalMethodFromStackframe(StackFrame frame)
{
if (frame == null) throw new ArgumentNullException(nameof(frame));
return PatchManager.GetStackFrameMethod(frame, useReplacement: false);
}
/// <summary>Gets Harmony version for all active Harmony instances</summary>
/// <param name="currentVersion">[out] The current Harmony version</param>
/// <returns>A dictionary containing assembly versions keyed by Harmony IDs</returns>
///
public static Dictionary<string, Version> VersionInfo(out Version currentVersion)
=> PatchProcessor.VersionInfo(out currentVersion);
private static int _autoGuidCounter = 100;
/// <summary>Creates a new Harmony instance and applies all patches specified in the type</summary>
/// <param name="type">The type to scan for patches.</param>
/// <param name="harmonyInstanceId">ID of the Harmony instance which will be created. Specify the ID if other plugins may want to interact with your patches.</param>
///
public static Harmony CreateAndPatchAll(Type type, string harmonyInstanceId = null)
{
if (type == null) throw new ArgumentNullException(nameof(type));
var harmony = new Harmony(harmonyInstanceId ?? $"harmony-auto-{System.Threading.Interlocked.Increment(ref _autoGuidCounter)}-{type.Assembly.GetName().Name}-{type.FullName}");
harmony.PatchAll(type);
return harmony;
}
/// <summary>Applies all patches specified in the assembly</summary>
/// <param name="assembly">The assembly to scan.</param>
/// <param name="harmonyInstanceId">ID of the Harmony instance which will be created. Specify the ID if other plugins may want to interact with your patches.</param>
///
public static Harmony CreateAndPatchAll(Assembly assembly, string harmonyInstanceId = null)
{
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
var harmony = new Harmony(harmonyInstanceId ?? $"harmony-auto-{System.Threading.Interlocked.Increment(ref _autoGuidCounter)}-{assembly.GetName().Name}");
harmony.PatchAll(assembly);
return harmony;
}
}
}