Skip to content

Commit 50896dd

Browse files
committed
Add exact argument name binding
1 parent 0c67cf8 commit 50896dd

6 files changed

Lines changed: 114 additions & 7 deletions

File tree

Documentation/articles/patching-injections.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ To access or change one or several of the original methods arguments, simply rep
3939
- The type of an injected argument must be assignable from the original argument (or just use `object`)
4040
- The name of a given argument (that is to be matched to the argument of the original method) must either be the same name or of the form **`__n`**, where `n` is the zero-based index of the argument in the orignal method (you can also use argument annotations to map to custom names).
4141

42+
If an original argument name conflicts with a Harmony injection name or naming convention, use `ArgumentMode.Original` to match its exact, case-sensitive name without interpreting it:
43+
44+
```csharp
45+
static void Prefix([HarmonyArgument("__result", ArgumentMode.Original)] ref bool result)
46+
```
47+
4248
### __originalMethod
4349

4450
To allow patches to identify on which method they are attached to, you can inject the original methods MethodBase by using an argument called **`__originalMethod`**.

Harmony/Internal/InjectedParameter.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ internal class InjectedParameter
2222
internal ParameterInfo parameterInfo;
2323
internal string realName;
2424
internal InjectionType injectionType;
25+
internal ArgumentMode argumentMode;
2526

2627
internal const string INSTANCE_PARAM = "__instance";
2728
internal const string ORIGINAL_METHOD_PARAM = "__originalMethod";
@@ -35,16 +36,25 @@ internal class InjectedParameter
3536
internal InjectedParameter(MethodInfo method, ParameterInfo parameterInfo)
3637
{
3738
this.parameterInfo = parameterInfo;
38-
realName = CalculateRealName(method);
39-
injectionType = Type(realName);
39+
var arg = parameterInfo.GetArgumentAttribute();
40+
argumentMode = arg?.Mode ?? ArgumentMode.Default;
41+
if (argumentMode == ArgumentMode.Original)
42+
{
43+
realName = arg.NewName;
44+
injectionType = InjectionType.Unknown;
45+
}
46+
else
47+
{
48+
realName = CalculateRealName(method, arg);
49+
injectionType = Type(realName);
50+
}
4051
}
4152

42-
string CalculateRealName(MethodInfo method)
53+
string CalculateRealName(MethodInfo method, HarmonyArgument arg)
4354
{
4455
var baseArgs = method.GetArgumentAttributes();
4556
if (method.DeclaringType is not null)
4657
baseArgs = baseArgs.Union(method.DeclaringType.GetArgumentAttributes());
47-
var arg = parameterInfo.GetArgumentAttribute();
4858
if (arg != null)
4959
return arg.OriginalName ?? parameterInfo.Name;
5060
return baseArgs.GetRealName(parameterInfo.Name, null) ?? parameterInfo.Name;

Harmony/Internal/MethodCreatorTools.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ List<KeyValuePair<LocalBuilder, Type>> tmpBoxVars
280280
continue;
281281
}
282282

283-
if (paramRealName.StartsWith(INSTANCE_FIELD_PREFIX, StringComparison.Ordinal))
283+
if (injection.argumentMode != ArgumentMode.Original && paramRealName.StartsWith(INSTANCE_FIELD_PREFIX, StringComparison.Ordinal))
284284
{
285285
var fieldName = paramRealName.Substring(INSTANCE_FIELD_PREFIX.Length);
286286
FieldInfo fieldInfo;
@@ -363,15 +363,21 @@ List<KeyValuePair<LocalBuilder, Type>> tmpBoxVars
363363
continue;
364364
}
365365

366-
if (config.localVariables.TryGetValue(paramRealName, out var localBuilder))
366+
if (injection.argumentMode != ArgumentMode.Original && config.localVariables.TryGetValue(paramRealName, out var localBuilder))
367367
{
368368
var ldlocCode = paramType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc;
369369
codes.Add(new CodeInstruction(ldlocCode, localBuilder));
370370
continue;
371371
}
372372

373373
int argumentIdx;
374-
if (paramRealName.StartsWith(PARAM_INDEX_PREFIX, StringComparison.Ordinal))
374+
if (injection.argumentMode == ArgumentMode.Original)
375+
{
376+
argumentIdx = Array.IndexOf(originalParameterNames, paramRealName);
377+
if (argumentIdx == -1)
378+
throw new Exception($"Parameter \"{paramRealName}\" not found in method {original.FullDescription()}");
379+
}
380+
else if (paramRealName.StartsWith(PARAM_INDEX_PREFIX, StringComparison.Ordinal))
375381
{
376382
var val = paramRealName.Substring(PARAM_INDEX_PREFIX.Length);
377383
if (!int.TryParse(val, out argumentIdx))

Harmony/Public/Attributes.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,16 @@ public enum ArgumentType
9999
Pointer
100100
}
101101

