Skip to content

Commit 9ef29c9

Browse files
committed
Improved pattern matching readability
1 parent 52cd05d commit 9ef29c9

23 files changed

+642
-582
lines changed

Nitrox.Test/Patcher/PatchTestHelper.cs

-14
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,6 @@ public static ILGenerator GetILGenerator(this MethodInfo method)
4949
return new DynamicMethod(method.Name, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
5050
}
5151

52-
public static void TestPattern(MethodInfo targetMethod, InstructionsPattern pattern, out IEnumerable<CodeInstruction> originalIl, out IEnumerable<CodeInstruction> transformedIl)
53-
{
54-
bool shouldHappen = false;
55-
originalIl = PatchProcessor.GetCurrentInstructions(targetMethod);
56-
transformedIl = originalIl
57-
.Transform(pattern, (_, _) =>
58-
{
59-
shouldHappen = true;
60-
})
61-
.ToArray(); // Required, otherwise nothing happens.
62-
63-
shouldHappen.Should().BeTrue();
64-
}
65-
6652
/// <summary>
6753
/// Clones the instructions so that the returned instructions are not the same reference.
6854
/// </summary>

Nitrox.Test/Patcher/Patches/PatchesTranspilerTest.cs

+21-39
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ public class PatchesTranspilerTest
1818
[typeof(AttackCyclops_OnCollisionEnter_Patch), -17],
1919
[typeof(AttackCyclops_UpdateAggression_Patch), -23],
2020
[typeof(Bullet_Update_Patch), 3],
21-
[typeof(BaseDeconstructable_Deconstruct_Patch), BaseDeconstructable_Deconstruct_Patch.InstructionsToAdd(true).Count() * 2],
21+
[typeof(BaseDeconstructable_Deconstruct_Patch), 10],
2222
[typeof(BaseHullStrength_CrushDamageUpdate_Patch), 3],
2323
[typeof(BreakableResource_SpawnResourceFromPrefab_Patch), 2],
24-
[typeof(Builder_TryPlace_Patch), Builder_TryPlace_Patch.InstructionsToAdd1.Count + Builder_TryPlace_Patch.InstructionsToAdd2.Count],
24+
[typeof(Builder_TryPlace_Patch), 4],
2525
[typeof(CellManager_TryLoadCacheBatchCells_Patch), 4],
26-
[typeof(Constructable_Construct_Patch), Constructable_Construct_Patch.InstructionsToAdd.Count],
27-
[typeof(Constructable_DeconstructAsync_Patch), Constructable_DeconstructAsync_Patch.InstructionsToAdd.Count],
28-
[typeof(ConstructableBase_SetState_Patch), ConstructableBase_SetState_Patch.InstructionsToAdd.Count],
26+
[typeof(Constructable_Construct_Patch), 3],
27+
[typeof(Constructable_DeconstructAsync_Patch), 3],
28+
[typeof(ConstructableBase_SetState_Patch), 2],
2929
[typeof(ConstructorInput_OnCraftingBegin_Patch), 7],
3030
[typeof(CrafterLogic_TryPickupSingleAsync_Patch), 4],
3131
[typeof(CrashHome_Spawn_Patch), 2],
@@ -90,7 +90,7 @@ public class PatchesTranspilerTest
9090
[TestMethod]
9191
public void AllTranspilerPatchesHaveSanityTest()
9292
{
93-
Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(NitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray();
93+
Type[] allPatchesWithTranspiler = typeof(NitroxPatcher.Main).Assembly.GetTypes().Where(p => typeof(INitroxPatch).IsAssignableFrom(p) && p.IsClass).Where(x => x.GetMethod("Transpiler") != null).ToArray();
9494

9595
foreach (Type patch in allPatchesWithTranspiler)
9696
{
@@ -146,7 +146,14 @@ public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bo
146146

147147
if (logInstructions)
148148
{
149+
Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~");
150+
Console.WriteLine("~~~ TRANSFORMED IL ~~~");
151+
Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~");
149152
Console.WriteLine(transformedIl.ToPrettyString());
153+
Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~");
154+
Console.WriteLine("~~~ ORIGINAL IL ~~~");
155+
Console.WriteLine("~~~~~~~~~~~~~~~~~~~~~~");
156+
Console.WriteLine(originalIlCopy.ToPrettyString());
150157
}
151158

152159
if (transformedIl == null || transformedIl.Count == 0)
@@ -155,7 +162,14 @@ public void AllPatchesTranspilerSanity(Type patchClassType, int ilDifference, bo
155162
}
156163

157164
originalIlCopy.Count.Should().Be(transformedIl.Count - ilDifference);
158-
Assert.IsFalse(originalIlCopy.SequenceEqual(transformedIl, new CodeInstructionComparer()), $"The transpiler patch of {patchClassType.Name} did not change the IL");
165+
if (originalIlCopy.Count == transformedIl.Count)
166+
{
167+
string originalIlPrettyString = originalIlCopy.ToPrettyString();
168+
if (originalIlPrettyString == transformedIl.ToPrettyString())
169+
{
170+
Assert.Fail($"The transpiler patch of {patchClassType.Name} did not change the IL:{Environment.NewLine}{originalIlPrettyString}");
171+
}
172+
}
159173
}
160174

161175
private static readonly ModuleBuilder patchTestModule;
@@ -177,35 +191,3 @@ private static ILGenerator GetILGenerator(MethodInfo method, Type generatingType
177191
return myTypeBld.DefineMethod(method.Name, MethodAttributes.Public, method.ReturnType, method.GetParameters().Types()).GetILGenerator();
178192
}
179193
}
180-
181-
public class CodeInstructionComparer : IEqualityComparer<CodeInstruction>
182-
{
183-
public bool Equals(CodeInstruction x, CodeInstruction y)
184-
{
185-
if (ReferenceEquals(x, y))
186-
{
187-
return true;
188-
}
189-
if (x is null)
190-
{
191-
return false;
192-
}
193-
if (y is null)
194-
{
195-
return false;
196-
}
197-
if (x.GetType() != y.GetType())
198-
{
199-
return false;
200-
}
201-
return x.opcode.Equals(y.opcode) && Equals(x.operand, y.operand);
202-
}
203-
204-
public int GetHashCode(CodeInstruction obj)
205-
{
206-
unchecked
207-
{
208-
return (obj.opcode.GetHashCode() * 397) ^ (obj.operand != null ? obj.operand.GetHashCode() : 0);
209-
}
210-
}
211-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using HarmonyLib;
2+
using NitroxModel.Helper;
3+
using NitroxPatcher.PatternMatching;
4+
using NitroxPatcher.PatternMatching.Ops;
5+
using NitroxTest.Patcher;
6+
using static System.Reflection.Emit.OpCodes;
7+
8+
namespace Nitrox.Test.Patcher.PatternMatching;
9+
10+
[TestClass]
11+
public class RewriteOnPatternTest
12+
{
13+
private static List<CodeInstruction> testCode;
14+
15+
[TestInitialize]
16+
public void TestInitialize()
17+
{
18+
testCode =
19+
[
20+
new(Ldarg_1),
21+
new(Callvirt, Reflect.Property((ResolveEventArgs args) => args.Name).GetGetMethod()),
22+
new(Ldc_I4_S),
23+
new(Ldc_I4_0),
24+
new(Callvirt, Reflect.Method((string s) => s.Split(default))),
25+
new(Ldc_I4_0),
26+
new(Ldelem_Ref),
27+
new(Stloc_0)
28+
];
29+
}
30+
31+
[TestMethod]
32+
public void ShouldDoNothingWithEmptyInstructions()
33+
{
34+
Array.Empty<CodeInstruction>().RewriteOnPattern([]).Should().BeEmpty();
35+
}
36+
37+
[TestMethod]
38+
public void ShouldReturnSameIfPatternDoesNotMatch()
39+
{
40+
testCode.RewriteOnPattern([Call], 0).Should().NotBeEmpty().And.HaveCount(testCode.Count);
41+
}
42+
43+
[TestMethod]
44+
public void ShouldNotMatchIfPatternLargerThanIl()
45+
{
46+
testCode.RewriteOnPattern([..testCode]).Should().NotBeEmpty().And.HaveCount(testCode.Count);
47+
testCode.RewriteOnPattern([..testCode, Callvirt], 0).Should().NotBeEmpty().And.HaveCount(testCode.Count);
48+
}
49+
50+
[TestMethod]
51+
public void ShouldNotMakeChangesIfNoOperationsInPattern()
52+
{
53+
CodeInstruction[] copy = testCode.Clone().ToArray();
54+
testCode.RewriteOnPattern([Ldc_I4_0], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count);
55+
copy.ToPrettyString().Should().Be(testCode.ToPrettyString());
56+
}
57+
58+
[TestMethod]
59+
public void ShouldThrowIfMatchingUnexpectedAmountOfTimes()
60+
{
61+
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], -1));
62+
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], 0));
63+
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], 1));
64+
Assert.ThrowsException<Exception>(() => testCode.RewriteOnPattern([Ldc_I4_0], 3));
65+
}
66+
67+
[TestMethod]
68+
public void ShouldDifferIfOperationsExecuted()
69+
{
70+
CodeInstruction[] copy = testCode.Clone().ToArray();
71+
testCode.RewriteOnPattern([PatternOp.Change(Ldc_I4_0, i => i.opcode = Ldc_I4_1)], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count);
72+
copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString());
73+
74+
// Pattern should now match without error.
75+
testCode.RewriteOnPattern([Ldc_I4_1, Callvirt]);
76+
}
77+
78+
[TestMethod]
79+
public void ShouldNotInsertIfEmptyInsertOperation()
80+
{
81+
CodeInstruction[] copy = testCode.Clone().ToArray();
82+
testCode.RewriteOnPattern([Ldc_I4_0, []], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count);
83+
copy.ToPrettyString().Should().Be(testCode.ToPrettyString());
84+
}
85+
86+
[TestMethod]
87+
public void ShouldAddIlIfInsertOperationExecuted()
88+
{
89+
CodeInstruction[] copy = testCode.Clone().ToArray();
90+
int originalCount = copy.Length;
91+
testCode.RewriteOnPattern([Ldc_I4_0, [Ldc_I4_1]], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count);
92+
copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString());
93+
copy.Should().HaveCount(originalCount);
94+
testCode.Should().HaveCount(originalCount + 2);
95+
}
96+
97+
[TestMethod]
98+
public void ShouldAddMultipleInstructionsIfInsertOperationHasMultiple()
99+
{
100+
CodeInstruction[] copy = testCode.Clone().ToArray();
101+
int originalCount = copy.Length;
102+
testCode.RewriteOnPattern([Ldc_I4_0, [Ldc_I4_1, Ldc_I4_1]], 2).Should().NotBeEmpty().And.HaveCount(testCode.Count);
103+
copy.ToPrettyString().Should().NotBe(testCode.ToPrettyString());
104+
copy.Should().HaveCount(originalCount);
105+
testCode.Should().HaveCount(originalCount + 4);
106+
107+
// Pattern should now match without error.
108+
testCode.RewriteOnPattern([Ldc_I4_0, Ldc_I4_1, Ldc_I4_1], 2);
109+
}
110+
}

