Skip to content

Commit

Permalink
Improved pattern matching readability
Browse files Browse the repository at this point in the history
  • Loading branch information
Measurity committed Jan 6, 2025
1 parent 52cd05d commit fb797b9
Show file tree
Hide file tree
Showing 23 changed files with 643 additions and 583 deletions.
14 changes: 0 additions & 14 deletions Nitrox.Test/Patcher/PatchTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodeInstruction> originalIl, out IEnumerable<CodeInstruction> transformedIl)
{
bool shouldHappen = false;
originalIl = PatchProcessor.GetCurrentInstructions(targetMethod);
transformedIl = originalIl
.Transform(pattern, (_, _) =>
{
shouldHappen = true;
})
.ToArray(); // Required, otherwise nothing happens.

shouldHappen.Should().BeTrue();
}

/// <summary>
/// Clones the instructions so that the returned instructions are not the same reference.
/// </summary>
Expand Down
60 changes: 21 additions & 39 deletions Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -90,7 +90,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)
{
Expand Down Expand Up @@ -146,7 +146,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)
Expand All @@ -155,7 +162,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;
Expand All @@ -177,35 +191,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<CodeInstruction>
{
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);
}
}
}
110 changes: 110 additions & 0 deletions Nitrox.Test/Patcher/PatternMatching/RewriteOnPatternTest.cs
Original file line number Diff line number Diff line change
@@ -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<CodeInstruction> 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<CodeInstruction>().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<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], -1));
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], 0));
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], 1));
Assert.ThrowsException<Exception>(() => 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,50 +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" },
};

public static IEnumerable<CodeInstruction> InstructionsToAdd(bool destroyed)
{
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)));
}
private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;

public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> 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;
});
instructions.RewriteOnPattern(
[
Callvirt,
Call,
Ldloc_3,
Reflect.Method((BaseGhost b) => b.ClearTargetBase()),
[
Ldarg_0,
Ldloc_2,
Ldloc_0,
Ldc_I4_1,
Reflect.Method(() => PieceDeconstructed(default, default, default, default))
],
])
.RewriteOnPattern(
[
Ldloc_0,
Reflect.Method((Base b) => b.FixCorridorLinks()),
Ldloc_0,
Reflect.Method((Base b) => b.RebuildGeometry()),
[
Ldarg_0,
Ldloc_2,
Ldloc_0,
Ldc_I4_0,
Reflect.Method(() => PieceDeconstructed(default, default, default, default))
],
]);

public static void Prefix(BaseDeconstructable __instance)
{
Expand Down Expand Up @@ -180,9 +170,9 @@ public static void PieceDeconstructed(BaseDeconstructable baseDeconstructable, C
BuildingHandler.Main.EnsureTracker(baseId).LocalOperations++;
int operationId = BuildingHandler.Main.GetCurrentOperationIdOrDefault(baseId);

PieceDeconstructed 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 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<IPacketSender>().Send(pieceDeconstructed);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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...
/// </summary>
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<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
return instructions.InsertAfterMarker(SpawnResFromPrefPattern, "DropItemInstance", new CodeInstruction[]
{
new(Ldloc_1),
new(Call, ((Action<GameObject>)Callback).Method)
});
}
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
instructions
.RewriteOnPattern(
[
Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))),
[
Ldloc_1, // Dropped item GameObject
((Action<GameObject>)Callback).Method
],
Ldc_I4_0
]
);

private static void Callback(GameObject __instance)
{
Expand Down
Loading

0 comments on commit fb797b9

Please sign in to comment.