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))]