NitroxPatcher/Patches/Dynamic/BaseDeconstructable_Deconstruct_Patch.cs

+33-43
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using NitroxModel.Helper;
1212
using NitroxModel.Packets;
1313
using NitroxPatcher.PatternMatching;
14+
using NitroxPatcher.PatternMatching.Ops;
1415
using UnityEngine;
1516
using static System.Reflection.Emit.OpCodes;
1617
using static NitroxClient.GameLogic.Bases.BuildingHandler;
@@ -20,50 +21,39 @@ namespace NitroxPatcher.Patches.Dynamic;
2021
public sealed partial class BaseDeconstructable_Deconstruct_Patch : NitroxPatch, IDynamicPatch
2122
{
2223
public static readonly MethodInfo TARGET_METHOD = Reflect.Method((BaseDeconstructable t) => t.Deconstruct());
23-
24-
private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;
2524
private static BuildPieceIdentifier cachedPieceIdentifier;
2625

27-
public static readonly InstructionsPattern BaseDeconstructInstructionPattern1 = new()
28-
{
29-
Callvirt,
30-
Call,
31-
Ldloc_3,
32-
{ new() { OpCode = Callvirt, Operand = new(nameof(BaseGhost), nameof(BaseGhost.ClearTargetBase)) }, "Insert1" }
33-
};
34-
public static readonly InstructionsPattern BaseDeconstructInstructionPattern2 = new()
35-
{
36-
Ldloc_0,
37-
new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.FixCorridorLinks)) },
38-
Ldloc_0,
39-
{ new() { OpCode = Callvirt, Operand = new(nameof(Base), nameof(Base.RebuildGeometry)) }, "Insert2" },
40-
};
41-
42-
public static IEnumerable<CodeInstruction> InstructionsToAdd(bool destroyed)
43-
{
44-
yield return new(Ldarg_0);
45-
yield return new(Ldloc_2);
46-
yield return new(Ldloc_0);
47-
yield return new(destroyed ? Ldc_I4_1 : Ldc_I4_0);
48-
yield return new(Call, Reflect.Method(() => PieceDeconstructed(default, default, default, default)));
49-
}
26+
private static TemporaryBuildData Temp => BuildingHandler.Main.Temp;
5027

