From 663e35fc503ebef5eabb9b1878d23e8554a71af3 Mon Sep 17 00:00:00 2001
From: SokyranTheDragon <36712560+SokyranTheDragon@users.noreply.github.com>
Date: Sun, 20 Oct 2024 00:11:23 +0200
Subject: [PATCH] Move and slightly modify ReplaceMethod method (#483)
- Moved `ReplaceMethod` from `ARimworldOfMagic` to `PatchingUtilities`
- Made the method an extension method
- Renamed parameters
- `target` and `replacement` changed to `from` and `to`, matching Harmony's MethodReplacer
- `buttonText` changed to `targetText`, since the method
- Changed the parameter order
- `baseMethod` now comes after `from` and `to` parameters and is now optional
- `buttonText` now comes
- The `from` and `to` arguments are now `MethodBase` rather than `MethodInfo`
- This will allow replacement of constructors
- The method no longer checks the operand and only checks if the operator is `MethodBase` and equal to `from`
- It wasn't really needed, and will now allow for replacement of constructors
- To support constructor replacement, the replaced `opcode` will be set to `Newobj` if the `to` is a constructor
- The `extraInstructions` now gives a single argument (current instruction` to allow for potential modifications
- Likewise, `extraInstructions` is now called after opcode/operand are changed
---
Source/PatchingUtilities.cs | 85 +++++++++++++++++++++++++++
Source_Referenced/ARimWorldOfMagic.cs | 85 ++++-----------------------
2 files changed, 96 insertions(+), 74 deletions(-)
diff --git a/Source/PatchingUtilities.cs b/Source/PatchingUtilities.cs
index f1e05dc..a63d85d 100644
--- a/Source/PatchingUtilities.cs
+++ b/Source/PatchingUtilities.cs
@@ -981,5 +981,90 @@ private static bool StopSecondHostCall()
}
#endregion
+
+ #region Method replacer
+
+ ///
+ /// A more specialized alternative to .
+ /// It will replace all occurrences of a given method, but only if the specified text was encountered (unless another disallowed text was encountered).
+ /// It may come especially handy for replacing text buttons with a specific text.
+ ///
+ /// The enumeration of to act on.
+ /// Method or constructor to search for.
+ /// Method or constructor to replace with.
+ /// Method or constructor that is being patched, used for logging to provide information on which patched method had issues.
+ /// Extra instructions to insert before the method or constructor is called.
+ /// The expected number of times the method should be replaced. Use -1 to to disable, or use -2 to expect unspecified amount (but more than 1 replacement).
+ /// The text that should appear before replacing a method. A first occurence of the method after this text will be replaced.
+ /// The text that excludes the next method from being patched. Will prevent skip patching the next time the method was going to be patched.
+ /// Modified enumeration of
+ public static IEnumerable ReplaceMethod(this IEnumerable instr, MethodBase from, MethodBase to, MethodBase baseMethod = null, Func> extraInstructions = null, int expectedReplacements = -1, string targetText = null, string excludedText = null)
+ {
+ // Check for text only if expected text isn't null
+ var isCorrectText = targetText == null;
+ var skipNextCall = false;
+ var replacedCount = 0;
+
+ foreach (var ci in instr)
+ {
+ if (ci.opcode == OpCodes.Ldstr && ci.operand is string s)
+ {
+ // Excluded text (if not null) will cancel replacement of the next occurrence
+ // of the method. Used by `MagicCardUtility:CustomPowersHandler`, as the text
+ // `TM_Learn` appears twice there, but in a single case it's combined with
+ // `TM_MCU_PointsToLearn`, in which case we ignore the button (as the
+ // button does nothing in that particular case).
+ if (excludedText != null && s == excludedText)
+ skipNextCall = true;
+ else if (s == targetText)
+ isCorrectText = true;
+ }
+ else if (isCorrectText)
+ {
+ if (ci.operand is MethodBase method && method == from)
+ {
+ if (skipNextCall)
+ {
+ skipNextCall = false;
+ }
+ else
+ {
+ // Replace method with our own
+ ci.opcode = from.IsConstructor ? OpCodes.Newobj : OpCodes.Call;
+ ci.operand = to;
+
+ if (extraInstructions != null)
+ {
+ foreach (var extraInstr in extraInstructions(ci))
+ yield return extraInstr;
+ }
+
+ replacedCount++;
+ // Check for text only if expected text isn't null
+ isCorrectText = targetText == null;
+ }
+ }
+ }
+
+ yield return ci;
+ }
+
+ string MethodName()
+ {
+ if (baseMethod == null)
+ return "(unknown)";
+ if ((baseMethod.DeclaringType?.Namespace).NullOrEmpty())
+ return baseMethod.Name;
+ return $"{baseMethod.DeclaringType!.Name}:{baseMethod.Name}";
+ }
+
+ if (replacedCount != expectedReplacements && expectedReplacements >= 0)
+ Log.Warning($"Patched incorrect number of {from.DeclaringType?.Name ?? "null"}.{from.Name} calls (patched {replacedCount}, expected {expectedReplacements}) for method {MethodName()}");
+ // Special case (-2) - expected some patched methods, but amount unspecified
+ else if (replacedCount == 0 && expectedReplacements == -2)
+ Log.Warning($"No calls of {from.DeclaringType?.Name ?? "null"}.{from.Name} were patched for method {MethodName()}");
+ }
+
+ #endregion
}
}
\ No newline at end of file
diff --git a/Source_Referenced/ARimWorldOfMagic.cs b/Source_Referenced/ARimWorldOfMagic.cs
index b7a62b6..8f57e8d 100644
--- a/Source_Referenced/ARimWorldOfMagic.cs
+++ b/Source_Referenced/ARimWorldOfMagic.cs
@@ -803,7 +803,7 @@ private static IEnumerable UniversalReplaceLevelUpPlusButton(IE
// Shouldn't happen
else throw new Exception($"Trying to apply transpiler ({nameof(UniversalReplaceLevelUpPlusButton)}) for an unsupported type ({baseMethod.DeclaringType.FullDescription()}).");
- IEnumerable ExtraInstructions() =>
+ IEnumerable ExtraInstructions(CodeInstruction _) =>
[
// Load the magic/might comp parameter
new CodeInstruction(OpCodes.Ldarg_1),
@@ -814,7 +814,7 @@ IEnumerable ExtraInstructions() =>
new CodeInstruction(OpCodes.Ldloc_0),
];
- return ReplaceMethod(instr, baseMethod, target, replacement, ExtraInstructions, "+", 1);
+ return instr.ReplaceMethod(target, replacement, baseMethod, ExtraInstructions, 1, "+");
}
[MpCompatTranspiler(typeof(MagicCardUtility), nameof(MagicCardUtility.DrawLevelBar))]
@@ -825,13 +825,13 @@ private static IEnumerable UniversalReplaceGlobalLevelUpPlusBut
[typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor?)]);
MethodInfo replacement;
int expected;
- Func> extraInstructions;
+ Func> extraInstructions;
if (baseMethod.DeclaringType == typeof(MagicCardUtility))
{
replacement = MpMethodUtil.MethodOf(ReplacedGlobalLevelUpMagicButton);
expected = 3;
- extraInstructions = () =>
+ extraInstructions = _ =>
[
// Load the pawn argument
new CodeInstruction(OpCodes.Ldarg_1),
@@ -845,7 +845,7 @@ private static IEnumerable UniversalReplaceGlobalLevelUpPlusBut
{
replacement = MpMethodUtil.MethodOf(ReplacedGlobalLevelUpMightButton);
expected = 4;
- extraInstructions = () =>
+ extraInstructions = _ =>
[
// Load the pawn argument
new CodeInstruction(OpCodes.Ldarg_1),
@@ -859,7 +859,7 @@ private static IEnumerable UniversalReplaceGlobalLevelUpPlusBut
// Shouldn't happen
else throw new Exception($"Trying to apply transpiler ({nameof(UniversalReplaceLevelUpPlusButton)}) for an unsupported type ({baseMethod.DeclaringType.FullDescription()}).");
- return ReplaceMethod(instr, baseMethod, target, replacement, extraInstructions, "+", expected);
+ return instr.ReplaceMethod(target, replacement, baseMethod, extraInstructions, expected, "+");
}
#endregion
@@ -1113,7 +1113,7 @@ private static IEnumerable ReplaceLearnSkillButton(IEnumerable<
// Shouldn't happen
else throw new Exception($"Trying to apply transpiler ({nameof(ReplaceLearnSkillButton)}) for an unsupported type ({baseMethod.DeclaringType.FullDescription()}).");
- IEnumerable ExtraInstructions() =>
+ IEnumerable ExtraInstructions(CodeInstruction _) =>
[
// Load the magic/might comp parameter
new CodeInstruction(OpCodes.Ldarg_1),
@@ -1123,9 +1123,9 @@ IEnumerable ExtraInstructions() =>
];
// Replace the "TM_Learn" button to learn a power
- var replacedLearnButton = ReplaceMethod(instr, baseMethod, targetTextButton, textButtonReplacement, ExtraInstructions, "TM_Learn", 1, "TM_MCU_PointsToLearn");
+ var replacedLearnButton = instr.ReplaceMethod(targetTextButton, textButtonReplacement, baseMethod, ExtraInstructions, 1, "TM_Learn", "TM_MCU_PointsToLearn");
// Replace the image button to level-up a power
- return ReplaceMethod(replacedLearnButton, baseMethod, targetImageButton, imageButtonReplacement, ExtraInstructions, null, 1);
+ return replacedLearnButton.ReplaceMethod(targetImageButton, imageButtonReplacement, baseMethod, ExtraInstructions, 1);
}
#endregion
@@ -1363,14 +1363,14 @@ private static IEnumerable ReplaceApplyGolemNameButtonTranspile
[typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor?)]);
var replacement = MpMethodUtil.MethodOf(ReplacedApplyGolemNameButton);
- IEnumerable ExtraInstructions() =>
+ IEnumerable ExtraInstructions(CodeInstruction _) =>
[
// Load in "this" (GolemNameWindow)
new CodeInstruction(OpCodes.Ldarg_0),
];
// The "Apply" text isn't translated in the mod...
- return ReplaceMethod(instr, baseMethod, target, replacement, ExtraInstructions, "Apply", 1);
+ return instr.ReplaceMethod(target, replacement, baseMethod, ExtraInstructions, 1, "Apply");
}
#endregion
@@ -1569,69 +1569,6 @@ private static void PostMpCompExposeData()
#endregion
- #region Shared
-
- private static IEnumerable ReplaceMethod(IEnumerable instr, MethodBase baseMethod, MethodInfo target, MethodInfo replacement, Func> extraInstructions = null, string buttonText = null, int expectedReplacements = -1, string excludedText = null)
- {
- // Check for text only if expected text isn't null
- var isCorrectText = buttonText == null;
- var skipNextCall = false;
- var replacedCount = 0;
-
- foreach (var ci in instr)
- {
- if (ci.opcode == OpCodes.Ldstr && ci.operand is string s)
- {
- // Excluded text (if not null) will cancel replacement of the next occurrence
- // of the method. Used by `MagicCardUtility:CustomPowersHandler`, as the text
- // `TM_Learn` appears twice there, but in a single case it's combined with
- // `TM_MCU_PointsToLearn`, in which case we ignore the button (as the
- // button does nothing in that particular case).
- if (excludedText != null && s == excludedText)
- skipNextCall = true;
- else if (s == buttonText)
- isCorrectText = true;
- }
- else if (isCorrectText)
- {
- if (ci.Calls(target))
- {
- if (skipNextCall)
- {
- skipNextCall = false;
- }
- else
- {
- if (extraInstructions != null)
- {
- foreach (var extraInstr in extraInstructions())
- yield return extraInstr;
- }
-
- // Replace method with our own
- ci.opcode = OpCodes.Call;
- ci.operand = replacement;
-
- replacedCount++;
- // Check for text only if expected text isn't null
- isCorrectText = buttonText == null;
- }
- }
- }
-
- yield return ci;
- }
-
- string MethodName() => (baseMethod.DeclaringType?.Namespace).NullOrEmpty() ? baseMethod.Name : $"{baseMethod.DeclaringType!.Name}:{baseMethod.Name}";
- if (replacedCount != expectedReplacements && expectedReplacements >= 0)
- Log.Warning($"Patched incorrect number of {target.DeclaringType?.Name ?? "null"}.{target.Name} calls (patched {replacedCount}, expected {expectedReplacements}) for method {MethodName()}");
- // Special case (-2) - expected some patched methods, but amount unspecified
- else if (replacedCount == 0 && expectedReplacements == -2)
- Log.Warning($"No calls of {target.DeclaringType?.Name ?? "null"}.{target.Name} were patched for method {MethodName()}");
- }
-
- #endregion
-
#region Optimizations
[MpCompatPrefix(typeof(TM_Calc), nameof(TM_Calc.FindConnectedWalls))]