Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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)
{
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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<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,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<CodeInstruction> InstructionsToAdd(bool destroyed)
public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> 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<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;
});

public static void Prefix(BaseDeconstructable __instance)
{
BuildUtils.TryGetIdentifier(__instance, out cachedPieceIdentifier, null, __instance.face);
Expand Down Expand Up @@ -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<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