5128
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
52-
instructions.Transform(BaseDeconstructInstructionPattern1, (label, instruction) =>
53-
{
54-
if (label.Equals("Insert1"))
55-
{
56-
return InstructionsToAdd(true);
57-
}
58-
return null;
59-
}).Transform(BaseDeconstructInstructionPattern2, (label, instruction) =>
60-
{
61-
if (label.Equals("Insert2"))
62-
{
63-
return InstructionsToAdd(false);
64-
}
65-
return null;
66-
});
29+
instructions.RewriteOnPattern(
30+
[
31+
Callvirt,
32+
Call,
33+
Ldloc_3,
34+
Reflect.Method((BaseGhost b) => b.ClearTargetBase()),
35+
[
36+
Ldarg_0,
37+
Ldloc_2,
38+
Ldloc_0,
39+
Ldc_I4_1,
40+
Reflect.Method(() => PieceDeconstructed(default, default, default, default))
41+
],
42+
])
43+
.RewriteOnPattern(
44+
[
45+
Ldloc_0,
46+
Reflect.Method((Base b) => b.FixCorridorLinks()),
47+
Ldloc_0,
48+
Reflect.Method((Base b) => b.RebuildGeometry()),
49+
[
50+
Ldarg_0,
51+
Ldloc_2,
52+
Ldloc_0,
53+
Ldc_I4_0,
54+
Reflect.Method(() => PieceDeconstructed(default, default, default, default))
55+
],
56+
]);
6757

