diff --git a/Nitrox.Test/Patcher/PatchTestHelper.cs b/Nitrox.Test/Patcher/PatchTestHelper.cs index 04035e83ea..9f9013d5e0 100644 --- a/Nitrox.Test/Patcher/PatchTestHelper.cs +++ b/Nitrox.Test/Patcher/PatchTestHelper.cs @@ -49,20 +49,6 @@ public static ILGenerator GetILGenerator(this MethodInfo method) return new DynamicMethod(method.Name, method.ReturnType, method.GetParameters().Types()).GetILGenerator(); } - public static void TestPattern(MethodInfo targetMethod, InstructionsPattern pattern, out IEnumerable originalIl, out IEnumerable transformedIl) - { - bool shouldHappen = false; - originalIl = PatchProcessor.GetCurrentInstructions(targetMethod); - transformedIl = originalIl - .Transform(pattern, (_, _) => - { - shouldHappen = true; - }) - .ToArray(); // Required, otherwise nothing happens. - - shouldHappen.Should().BeTrue(); - } - /// /// Clones the instructions so that the returned instructions are not the same reference. /// diff --git a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs index 099ccbfe4a..74907e0014 100644 --- a/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs +++ b/Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs @@ -18,14 +18,14 @@ public class PatchesTranspilerTest [typeof(AttackCyclops_OnCollisionEnter_Patch), -17], [typeof(AttackCyclops_UpdateAggression_Patch), -23], [typeof(Bullet_Update_Patch), 3], - [typeof(BaseDeconstructable_Deconstruct_Patch), BaseDeconstructable_Deconstruct_Patch.InstructionsToAdd(true).Count() * 2], + [typeof(BaseDeconstructable_Deconstruct_Patch), 10], [typeof(BaseHullStrength_CrushDamageUpdate_Patch), 3], [typeof(BreakableResource_SpawnResourceFromPrefab_Patch), 2], - [typeof(Builder_TryPlace_Patch), Builder_TryPlace_Patch.InstructionsToAdd1.Count + Builder_TryPlace_Patch.InstructionsToAdd2.Count], + [typeof(Builder_TryPlace_Patch), 4], [typeof(CellManager_TryLoadCacheBatchCells_Patch), 4], - [typeof(Constructable_Construct_Patch), Constructable_Construct_Patch.InstructionsToAdd.Count], - [typeof(Constructable_DeconstructAsync_Patch), Constructable_DeconstructAsync_Patch.InstructionsToAdd.Count], - [typeof(ConstructableBase_SetState_Patch), ConstructableBase_SetState_Patch.InstructionsToAdd.Count], + [typeof(Constructable_Construct_Patch), 3], + [typeof(Constructable_DeconstructAsync_Patch), 3], + [typeof(ConstructableBase_SetState_Patch), 2], [typeof(ConstructorInput_OnCraftingBegin_Patch), 7], [typeof(CrafterLogic_TryPickupSingleAsync_Patch), 4], [typeof(CrashHome_Spawn_Patch), 2], @@ -97,7 +97,7 @@ public class PatchesTranspilerTest [TestMethod] public void AllTranspilerPatchesHaveSanityTest() { - Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(NitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray(); + Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(INitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray(); foreach (Type patch in allPatchesWithTranspiler) { @@ -153,7 +153,14 @@ public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bo if (logInstructions) { + Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~"); + Console.WriteLine("~~~ TRANSFORMED IL ~~~"); + Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~"); Console.WriteLine(transformedIl.ToPrettyString()); + Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~"); + Console.WriteLine("~~~ ORIGINAL IL ~~~"); + Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~"); + Console.WriteLine(originalIlCopy.ToPrettyString()); } if (transformedIl == null || transformedIl.Count == 0) @@ -162,7 +169,14 @@ public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bo } originalIlCopy.Count.Should().Be(transformedIl.Count - ilDifference); - Assert.IsFalse(originalIlCopy.SequenceEqual(transformedIl, new CodeInstructionComparer()), $"The transpiler patch of {patchClassType.Name} did not change the IL"); + if (originalIlCopy.Count == transformedIl.Count) + { + string originalIlPrettyString = originalIlCopy.ToPrettyString(); + if (originalIlPrettyString == transformedIl.ToPrettyString()) + { + Assert.Fail($"The transpiler patch of {patchClassType.Name} did not change the IL:{Environment.NewLine}{originalIlPrettyString}"); + } + } } private static readonly ModuleBuilder patchTestModule; @@ -184,35 +198,3 @@ private static ILGenerator GetILGenerator(MethodInfo method, Type generatingType return myTypeBld.DefineMethod(method.Name, MethodAttributes.Public, method.ReturnType, method.GetParameters().Types()).GetILGenerator(); } } - -public class CodeInstructionComparer : IEqualityComparer -{ - public bool Equals(CodeInstruction x, CodeInstruction y) - { - if (ReferenceEquals(x, y)) - { - return true; - } - if (x is null) - { - return false; - } - if (y is null) - { - return false; - } - if (x.GetType() != y.GetType()) - { - return false; - } - return x.opcode.Equals(y.opcode) && Equals(x.operand, y.operand); - } - - public int GetHashCode(CodeInstruction obj) - { - unchecked - { - return (obj.opcode.GetHashCode() * 397) ^ (obj.operand != null ? obj.operand.GetHashCode() : 0); - } - } -} diff --git a/Nitrox.Test/Patcher/PatternMatching/RewriteOnPatternTest.cs b/Nitrox.Test/Patcher/PatternMatching/RewriteOnPatternTest.cs new file mode 100644 index 0000000000..b1459e4520 --- /dev/null +++ b/Nitrox.Test/Patcher/PatternMatching/RewriteOnPatternTest.cs @@ -0,0 +1,110 @@ +using HarmonyLib; +using NitroxModel.Helper; +using NitroxPatcher.PatternMatching; +using NitroxPatcher.PatternMatching.Ops; +using NitroxTest.Patcher; +using static System.Reflection.Emit.OpCodes; + +namespace Nitrox.Test.Patcher.PatternMatching; + +[TestClass] +public class RewriteOnPatternTest +{ + private static List testCode; + + [TestInitialize] + public void TestInitialize() + { + testCode = + [ + new(Ldarg_1), + new(Callvirt, Reflect.Property((ResolveEventArgs args) => args.Name).GetGetMethod()), + new(Ldc_I4_S), + new(Ldc_I4_0), + new(Callvirt, Reflect.Method((string s) => s.Split(default))), + new(Ldc_I4_0), + new(Ldelem_Ref), + new(Stloc_0) + ]; + } + + [TestMethod] + public void ShouldDoNothingWithEmptyInstructions() + { + Array.Empty().RewriteOnPattern([]).Should().BeEmpty(); + } + + [TestMethod] + public void ShouldReturnSameIfPatternDoesNotMatch() + { + testCode.RewriteOnPattern([Call], 0).Should().NotBeEmpty().And.HaveCount(testCode.Count); + } + + [TestMethod] + public void ShouldNotMatchIfPatternLargerThanIl() + { + testCode.RewriteOnPattern([..testCode]).Should().NotBeEmpty().And.HaveCount(testCode.Count); + testCode.RewriteOnPattern([..testCode, Callvirt], 0).Should().NotBeEmpty().And.HaveCount(testCode.Count); + } + + [TestMethod] + public void ShouldNotMakeChangesIfNoOperationsInPattern() + { + CodeInstruction[] copy = testCode.Clone().ToArray(); + testCode.RewriteOnPattern([Ldc_I4_0], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count); + copy.ToPrettyString().Should().Be(testCode.ToPrettyString()); + } + + [TestMethod] + public void ShouldThrowIfMatchingUnexpectedAmountOfTimes() + { + Assert.ThrowsException(() => testCode.RewriteOnPattern([Ldc_I4_0], -1)); + Assert.ThrowsException(() => testCode.RewriteOnPattern([Ldc_I4_0], 0)); + Assert.ThrowsException(() => testCode.RewriteOnPattern([Ldc_I4_0], 1)); + Assert.ThrowsException(() => testCode.RewriteOnPattern([Ldc_I4_0], 3)); + } + + [TestMethod] + public void ShouldDifferIfOperationsExecuted() + { + CodeInstruction[] copy = testCode.Clone().ToArray(); + testCode.RewriteOnPattern([PatternOp.Change(Ldc_I4_0, i => i.opcode = Ldc_I4_1)], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count); + copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString()); + + // Pattern should now match without error. + testCode.RewriteOnPattern([Ldc_I4_1, Callvirt]); + } + + [TestMethod] + public void ShouldNotInsertIfEmptyInsertOperation() + { + CodeInstruction[] copy = testCode.Clone().ToArray(); + testCode.RewriteOnPattern([Ldc_I4_0, []], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count); + copy.ToPrettyString().Should().Be(testCode.ToPrettyString()); + } + + [TestMethod] + public void ShouldAddIlIfInsertOperationExecuted() + { + CodeInstruction[] copy = testCode.Clone().ToArray(); + int originalCount = copy.Length; + testCode.RewriteOnPattern([Ldc_I4_0, [Ldc_I4_1]], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count); + copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString()); + copy.Should().HaveCount(originalCount); + testCode.Should().HaveCount(originalCount + 2); + } + + [TestMethod] + public void ShouldAddMultipleInstructionsIfInsertOperationHasMultiple() + { + CodeInstruction[] copy = testCode.Clone().ToArray(); + int originalCount = copy.Length; + testCode.RewriteOnPattern([Ldc_I4_0, [Ldc_I4_1, Ldc_I4_1]], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count); + copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString()); + copy.Should().HaveCount(originalCount); + testCode.Should().HaveCount(originalCount + 4); + + // Pattern should now match without error. + testCode.RewriteOnPattern([Ldc_I4_0, Ldc_I4_1, Ldc_I4_1], 2); + } +} diff --git a/NitroxPatcher/Patches/Dynamic/BaseDeconstructable_Deconstruct_Patch.cs b/NitroxPatcher/Patches/Dynamic/BaseDeconstructable_Deconstruct_Patch.cs index f46db689f4..3b58ff3c3d 100644 --- a/NitroxPatcher/Patches/Dynamic/BaseDeconstructable_Deconstruct_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/BaseDeconstructable_Deconstruct_Patch.cs @@ -11,6 +11,7 @@ using NitroxModel.Helper; using NitroxModel.Packets; using NitroxPatcher.PatternMatching; +using NitroxPatcher.PatternMatching.Ops; using UnityEngine; using static System.Reflection.Emit.OpCodes; using static NitroxClient.GameLogic.Bases.BuildingHandler; @@ -20,51 +21,39 @@ namespace NitroxPatcher.Patches.Dynamic; public sealed partial class BaseDeconstructable_Deconstruct_Patch : NitroxPatch, IDynamicPatch { public static readonly MethodInfo TARGET_METHOD = Reflect.Method((BaseDeconstructable t) => t.Deconstruct()); - - private static TemporaryBuildData Temp => BuildingHandler.Main.Temp; private static BuildPieceIdentifier cachedPieceIdentifier; - public static readonly InstructionsPattern BaseDeconstructInstructionPattern1 = new() - { - Callvirt, - Call, - Ldloc_3, - { new() { OpCode = Callvirt, Operand = new(nameof(BaseGhost), nameof(BaseGhost.ClearTargetBase)) }, "Insert1" } - }; - public static readonly InstructionsPattern BaseDeconstructInstructionPattern2 = new() - { - Ldloc_0, - new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.FixCorridorLinks)) }, - Ldloc_0, - { new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.RebuildGeometry)) }, "Insert2" }, - }; + private static TemporaryBuildData Temp => BuildingHandler.Main.Temp; - public static IEnumerable InstructionsToAdd(bool destroyed) + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) { - yield return new(Ldarg_0); - yield return new(Ldloc_2); - yield return new(Ldloc_0); - yield return new(destroyed ? Ldc_I4_1 : Ldc_I4_0); - yield return new(Call, Reflect.Method(() => PieceDeconstructed(default, default, default, default))); + return instructions.RewriteOnPattern( + [ + Callvirt, + Call, + Ldloc_3, + Reflect.Method((BaseGhost b) => b.ClearTargetBase()), + [.. InstructionsToAdd(true)], + ]) + .RewriteOnPattern( + [ + Ldloc_0, + Reflect.Method((Base b) => b.FixCorridorLinks()), + Ldloc_0, + Reflect.Method((Base b) => b.RebuildGeometry()), + [.. InstructionsToAdd(false)] + ]); + + static PatternOp[] InstructionsToAdd(bool destroyed) => + [ + Ldarg_0, + Ldloc_2, + Ldloc_0, + destroyed ? Ldc_I4_1 : Ldc_I4_0, + Reflect.Method(() => PieceDeconstructed(default, default, default, default)) + ]; } - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => - instructions.Transform(BaseDeconstructInstructionPattern1, (label, instruction) => - { - if (label.Equals("Insert1")) - { - return InstructionsToAdd(true); - } - return null; - }).Transform(BaseDeconstructInstructionPattern2, (label, instruction) => - { - if (label.Equals("Insert2")) - { - return InstructionsToAdd(false); - } - return null; - }); - public static void Prefix(BaseDeconstructable __instance) { BuildUtils.TryGetIdentifier(__instance, out cachedPieceIdentifier, null, __instance.face); @@ -187,11 +176,10 @@ public static void PieceDeconstructed(BaseDeconstructable baseDeconstructable, C } else { - pieceDeconstructed = Temp.NewWaterPark == null ? - new PieceDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), operationId) : - new WaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.NewWaterPark, Temp.MovedChildrenIds, Temp.Transfer, operationId); - } - + pieceDeconstructed = Temp.NewWaterPark == null + ? new PieceDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), operationId) + : new WaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.NewWaterPark, Temp.MovedChildrenIds, Temp.Transfer, operationId);} + Log.Verbose($"Base is not empty, sending packet {pieceDeconstructed}"); Resolve().Send(pieceDeconstructed); diff --git a/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs b/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs index 6ef281302f..b1bbd69b83 100644 --- a/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs @@ -1,37 +1,35 @@ using System; +using System.Collections.Generic; +using System.Reflection; using HarmonyLib; using NitroxClient.GameLogic; using NitroxClient.MonoBehaviours; using NitroxModel.Helper; using NitroxPatcher.PatternMatching; -using System.Collections.Generic; -using System.Reflection; using UnityEngine; using static System.Reflection.Emit.OpCodes; namespace NitroxPatcher.Patches.Dynamic; /// -/// Synchronizes entities that can be broken and that will drop material, such as limestones... +/// Synchronizes entities that can be broken and that will drop material, such as limestones... /// public sealed partial class BreakableResource_SpawnResourceFromPrefab_Patch : NitroxPatch, IDynamicPatch { public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method(() => BreakableResource.SpawnResourceFromPrefab(default, default, default))); - private static readonly InstructionsPattern SpawnResFromPrefPattern = new() - { - { Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))), "DropItemInstance" }, - Ldc_I4_0 - }; - - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) - { - return instructions.InsertAfterMarker(SpawnResFromPrefPattern, "DropItemInstance", new CodeInstruction[] - { - new(Ldloc_1), - new(Call, ((Action)Callback).Method) - }); - } + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => + instructions + .RewriteOnPattern( + [ + Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))), + [ + Ldloc_1, // Dropped item GameObject + ((Action)Callback).Method + ], + Ldc_I4_0 + ] + ); private static void Callback(GameObject __instance) { diff --git a/NitroxPatcher/Patches/Dynamic/Builder_TryPlace_Patch.cs b/NitroxPatcher/Patches/Dynamic/Builder_TryPlace_Patch.cs index b8274658fb..6444f6cf23 100644 --- a/NitroxPatcher/Patches/Dynamic/Builder_TryPlace_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Builder_TryPlace_Patch.cs @@ -20,52 +20,31 @@ public sealed partial class Builder_TryPlace_Patch : NitroxPatch, IDynamicPatch { public static readonly MethodInfo TARGET_METHOD = Reflect.Method(() => Builder.TryPlace()); - public static readonly InstructionsPattern AddInstructionPattern1 = new() - { - Ldloc_0, - Ldc_I4_0, - Ldc_I4_1, - new() { OpCode = Callvirt, Operand = new(nameof(Constructable), nameof(Constructable.SetState)) }, - { Pop, "Insert1" } - }; - - public static readonly List InstructionsToAdd1 = new() - { - new(Ldloc_0), - new(Call, Reflect.Method(() => GhostCreated(default))) - }; - - public static readonly InstructionsPattern AddInstructionPattern2 = new() - { - Ldloc_S, - Ldloc_3, - Ldloc_S, - Or, - { new() { OpCode = Callvirt, Operand = new(nameof(Constructable), nameof(Constructable.SetIsInside)) }, "Insert2" } - }; - - public static readonly List InstructionsToAdd2 = new() - { - TARGET_METHOD.Ldloc(), - new(Call, Reflect.Method(() => GhostCreated(default))) - }; - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => - instructions.Transform(AddInstructionPattern1, (label, instruction) => - { - if (label.Equals("Insert1")) - { - return InstructionsToAdd1; - } - return null; - }).Transform(AddInstructionPattern2, (label, instruction) => - { - if (label.Equals("Insert2")) - { - return InstructionsToAdd2; - } - return null; - }); + instructions.RewriteOnPattern( + [ + Ldloc_0, + Ldc_I4_0, + Ldc_I4_1, + Reflect.Method((Constructable c) => c.SetState(default, default)), + Pop, + [ + Ldloc_0, + Reflect.Method(() => GhostCreated(default)) + ] + ]) + .RewriteOnPattern( + [ + Ldloc_S, + Ldloc_3, + Ldloc_S, + Or, + Reflect.Method((Constructable c) => c.SetIsInside(default)), + [ + TARGET_METHOD.Ldloc(), + Reflect.Method(() => GhostCreated(default)) + ] + ]); public static void GhostCreated(Constructable constructable) { diff --git a/NitroxPatcher/Patches/Dynamic/ConstructableBase_SetState_Patch.cs b/NitroxPatcher/Patches/Dynamic/ConstructableBase_SetState_Patch.cs index 78a1bd150c..011ac231a2 100644 --- a/NitroxPatcher/Patches/Dynamic/ConstructableBase_SetState_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ConstructableBase_SetState_Patch.cs @@ -17,33 +17,21 @@ public sealed partial class ConstructableBase_SetState_Patch : NitroxPatch, IDyn /* * Make it become - * if (Builder.CanDestroyObject(gameObject)) - * { - * ConstructableBase_SetState_Patch.BeforeDestroy(gameObject); <========== - * UnityEngine.Object.Destroy(gameObject); - * } + * if (Builder.CanDestroyObject(gameObject)) + * { + * ConstructableBase_SetState_Patch.BeforeDestroy(gameObject); <========== + * UnityEngine.Object.Destroy(gameObject); + * } */ - public static readonly InstructionsPattern InstructionPattern = new() - { - new() { OpCode = Call, Operand = new(nameof(Builder), nameof(Builder.CanDestroyObject)) }, - { Brfalse, "Insert" } - }; - - public static readonly List InstructionsToAdd = new() - { - TARGET_METHOD.Ldloc(), - new(Call, Reflect.Method(() => BeforeDestroy(default))) - }; - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => - instructions.Transform(InstructionPattern, (label, instruction) => - { - if (label.Equals("Insert")) - { - return InstructionsToAdd; - } - return null; - }); + instructions.RewriteOnPattern([ + Reflect.Method(() => Builder.CanDestroyObject(default)), + Brfalse, + [ + TARGET_METHOD.Ldloc(), + Reflect.Method(() => BeforeDestroy(default)) + ] + ]); public static void BeforeDestroy(GameObject gameObject) { diff --git a/NitroxPatcher/Patches/Dynamic/Constructable_Construct_Patch.cs b/NitroxPatcher/Patches/Dynamic/Constructable_Construct_Patch.cs index ee8140c736..ecbff06b57 100644 --- a/NitroxPatcher/Patches/Dynamic/Constructable_Construct_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Constructable_Construct_Patch.cs @@ -30,32 +30,22 @@ public sealed partial class Constructable_Construct_Patch : NitroxPatch, IDynami private static TemporaryBuildData Temp => BuildingHandler.Main.Temp; - public static readonly InstructionsPattern InstructionsPattern = new() - { - Div, - Stfld, - Ldc_I4_0, - Ret, - Ldarg_0, - { InstructionPattern.Call(nameof(Constructable), nameof(Constructable.UpdateMaterial)), "Insert" } - }; - - public static readonly List InstructionsToAdd = new() - { - new(Ldarg_0), - new(Ldc_I4_1), // True for "constructing" - new(Call, Reflect.Method(() => ConstructionAmountModified(default, default))) - }; - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => - instructions.Transform(InstructionsPattern, (label, instruction) => - { - if (label.Equals("Insert")) - { - return InstructionsToAdd; - } - return null; - }); + instructions.RewriteOnPattern( + [ + Div, + Stfld, + Ldc_I4_0, + Ret, + Ldarg_0, + Reflect.Method((Constructable c) => c.UpdateMaterial()), + [ + Ldarg_0, + Ldc_I4_1, // True for "constructing" + Reflect.Method(() => ConstructionAmountModified(default, default)) + ] + ] + ); public static void ConstructionAmountModified(Constructable constructable, bool constructing) { @@ -90,7 +80,7 @@ public static void ConstructionAmountModified(Constructable constructable, bool return; } IEnumerator postSpawner = BuildingPostSpawner.ApplyPostSpawner(constructable.gameObject, entityId); - + // Can be null if no post spawner is set for the constructable's techtype if (postSpawner != null) { diff --git a/NitroxPatcher/Patches/Dynamic/Constructable_DeconstructAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/Constructable_DeconstructAsync_Patch.cs index 28ca273039..acdcb596a6 100644 --- a/NitroxPatcher/Patches/Dynamic/Constructable_DeconstructAsync_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/Constructable_DeconstructAsync_Patch.cs @@ -11,28 +11,17 @@ public sealed partial class Constructable_DeconstructAsync_Patch : NitroxPatch, { public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((Constructable t) => t.DeconstructAsync(default, default))); - public static readonly InstructionsPattern InstructionsPattern = new() - { - Ldc_I4_0, - Ret, - Ldloc_1, - { InstructionPattern.Call(nameof(Constructable), nameof(Constructable.UpdateMaterial)), "InsertDestruction" } - }; - - public static readonly List InstructionsToAdd = new() - { - new(Ldloc_1), - new(Ldc_I4_0), // False for "constructing" - new(Call, Reflect.Method(() => Constructable_Construct_Patch.ConstructionAmountModified(default, default))) - }; - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => - instructions.Transform(InstructionsPattern, (label, instruction) => - { - if (label.Equals("InsertDestruction")) - { - return InstructionsToAdd; - } - return null; - }); + instructions.RewriteOnPattern( + [ + Ldc_I4_0, + Ret, + Ldloc_1, + Reflect.Method((Constructable c) => c.UpdateMaterial()), + [ + Ldloc_1, + Ldc_I4_0, // False for "constructing" + Reflect.Method(() => Constructable_Construct_Patch.ConstructionAmountModified(default, default)) + ] + ]); } diff --git a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs index fb519cf3b9..b33048d5a4 100644 --- a/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/CyclopsDestructionEvent_SpawnLootAsync_Patch.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Reflection; -using System.Reflection.Emit; using HarmonyLib; using NitroxClient.Communication; using NitroxClient.GameLogic; @@ -13,6 +12,7 @@ using NitroxModel_Subnautica.DataStructures; using NitroxPatcher.PatternMatching; using UnityEngine; +using static System.Reflection.Emit.OpCodes; namespace NitroxPatcher.Patches.Dynamic; @@ -20,23 +20,21 @@ public sealed partial class CyclopsDestructionEvent_SpawnLootAsync_Patch : Nitro { public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method((CyclopsDestructionEvent t) => t.SpawnLootAsync())); - // Matches twice, once for scrap metal and once for computer chips - public static readonly InstructionsPattern PATTERN = new(expectedMatches: 2) - { - { Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), "SpawnObject" } - }; - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) { return new CodeMatcher(instructions) - .MatchStartForward(new CodeMatch(OpCodes.Switch)) - .InsertAndAdvance(new CodeInstruction(OpCodes.Call, Reflect.Method(() => TrampolineCallback(default)))) + .MatchStartForward(new CodeMatch(Switch)) + .InsertAndAdvance(new CodeInstruction(Call, Reflect.Method(() => TrampolineCallback(default)))) .InstructionEnumeration() - .InsertAfterMarker(PATTERN, "SpawnObject", [ - new(OpCodes.Dup), - new(OpCodes.Ldloc_1), - new(OpCodes.Call, ((Action)SpawnObjectCallback).Method) - ]); + // Matches twice, once for scrap metal and once for computer chips + .RewriteOnPattern([ + Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), + [ + Dup, + Ldloc_1, + ((Action)SpawnObjectCallback).Method + ] + ], 2); } public static void SpawnObjectCallback(GameObject gameObject, CyclopsDestructionEvent __instance) diff --git a/NitroxPatcher/Patches/Dynamic/DevConsole_Update_Patch.cs b/NitroxPatcher/Patches/Dynamic/DevConsole_Update_Patch.cs index 43a32bd6fc..381ff4c6fc 100644 --- a/NitroxPatcher/Patches/Dynamic/DevConsole_Update_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/DevConsole_Update_Patch.cs @@ -3,6 +3,7 @@ using HarmonyLib; using NitroxModel.Helper; using NitroxPatcher.PatternMatching; +using NitroxPatcher.PatternMatching.Ops; using UnityEngine; using static System.Reflection.Emit.OpCodes; @@ -13,22 +14,20 @@ namespace NitroxPatcher.Patches.Dynamic; /// public sealed partial class DevConsole_Update_Patch : NitroxPatch, IDynamicPatch { - private static readonly InstructionsPattern devConsoleSetStateTruePattern = new() - { - Reflect.Method(() => Input.GetKeyDown(default(KeyCode))), - Brfalse, - Ldarg_0, - Ldfld, - Brtrue, - Ldarg_0, - { Ldc_I4_1, "ConsoleEnableFlag" }, - Reflect.Method((DevConsole t) => t.SetState(default(bool))) - }; - public static readonly MethodInfo TARGET_METHOD = Reflect.Method((DevConsole t) => t.Update()); - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) - { - return instructions.ChangeAtMarker(devConsoleSetStateTruePattern, "ConsoleEnableFlag", i => i.opcode = Ldc_I4_0); - } + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => + instructions + .RewriteOnPattern( + [ + Reflect.Method(() => Input.GetKeyDown(default(KeyCode))), + Brfalse, + Ldarg_0, + Ldfld, + Brtrue, + Ldarg_0, + PatternOp.Change(Ldc_I4_1, i => i.opcode = Ldc_I4_0), + Reflect.Method((DevConsole t) => t.SetState(default(bool))) + ] + ); } diff --git a/NitroxPatcher/Patches/Dynamic/ItemsContainer_DestroyItem_Patch.cs b/NitroxPatcher/Patches/Dynamic/ItemsContainer_DestroyItem_Patch.cs index 288211160e..64655bb959 100644 --- a/NitroxPatcher/Patches/Dynamic/ItemsContainer_DestroyItem_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/ItemsContainer_DestroyItem_Patch.cs @@ -11,31 +11,26 @@ namespace NitroxPatcher.Patches.Dynamic; /// -/// When the player crafts items the game will leverage this API to select a pickupable -/// from their inventory and delete it. We want to let the server know that the item -/// was successfully deleted. +/// When the player crafts items the game will leverage this API to select a pickupable +/// from their inventory and delete it. We want to let the server know that the item +/// was successfully deleted. /// public sealed partial class ItemsContainer_DestroyItem_Patch : NitroxPatch, IDynamicPatch { internal static readonly MethodInfo TARGET_METHOD = Reflect.Method((ItemsContainer t) => t.DestroyItem(default(TechType))); - private static readonly InstructionsPattern removeItemPattern = new() - { - Ldarg_0, - Ldarg_1, - Reflect.Method((ItemsContainer container) => container.RemoveItem(default(TechType))), - { Stloc_0, "NotifyServer" } - }; - - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) - { + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => // After the call to RemoveItem (and storing the return value) we want to call our callback method - return instructions.InsertAfterMarker(removeItemPattern, "NotifyServer", new CodeInstruction[] - { - new(Ldloc_0), - new(Call, Reflect.Method(() => Callback(default))) - }); - } + instructions.RewriteOnPattern([ + Ldarg_0, + Ldarg_1, + Reflect.Method((ItemsContainer container) => container.RemoveItem(default(TechType))), + Stloc_0, + [ + Ldloc_0, + Reflect.Method(() => Callback(default)) + ] + ]); private static void Callback(Pickupable pickupable) { diff --git a/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs b/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs index f076206232..44877ed36d 100644 --- a/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/SpawnOnKill_OnKill_Patch.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Reflection; using HarmonyLib; using NitroxClient.Communication.Abstract; using NitroxClient.GameLogic; @@ -6,36 +8,31 @@ using NitroxModel.Helper; using NitroxModel.Packets; using NitroxPatcher.PatternMatching; -using System.Collections.Generic; -using System.Reflection; using UnityEngine; using static System.Reflection.Emit.OpCodes; namespace NitroxPatcher.Patches.Dynamic; /// -/// Synchronizes entities that Spawn something when they are killed, e.g. Coral Disks. +/// Synchronizes entities that Spawn something when they are killed, e.g. Coral Disks. /// public sealed partial class SpawnOnKill_OnKill_Patch : NitroxPatch, IDynamicPatch { public static readonly MethodInfo TARGET_METHOD = Reflect.Method((SpawnOnKill t) => t.OnKill()); - private static readonly InstructionsPattern spawnInstanceOnKillPattern = new() - { - Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), - { Stloc_0, "DropOnKillInstance" }, - Ldarg_0, - }; - - public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) - { - return instructions.InsertAfterMarker(spawnInstanceOnKillPattern, "DropOnKillInstance", new CodeInstruction[] - { - new(Ldarg_0), - new(Ldloc_0), - new(Call, ((Action)Callback).Method) - }); - } + public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => + instructions.RewriteOnPattern( + [ + Reflect.Method(() => UnityEngine.Object.Instantiate(default(GameObject), default(Vector3), default(Quaternion))), + Stloc_0, + [ + Ldarg_0, + Ldloc_0, + ((Action)Callback).Method + ], + Ldarg_0 + ] + ); private static void Callback(SpawnOnKill spawnOnKill, GameObject spawningItem) { diff --git a/NitroxPatcher/Patches/Dynamic/uSkyManager_SetVaryingMaterialProperties_Patch.cs b/NitroxPatcher/Patches/Dynamic/uSkyManager_SetVaryingMaterialProperties_Patch.cs index 671316f03e..a9f62eb492 100644 --- a/NitroxPatcher/Patches/Dynamic/uSkyManager_SetVaryingMaterialProperties_Patch.cs +++ b/NitroxPatcher/Patches/Dynamic/uSkyManager_SetVaryingMaterialProperties_Patch.cs @@ -4,6 +4,7 @@ using NitroxClient.GameLogic; using NitroxModel.Helper; using NitroxPatcher.PatternMatching; +using NitroxPatcher.PatternMatching.Ops; using UnityEngine; using static System.Reflection.Emit.OpCodes; @@ -17,19 +18,6 @@ public sealed partial class uSkyManager_SetVaryingMaterialProperties_Patch : Nit { public static readonly MethodInfo TARGET_METHOD = Reflect.Method((uSkyManager t) => t.SetVaryingMaterialProperties(default)); - /// - /// This pattern detects the property in the following line - /// and replaces the property call target to : - /// Quaternion q = Quaternion.AngleAxis(cloudsRotateSpeed * Time.time, Vector3.up); - /// - public static readonly InstructionsPattern ModifyInstructionPattern = new() - { - Ldarg_0, - Ldfld, - { Reflect.Property(() => Time.time).GetMethod, "Modify" }, - Mul - }; - /// /// Intermediate time property to simplify the dependency resolving for the transpiler. /// @@ -39,5 +27,12 @@ public sealed partial class uSkyManager_SetVaryingMaterialProperties_Patch : Nit /// Replaces Time.time call to Time.realtimeSinceStartup so that it doesn't take Time.timeScale into account /// public static IEnumerable Transpiler(MethodBase original, IEnumerable instructions) => instructions - .ChangeAtMarker(ModifyInstructionPattern, "Modify", instruction => instruction.operand = Reflect.Property(() => CurrentTime).GetMethod); + .RewriteOnPattern( + [ + Ldarg_0, + Ldfld, + PatternOp.Change(Reflect.Property(() => Time.time).GetMethod, i => i.operand = Reflect.Property(() => CurrentTime).GetMethod), + Mul + ] + ); } diff --git a/NitroxPatcher/PatternMatching/ILExtensions.cs b/NitroxPatcher/PatternMatching/ILExtensions.cs index d6b6e4ca50..ae33e405ff 100644 --- a/NitroxPatcher/PatternMatching/ILExtensions.cs +++ b/NitroxPatcher/PatternMatching/ILExtensions.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.RegularExpressions; using HarmonyLib; +using NitroxPatcher.PatternMatching.Ops; namespace NitroxPatcher.PatternMatching; @@ -11,7 +12,8 @@ internal static class ILExtensions private static readonly Regex spaceRegex = new(Regex.Escape(" ")); /// - /// Makes a string of an indexed list of instructions, line by line, formatted to have all opcodes and operand aligned in columns. + /// Makes a string of an indexed list of instructions, line by line, formatted to have all opcodes and operand aligned + /// in columns. /// public static string ToPrettyString(this IEnumerable instructions) { @@ -20,17 +22,6 @@ public static string ToPrettyString(this IEnumerable instructio { return "No instructions"; } - int tenPower = 0; - int count = instructionList.Count; - - while (count > 10) - { - count /= 10; - tenPower++; - } - - // if tenPower is 1 (number between 10 and 99), there are 2 numbers to show so we always add 1 to tenPower - string format = $"D{tenPower + 1}"; // We need to find the max length of the opcodes to have all of them take the same amount of space int opcodeMaxLength = 0; @@ -43,6 +34,15 @@ public static string ToPrettyString(this IEnumerable instructio } } + int tenPower = 0; + int count = instructionList.Count; + while (count > 10) + { + count /= 10; + tenPower++; + } + string format = $"D{tenPower + 1}"; // if tenPower is 1 (number between 10 and 99), there are 2 numbers to show so we always add 1 to tenPower + StringBuilder builder = new(); for (int i = 0; i < instructionList.Count; i++) { @@ -52,60 +52,109 @@ public static string ToPrettyString(this IEnumerable instructio string instructionToString = spaceRegex.Replace(instruction.ToString(), new string(' ', spacesRequired), 1); builder.AppendLine($"{i.ToString(format)} {instructionToString}"); } - return builder.ToString(); } /// - /// Iterates the instructions, searching for the given pattern. When the pattern matches, the transform function is - /// called. If the pattern does not match the expected match count , - /// an exception is thrown. + /// Rewrites the instructions based on the passed in pattern and its operations. + /// Sub-arrays will insert new instructions at the specific position in the pattern. + /// Use static functions on for more operations. /// - public static IEnumerable Transform(this IEnumerable instructions, InstructionsPattern pattern, Func> transform) - { - return pattern.ApplyTransform(instructions, transform); - } - - /// - public static IEnumerable Transform(this IEnumerable instructions, InstructionsPattern pattern, Action transform) + /// The modified instructions. + public static IEnumerable RewriteOnPattern(this IEnumerable instructions, PatternOp[] pattern, int expectedMatches = 1) { - return pattern.ApplyTransform(instructions, (label, instruction) => + if (pattern is null or []) { - transform(label, instruction); - return null; - }); - } + return instructions; + } + IList il = instructions as IList ?? [..instructions]; + int matches = 0; + // Starts from the bottom so that operations can be applied within the same loop. + for (int ilIndex = il.Count - 1; ilIndex >= 0; ilIndex--) + { + // See if pattern matches entirely from current IL position. + if (!IsPatternMatch(il, ilIndex, pattern)) + { + continue; + } + matches++; + // Match found: run operations defined in the pattern at current position. + ApplyOperations(il, ilIndex, pattern); + } + if (matches != expectedMatches) + { + throw new Exception($"Expected {expectedMatches} matches but actual was {matches}"); + } + return il; - /// - /// Inserts the new instructions on every occurence of the marker, as defined by the pattern. - /// - /// Code with the additions. - public static IEnumerable InsertAfterMarker(this IEnumerable instructions, InstructionsPattern pattern, string marker, CodeInstruction[] newInstructions) - { - return pattern.ApplyTransform(instructions, (m, _) => + static bool IsPatternMatch(IList il, int ilStartIndex, PatternOp[] pattern) { - if (m.Equals(marker, StringComparison.Ordinal)) + int ilOffset = 0; + foreach (PatternOp op in pattern) { - return newInstructions; + // Abort if trying to go out of IL range. + if (ilStartIndex + ilOffset >= il.Count) + { + return false; + } + + CodeInstruction currentIl = il[ilStartIndex + ilOffset]; + // Type check which operation we're dealing with. + switch (op.Op) + { + case PatternOpInstruction patternIl: + ilOffset++; + if (patternIl != currentIl) + { + return false; + } + break; + case PatternOpCodeOpChange patternOpCodeChange: + ilOffset++; + if (patternOpCodeChange.ExpectedOpCode != currentIl.opcode) + { + return false; + } + break; + case PatternMethodOpChange patternMethodChange: + ilOffset++; + if (!currentIl.OperandIs(patternMethodChange.ExpectedMethod)) + { + return false; + } + break; + } } - return null; - }); - } + return true; + } - /// - /// Calls the action on each instruction matching the given marker, as defined by the - /// pattern. - /// - public static IEnumerable ChangeAtMarker(this IEnumerable instructions, InstructionsPattern pattern, string marker, Action instructionChange) - { - return pattern.ApplyTransform(instructions, (m, instruction) => + static void ApplyOperations(IList il, int ilStartIndex, PatternOp[] pattern) { - if (m.Equals(marker, StringComparison.Ordinal)) + for (int ilIndex = ilStartIndex + pattern.Length - 1; ilIndex >= 0; ilIndex--) { - instructionChange(instruction); + int patternIndex = ilIndex - ilStartIndex; + if (patternIndex <= -1) + { + break; + } + + PatternOp op = pattern[patternIndex]; + switch (op.Op) + { + case PatternOpInsert insert: + for (int i = insert.Instructions.Count - 1; i >= 0; i--) + { + il.Insert(ilIndex, insert.Instructions[i]); + } + break; + case PatternOpCodeOpChange patternOpCodeChange: + patternOpCodeChange.Changer(il[ilIndex]); + break; + case PatternMethodOpChange patternMethodChange: + patternMethodChange.Changer(il[ilIndex]); + break; + } } - return null; - }); + } } } diff --git a/NitroxPatcher/PatternMatching/IPatternOp.cs b/NitroxPatcher/PatternMatching/IPatternOp.cs new file mode 100644 index 0000000000..1a54c1cfa2 --- /dev/null +++ b/NitroxPatcher/PatternMatching/IPatternOp.cs @@ -0,0 +1,3 @@ +namespace NitroxPatcher.PatternMatching; + +public interface IPatternOp; diff --git a/NitroxPatcher/PatternMatching/InstructionPattern.cs b/NitroxPatcher/PatternMatching/InstructionPattern.cs deleted file mode 100644 index c74645bc21..0000000000 --- a/NitroxPatcher/PatternMatching/InstructionPattern.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using HarmonyLib; -using NitroxModel.Helper; - -namespace NitroxPatcher.PatternMatching; - -public readonly struct InstructionPattern -{ - public bool Equals(InstructionPattern other) => OpCode.Equals(other.OpCode) && Operand.Equals(other.Operand) && Label == other.Label; - - public override bool Equals(object obj) => obj is InstructionPattern other && Equals(other); - - public override int GetHashCode() - { - unchecked - { - int hashCode = OpCode.GetHashCode(); - hashCode = (hashCode * 397) ^ Operand.GetHashCode(); - hashCode = (hashCode * 397) ^ (Label != null ? Label.GetHashCode() : 0); - return hashCode; - } - } - - public OpCodePattern OpCode { get; init; } - public OperandPattern Operand { get; init; } - public string Label { get; init; } - - public static implicit operator InstructionPattern(OpCode opCode) => new() { OpCode = opCode }; - public static implicit operator InstructionPattern(OperandPattern operand) => new() { Operand = operand }; - public static implicit operator InstructionPattern(MethodInfo method) => Call(method, true); - - public static InstructionPattern Call(string className, string methodName) => new() { OpCode = OpCodes.Call, Operand = new(className, methodName) }; - - public static InstructionPattern Call(MethodInfo method) => Call(method, false); - - private static InstructionPattern Call(MethodInfo method, bool matchAnyCallOpcode) - { - Type methodDeclaringType = method.DeclaringType; - Validate.NotNull(methodDeclaringType); - - return new() - { - OpCode = new OpCodePattern - { - OpCode = OpCodes.Call, - WeakMatch = matchAnyCallOpcode - }, - Operand = new(methodDeclaringType.FullName, method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray()) - }; - } - - public static bool operator ==(InstructionPattern pattern, CodeInstruction instruction) - { - if (instruction == null) - { - return false; - } - return pattern.OpCode == instruction.opcode && pattern.Operand == instruction.operand; - } - - public static bool operator ==(CodeInstruction instruction, InstructionPattern pattern) - { - return pattern == instruction; - } - - public static bool operator !=(CodeInstruction instruction, InstructionPattern pattern) - { - return !(instruction == pattern); - } - - public static bool operator !=(InstructionPattern pattern, CodeInstruction instruction) - { - return !(pattern == instruction); - } - - public override string ToString() => $"{OpCode.OpCode}{(Operand != default ? $" {Operand}" : "")}{(Label != null ? $" '{Label}'" : "")}"; -} diff --git a/NitroxPatcher/PatternMatching/InstructionsPattern.cs b/NitroxPatcher/PatternMatching/InstructionsPattern.cs deleted file mode 100644 index 6ab86ac3f6..0000000000 --- a/NitroxPatcher/PatternMatching/InstructionsPattern.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; - -namespace NitroxPatcher.PatternMatching; - -/// -/// Pattern matching is NOT thread safe. -/// -public class InstructionsPattern : IEnumerable -{ - private readonly int expectedMatches; - private readonly List pattern = new(); - - /// - /// Creates a new IL pattern to apply transforms to IL. By default, a pattern expects to match exactly once. - /// - public InstructionsPattern(int expectedMatches = 1) - { - if (expectedMatches < 1) - { - throw new ArgumentException($"Expected matches must be at least 1 but was {this.expectedMatches}", nameof(this.expectedMatches)); - } - this.expectedMatches = expectedMatches; - } - - public IEnumerator GetEnumerator() => pattern.GetEnumerator(); - - public void Add(InstructionPattern instruction) - { - pattern.Add(instruction); - } - - public void Add(InstructionPattern instruction, string label) - { - pattern.Add(instruction with { Label = label }); - } - - public IEnumerable ApplyTransform(IEnumerable instructions, Func> transform) - { - CodeInstruction[] il = instructions as CodeInstruction[] ?? instructions.ToArray(); - Dictionary> insertOperations = new(); - int matchCount = 0; -#if DEBUG - SetBestMatchAttemptIndex(-1); -#endif - for (int i = 0; i < il.Length; i++) - { - // If pattern can't fit in remaining instructions, abort. - if (i + pattern.Count > il.Length) - { - break; - } - // Test for pattern on current IL position. - bool patternMatched = pattern.Count > 0; - for (int j = 0; j < pattern.Count; j++) - { - CodeInstruction curInstr = il[i + j]; - InstructionPattern curInstrPattern = pattern[j]; - if (curInstr != curInstrPattern) - { - patternMatched = false; - break; - } -#if DEBUG - RememberBestMatchAttempt(j); -#endif - } - if (!patternMatched) - { - continue; - } - matchCount++; - - // Pattern matched: now run through pattern again, adding operations at the labelled instructions. - for (int j = 0; j < pattern.Count; j++) - { - if (!string.IsNullOrEmpty(pattern[j].Label)) - { - CodeInstruction instrAtLabel = il[i + j]; - IEnumerable insertingInstructions = transform(pattern[j].Label, instrAtLabel); - if (insertingInstructions != null) - { - insertOperations.Add(i + j, insertingInstructions); - } - } - } - } - if (matchCount != expectedMatches) - { - throw new Exception($"Expected pattern to match {expectedMatches} times but was {matchCount}. {Environment.NewLine}Pattern:{Environment.NewLine}{this}{Environment.NewLine}IL:{Environment.NewLine}{il.ToPrettyString()}"); - } - - // Apply operations on index of IL or return the original instruction. - for (int i = 0; i < il.Length; i++) - { - yield return il[i]; - if (insertOperations.TryGetValue(i, out IEnumerable inserts)) - { - foreach (CodeInstruction newInstruction in inserts) - { - yield return newInstruction; - } - } - } - } - -#if DEBUG - private int bestMatchAttemptIndex = -1; - - private void SetBestMatchAttemptIndex(int value) - { - bestMatchAttemptIndex = value; - } - - private void RememberBestMatchAttempt(int value) - { - SetBestMatchAttemptIndex(bestMatchAttemptIndex < value ? value : bestMatchAttemptIndex); - } -#endif - - public override string ToString() => string.Join(Environment.NewLine, pattern.Select((p, i) => - { - string result = p.ToString(); -#if DEBUG - if (bestMatchAttemptIndex >= 0 && bestMatchAttemptIndex == i) - { - result += " <-- last matched pattern index before failure"; - } -#endif - return result; - })); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} diff --git a/NitroxPatcher/PatternMatching/Ops/PatternMethodOpChange.cs b/NitroxPatcher/PatternMatching/Ops/PatternMethodOpChange.cs new file mode 100644 index 0000000000..e5a0aecad4 --- /dev/null +++ b/NitroxPatcher/PatternMatching/Ops/PatternMethodOpChange.cs @@ -0,0 +1,15 @@ +using System; +using System.Reflection; +using HarmonyLib; + +namespace NitroxPatcher.PatternMatching.Ops; + +public readonly struct PatternMethodOpChange : IPatternOp +{ + public PatternMethodOpChange() + { + } + + public MethodInfo ExpectedMethod { get; init; } + public Action Changer { get; init; } = _ => { }; +} diff --git a/NitroxPatcher/PatternMatching/Ops/PatternOp.cs b/NitroxPatcher/PatternMatching/Ops/PatternOp.cs new file mode 100644 index 0000000000..2e09692259 --- /dev/null +++ b/NitroxPatcher/PatternMatching/Ops/PatternOp.cs @@ -0,0 +1,104 @@ +extern alias JB; +using System; +using System.Collections; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; + +namespace NitroxPatcher.PatternMatching.Ops; + +/// +/// Pattern operation wrapper which contains the actual operation. Used to implicitly convert an input to a specific +/// operation. +/// +public struct PatternOp(IPatternOp op) : IPatternOp, IEnumerable +{ + public IPatternOp Op { get; private set; } = op; + + public static implicit operator PatternOp(OpCode opCode) + { + return new PatternOp { Op = new PatternOpInstruction { OpCode = opCode } }; + } + + public static implicit operator PatternOp(OperandPattern operand) + { + return new PatternOp { Op = new PatternOpInstruction { Operand = operand } }; + } + + public static implicit operator PatternOp(MethodInfo method) + { + return new PatternOp { Op = PatternOpInstruction.Call(method, true) }; + } + + public static implicit operator PatternOp(CodeInstruction instruction) + { + return new PatternOp + { + Op = new PatternOpInstruction + { + OpCode = instruction.opcode, + Operand = instruction.operand, + Labels = instruction.labels + } + }; + } + + public IEnumerator GetEnumerator() + { + throw new NotSupportedException(); + } + + public void Add(PatternOp op) + { + Op ??= new PatternOpInsert(); + if (Op is not PatternOpInsert insert) + { + throw new InvalidOperationException("Pattern operation must be an insert operation for Add() to work"); + } + if (op.Op is not PatternOpInstruction instruction) + { + throw new NotSupportedException($"Only {nameof(PatternOpInstruction)} can be added to an insert operation"); + } + if (instruction.OpCode.OpCode is not { } opCode) + { + throw new Exception($"{nameof(OpCode)} must not be null for new (to be inserted) instructions"); + } + insert.Instructions.Add(new CodeInstruction(opCode, instruction.Operand)); + } + + /// + /// Changes the if the current pattern matches entirely. + /// + /// The to look for at the current pattern position. + /// The action to invoke on the current IL position, if the pattern matched. + public static PatternOp Change(OpCode expectedOpCode, Action action) + { + return new PatternOp + { + Op = new PatternOpCodeOpChange + { + ExpectedOpCode = expectedOpCode, + Changer = action + } + }; + } + + /// + /// Changes the if the current pattern matches entirely. + /// + /// The to look for at the current pattern position. + /// The action to invoke on the current IL position, if the pattern matched. + public static PatternOp Change(MethodInfo expectedMethod, Action action) + { + return new PatternOp + { + Op = new PatternMethodOpChange + { + ExpectedMethod = expectedMethod, + Changer = action + } + }; + } + + public override string ToString() => Op?.ToString() ?? $"{nameof(Op)}: {Op}"; +} diff --git a/NitroxPatcher/PatternMatching/Ops/PatternOpCodeOpChange.cs b/NitroxPatcher/PatternMatching/Ops/PatternOpCodeOpChange.cs new file mode 100644 index 0000000000..ad5410f70b --- /dev/null +++ b/NitroxPatcher/PatternMatching/Ops/PatternOpCodeOpChange.cs @@ -0,0 +1,15 @@ +using System; +using System.Reflection.Emit; +using HarmonyLib; + +namespace NitroxPatcher.PatternMatching.Ops; + +public readonly struct PatternOpCodeOpChange : IPatternOp +{ + public PatternOpCodeOpChange() + { + } + + public OpCode ExpectedOpCode { get; init; } + public Action Changer { get; init; } = _ => { }; +} diff --git a/NitroxPatcher/PatternMatching/Ops/PatternOpInsert.cs b/NitroxPatcher/PatternMatching/Ops/PatternOpInsert.cs new file mode 100644 index 0000000000..147c9dfa88 --- /dev/null +++ b/NitroxPatcher/PatternMatching/Ops/PatternOpInsert.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using HarmonyLib; + +namespace NitroxPatcher.PatternMatching.Ops; + +public readonly struct PatternOpInsert : IPatternOp +{ + public PatternOpInsert() + { + } + + public IList Instructions { get; init; } = []; + + public static implicit operator PatternOpInsert(List instructions) => new() { Instructions = instructions }; +} diff --git a/NitroxPatcher/PatternMatching/Ops/PatternOpInstruction.cs b/NitroxPatcher/PatternMatching/Ops/PatternOpInstruction.cs new file mode 100644 index 0000000000..ba3b07849f --- /dev/null +++ b/NitroxPatcher/PatternMatching/Ops/PatternOpInstruction.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using HarmonyLib; +using NitroxModel.Helper; + +namespace NitroxPatcher.PatternMatching.Ops; + +public readonly record struct PatternOpInstruction() : IPatternOp +{ + public OpCodePattern OpCode { get; init; } = default; + public object Operand { get; init; } = default; + public List