102+
/// <summary>Specifies how Harmony resolves an argument name</summary>
103+
///
104+
public enum ArgumentMode
105+
{
106+
/// <summary>Use Harmony's default argument name handling</summary>
107+
Default = 0,
108+
/// <summary>Match an original method argument by its exact name</summary>
109+
Original = 1
110+
}
111+
102112
/// <summary>Specifies the type of patch</summary>
103113
///
104114
public enum HarmonyPatchType
@@ -667,6 +677,9 @@ public class HarmonyFinalizer : Attribute
667677
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)]
668678
public class HarmonyArgument : Attribute
669679
{
680+
// Older Harmony versions copy the existing fields and reject this name instead of silently treating the argument as a special injection
681+
const string LEGACY_REJECTION_NAME = "__HarmonyArgumentOriginal";
682+
670683
/// <summary>The name of the original argument</summary>
671684
///
672685
public string OriginalName { get; private set; }
@@ -679,12 +692,31 @@ public class HarmonyArgument : Attribute
679692
///
680693
public string NewName { get; private set; }
681694

695+
internal ArgumentMode Mode { get; private set; }
696+
682697
/// <summary>An annotation to declare injected arguments by name</summary>
683698
///
684699
public HarmonyArgument(string originalName) : this(originalName, null)
685700
{
686701
}
687702

703+
/// <summary>An annotation to declare an injected argument using the selected name handling</summary>
704+
/// <param name="originalName">Name of the original argument</param>
705+
/// <param name="mode">How Harmony resolves the argument name</param>
706+
///
707+
public HarmonyArgument(string originalName, ArgumentMode mode) : this(originalName)
708+
{
709+
Mode = mode;
710+
if (mode == ArgumentMode.Original)
711+
{
712+
OriginalName = LEGACY_REJECTION_NAME;
713+
Index = int.MinValue;
714+
NewName = originalName;
715+
}
716+
else if (mode != ArgumentMode.Default)
717+
throw new ArgumentOutOfRangeException(nameof(mode));
718+
}
719+
688720
/// <summary>An annotation to declare injected arguments by index</summary>
689721
/// <param name="index">Zero-based index</param>
690722
///

HarmonyTests/Patching/Arguments.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,28 @@ public void Test_RenamedArguments()
488488
Assert.AreEqual("val1, patched, val2, hello", log);
489489
}
490490

491+
[Test]
492+
public void Test_OriginalArgumentNames()
493+
{
494+
var harmony = new Harmony("test");
495+
var processor = new PatchClassProcessor(harmony, typeof(OriginalArgumentNamesPatch));
496+
var patches = processor.Patch();
497+
Assert.NotNull(patches, "patches");
498+
Assert.AreEqual(1, patches.Count);
499+
500+
var state = new object[] { "original state" };
501+
var result = false;
502+
var index = 0;
503+
OriginalArgumentNames.log = null;
504+
OriginalArgumentNamesPatch.gotInstance = false;
505+
new OriginalArgumentNames().Method(state, ref result, "original field", ref index);
506+
507+
Assert.AreEqual("patched state, True, patched field, 42", OriginalArgumentNames.log);
508+
Assert.IsTrue(result);
509+
Assert.AreEqual(42, index);
510+
Assert.IsTrue(OriginalArgumentNamesPatch.gotInstance);
511+
}
512+
491513
[Test]
492514
public void Test_CompatibleStateTypes()
493515
{

HarmonyTests/Patching/Assets/ArgumentCases.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,37 @@ public static void Postfix([HarmonyArgument("__instance")] RenamedArguments foo2
145145
}
146146
}
147147

148+
public class OriginalArgumentNames
149+
{
150+
public static string log;
151+
152+
[MethodImpl(MethodImplOptions.NoInlining)]
153+
public void Method(object[] __state, ref bool __result, string ___field, ref int __0)
154+
{
155+
log = $"{__state[0]}, {__result}, {___field}, {__0}";
156+
}
157+
}
158+
159+
[HarmonyPatch(typeof(OriginalArgumentNames), nameof(OriginalArgumentNames.Method))]
160+
public static class OriginalArgumentNamesPatch
161+
{
162+
public static bool gotInstance;
163+
164+
public static void Prefix(
165+
[HarmonyArgument("__state", ArgumentMode.Original)] object[] state,
166+
[HarmonyArgument("__result", ArgumentMode.Original)] ref bool result,
167+
[HarmonyArgument("___field", ArgumentMode.Original)] ref string field,
168+
[HarmonyArgument("__0", ArgumentMode.Original)] ref int index,
169+
[HarmonyArgument("__instance", ArgumentMode.Default)] OriginalArgumentNames instance)
170+
{
171+
state[0] = "patched state";
172+
result = true;
173+
field = "patched field";
174+
index = 42;
175+
gotInstance = instance is not null;
176+
}
177+
}
178+
148179
public class DifferingStateTypes
149180
{
150181
[MethodImpl(MethodImplOptions.NoInlining)]

0 commit comments

Comments
 (0)