Skip to content

Commit

Permalink
Move and slightly modify ReplaceMethod method (#483)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
SokyranTheDragon authored Oct 19, 2024
1 parent 66e1699 commit 663e35f
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 74 deletions.
85 changes: 85 additions & 0 deletions Source/PatchingUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -981,5 +981,90 @@ private static bool StopSecondHostCall()
}

#endregion

#region Method replacer

/// <summary>
/// A more specialized alternative to <see cref="Transpilers.MethodReplacer"/>.
/// 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.
/// </summary>
/// <param name="instr">The enumeration of <see cref="T:HarmonyLib.CodeInstruction"/> to act on.</param>
/// <param name="from">Method or constructor to search for.</param>
/// <param name="to">Method or constructor to replace with.</param>
/// <param name="baseMethod">Method or constructor that is being patched, used for logging to provide information on which patched method had issues.</param>
/// <param name="extraInstructions">Extra instructions to insert before the method or constructor is called.</param>
/// <param name="expectedReplacements">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).</param>
/// <param name="targetText">The text that should appear before replacing a method. A first occurence of the method after this text will be replaced.</param>
/// <param name="excludedText">The text that excludes the next method from being patched. Will prevent skip patching the next time the method was going to be patched.</param>
/// <returns>Modified enumeration of <see cref="T:HarmonyLib.CodeInstruction"/></returns>
public static IEnumerable<CodeInstruction> ReplaceMethod(this IEnumerable<CodeInstruction> instr, MethodBase from, MethodBase to, MethodBase baseMethod = null, Func<CodeInstruction, IEnumerable<CodeInstruction>> 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
}
}
85 changes: 11 additions & 74 deletions Source_Referenced/ARimWorldOfMagic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ private static IEnumerable<CodeInstruction> UniversalReplaceLevelUpPlusButton(IE
// Shouldn't happen
else throw new Exception($"Trying to apply transpiler ({nameof(UniversalReplaceLevelUpPlusButton)}) for an unsupported type ({baseMethod.DeclaringType.FullDescription()}).");

IEnumerable<CodeInstruction> ExtraInstructions() =>
IEnumerable<CodeInstruction> ExtraInstructions(CodeInstruction _) =>
[
// Load the magic/might comp parameter
new CodeInstruction(OpCodes.Ldarg_1),
Expand All @@ -814,7 +814,7 @@ IEnumerable<CodeInstruction> 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))]
Expand All @@ -825,13 +825,13 @@ private static IEnumerable<CodeInstruction> UniversalReplaceGlobalLevelUpPlusBut
[typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor?)]);
MethodInfo replacement;
int expected;
Func<IEnumerable<CodeInstruction>> extraInstructions;
Func<CodeInstruction, IEnumerable<CodeInstruction>> extraInstructions;

if (baseMethod.DeclaringType == typeof(MagicCardUtility))
{
replacement = MpMethodUtil.MethodOf(ReplacedGlobalLevelUpMagicButton);
expected = 3;
extraInstructions = () =>
extraInstructions = _ =>
[
// Load the pawn argument
new CodeInstruction(OpCodes.Ldarg_1),
Expand All @@ -845,7 +845,7 @@ private static IEnumerable<CodeInstruction> UniversalReplaceGlobalLevelUpPlusBut
{
replacement = MpMethodUtil.MethodOf(ReplacedGlobalLevelUpMightButton);
expected = 4;
extraInstructions = () =>
extraInstructions = _ =>
[
// Load the pawn argument
new CodeInstruction(OpCodes.Ldarg_1),
Expand All @@ -859,7 +859,7 @@ private static IEnumerable<CodeInstruction> 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
Expand Down Expand Up @@ -1113,7 +1113,7 @@ private static IEnumerable<CodeInstruction> ReplaceLearnSkillButton(IEnumerable<
// Shouldn't happen
else throw new Exception($"Trying to apply transpiler ({nameof(ReplaceLearnSkillButton)}) for an unsupported type ({baseMethod.DeclaringType.FullDescription()}).");

IEnumerable<CodeInstruction> ExtraInstructions() =>
IEnumerable<CodeInstruction> ExtraInstructions(CodeInstruction _) =>
[
// Load the magic/might comp parameter
new CodeInstruction(OpCodes.Ldarg_1),
Expand All @@ -1123,9 +1123,9 @@ IEnumerable<CodeInstruction> 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
Expand Down Expand Up @@ -1363,14 +1363,14 @@ private static IEnumerable<CodeInstruction> ReplaceApplyGolemNameButtonTranspile
[typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool), typeof(TextAnchor?)]);
var replacement = MpMethodUtil.MethodOf(ReplacedApplyGolemNameButton);

IEnumerable<CodeInstruction> ExtraInstructions() =>
IEnumerable<CodeInstruction> 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
Expand Down Expand Up @@ -1569,69 +1569,6 @@ private static void PostMpCompExposeData()

#endregion

#region Shared

private static IEnumerable<CodeInstruction> ReplaceMethod(IEnumerable<CodeInstruction> instr, MethodBase baseMethod, MethodInfo target, MethodInfo replacement, Func<IEnumerable<CodeInstruction>> 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))]
Expand Down

0 comments on commit 663e35f

Please sign in to comment.