6858
public static void Prefix(BaseDeconstructable __instance)
6959
{
@@ -180,9 +170,9 @@ public static void PieceDeconstructed(BaseDeconstructable baseDeconstructable, C
180170
BuildingHandler.Main.EnsureTracker(baseId).LocalOperations++;
181171
int operationId = BuildingHandler.Main.GetCurrentOperationIdOrDefault(baseId);
182172

183-
PieceDeconstructed pieceDeconstructed = Temp.NewWaterPark == null ?
184-
new PieceDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), operationId) :
185-
new WaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.NewWaterPark, Temp.MovedChildrenIds, Temp.Transfer, operationId);
173+
PieceDeconstructed pieceDeconstructed = Temp.NewWaterPark == null
174+
? new PieceDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), operationId)
175+
: new WaterParkDeconstructed(baseId, pieceId, cachedPieceIdentifier, ghostEntity, BuildEntitySpawner.GetBaseData(@base), Temp.NewWaterPark, Temp.MovedChildrenIds, Temp.Transfer, operationId);
186176
Log.Verbose($"Base is not empty, sending packet {pieceDeconstructed}");
187177

188178
Resolve<IPacketSender>().Send(pieceDeconstructed);

NitroxPatcher/Patches/Dynamic/BreakableResource_SpawnResourceFromPrefab_Patch.cs

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
24
using HarmonyLib;
35
using NitroxClient.GameLogic;
46
using NitroxClient.MonoBehaviours;
57
using NitroxModel.Helper;
68
using NitroxPatcher.PatternMatching;
7-
using System.Collections.Generic;
8-
using System.Reflection;
99
using UnityEngine;
1010
using static System.Reflection.Emit.OpCodes;
1111

1212
namespace NitroxPatcher.Patches.Dynamic;
1313

1414
/// <summary>
15-
/// Synchronizes entities that can be broken and that will drop material, such as limestones...
15+
/// Synchronizes entities that can be broken and that will drop material, such as limestones...
1616
/// </summary>
1717
public sealed partial class BreakableResource_SpawnResourceFromPrefab_Patch : NitroxPatch, IDynamicPatch
1818
{
1919
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(Reflect.Method(() => BreakableResource.SpawnResourceFromPrefab(default, default, default)));
2020

21-
private static readonly InstructionsPattern SpawnResFromPrefPattern = new()
22-
{
23-
{ Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))), "DropItemInstance" },
24-
Ldc_I4_0
25-
};
26-
27-
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
28-
{
29-
return instructions.InsertAfterMarker(SpawnResFromPrefPattern, "DropItemInstance", new CodeInstruction[]
30-
{
31-
new(Ldloc_1),
32-
new(Call, ((Action<GameObject>)Callback).Method)
33-
});
34-
}
21+
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions) =>
22+
instructions
23+
.RewriteOnPattern(
24+
[
25+
Reflect.Method((Rigidbody b) => b.AddForce(default(Vector3))),
26+
[
27+
Ldloc_1, // Dropped item GameObject
28+
((Action<GameObject>)Callback).Method
29+
],
30+
Ldc_I4_0
31+
]
32+
);
3533

3634
private static void Callback(GameObject __instance)
3735
{

0 commit comments

Comments
 (0)