first commit
This commit is contained in:
111
NitroxPatcher/PatternMatching/ILExtensions.cs
Normal file
111
NitroxPatcher/PatternMatching/ILExtensions.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NitroxPatcher.PatternMatching;
|
||||
|
||||
internal static class ILExtensions
|
||||
{
|
||||
private static readonly Regex spaceRegex = new(Regex.Escape(" "));
|
||||
|
||||
/// <summary>
|
||||
/// Makes a string of an indexed list of instructions, line by line, formatted to have all opcodes and operand aligned in columns.
|
||||
/// </summary>
|
||||
public static string ToPrettyString(this IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
List<CodeInstruction> instructionList = [.. instructions];
|
||||
if (instructionList.Count == 0)
|
||||
{
|
||||
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;
|
||||
foreach (CodeInstruction instruction in instructionList)
|
||||
{
|
||||
int length = instruction.opcode.ToString().Length;
|
||||
if (length > opcodeMaxLength)
|
||||
{
|
||||
opcodeMaxLength = length;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder builder = new();
|
||||
for (int i = 0; i < instructionList.Count; i++)
|
||||
{
|
||||
CodeInstruction instruction = instructionList[i];
|
||||
// We add 2 so the text is more readable
|
||||
int spacesRequired = 2 + Math.Max(0, opcodeMaxLength - instruction.opcode.ToString().Length);
|
||||
string instructionToString = spaceRegex.Replace(instruction.ToString(), new string(' ', spacesRequired), 1);
|
||||
builder.AppendLine($"{i.ToString(format)} {instructionToString}");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="InstructionsPattern.expectedMatches" />,
|
||||
/// an exception is thrown.
|
||||
/// </summary>
|
||||
public static IEnumerable<CodeInstruction> Transform(this IEnumerable<CodeInstruction> instructions, InstructionsPattern pattern, Func<string, CodeInstruction, IEnumerable<CodeInstruction>> transform)
|
||||
{
|
||||
return pattern.ApplyTransform(instructions, transform);
|
||||
}
|
||||
|
||||
/// <inheritdoc
|
||||
/// cref="Transform(System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction},NitroxPatcher.PatternMatching.InstructionsPattern,System.Func{string,HarmonyLib.CodeInstruction,System.Collections.Generic.IEnumerable{HarmonyLib.CodeInstruction}})" />
|
||||
public static IEnumerable<CodeInstruction> Transform(this IEnumerable<CodeInstruction> instructions, InstructionsPattern pattern, Action<string, CodeInstruction> transform)
|
||||
{
|
||||
return pattern.ApplyTransform(instructions, (label, instruction) =>
|
||||
{
|
||||
transform(label, instruction);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the new instructions on every occurence of the marker, as defined by the pattern.
|
||||
/// </summary>
|
||||
/// <returns>Code with the additions.</returns>
|
||||
public static IEnumerable<CodeInstruction> InsertAfterMarker(this IEnumerable<CodeInstruction> instructions, InstructionsPattern pattern, string marker, CodeInstruction[] newInstructions)
|
||||
{
|
||||
return pattern.ApplyTransform(instructions, (m, _) =>
|
||||
{
|
||||
if (m.Equals(marker, StringComparison.Ordinal))
|
||||
{
|
||||
return newInstructions;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the <paramref name="instructionChange" /> action on each instruction matching the given marker, as defined by the
|
||||
/// pattern.
|
||||
/// </summary>
|
||||
public static IEnumerable<CodeInstruction> ChangeAtMarker(this IEnumerable<CodeInstruction> instructions, InstructionsPattern pattern, string marker, Action<CodeInstruction> instructionChange)
|
||||
{
|
||||
return pattern.ApplyTransform(instructions, (m, instruction) =>
|
||||
{
|
||||
if (m.Equals(marker, StringComparison.Ordinal))
|
||||
{
|
||||
instructionChange(instruction);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
80
NitroxPatcher/PatternMatching/InstructionPattern.cs
Normal file
80
NitroxPatcher/PatternMatching/InstructionPattern.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
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}'" : "")}";
|
||||
}
|
137
NitroxPatcher/PatternMatching/InstructionsPattern.cs
Normal file
137
NitroxPatcher/PatternMatching/InstructionsPattern.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HarmonyLib;
|
||||
|
||||
namespace NitroxPatcher.PatternMatching;
|
||||
|
||||
/// <remarks>
|
||||
/// Pattern matching is NOT thread safe.
|
||||
/// </remarks>
|
||||
public class InstructionsPattern : IEnumerable<InstructionPattern>
|
||||
{
|
||||
private readonly int expectedMatches;
|
||||
private readonly List<InstructionPattern> pattern = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IL pattern to apply transforms to IL. By default, a pattern expects to match exactly once.
|
||||
/// </summary>
|
||||
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<InstructionPattern> 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<CodeInstruction> ApplyTransform(IEnumerable<CodeInstruction> instructions, Func<string, CodeInstruction, IEnumerable<CodeInstruction>> transform)
|
||||
{
|
||||
CodeInstruction[] il = instructions as CodeInstruction[] ?? instructions.ToArray();
|
||||
Dictionary<int, IEnumerable<CodeInstruction>> 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<CodeInstruction> 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<CodeInstruction> 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();
|
||||
}
|
34
NitroxPatcher/PatternMatching/OpCodePattern.cs
Normal file
34
NitroxPatcher/PatternMatching/OpCodePattern.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
namespace NitroxPatcher.PatternMatching;
|
||||
|
||||
public readonly struct OpCodePattern
|
||||
{
|
||||
public bool Equals(OpCodePattern other) => Nullable.Equals(OpCode, other.OpCode);
|
||||
|
||||
public override bool Equals(object obj) => obj is OpCodePattern other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => OpCode.GetHashCode();
|
||||
|
||||
public OpCode? OpCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, similar opcodes will be matched as being the same.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Example for similar opcodes (call): call, callvirt and calli.
|
||||
/// </remarks>
|
||||
public bool WeakMatch { get; init; }
|
||||
|
||||
public bool IsAnyCall => WeakMatch && (OpCode == OpCodes.Call || OpCode == OpCodes.Callvirt || OpCode == OpCodes.Calli);
|
||||
|
||||
public static implicit operator OpCodePattern(OpCode opCode) => new() { OpCode = opCode };
|
||||
|
||||
public static bool operator ==(OpCodePattern pattern, OpCode opCode) => pattern.OpCode == opCode ||
|
||||
(pattern.IsAnyCall && (opCode == OpCodes.Call || opCode == OpCodes.Callvirt || opCode == OpCodes.Calli));
|
||||
public static bool operator ==(OpCode opCode, OpCodePattern pattern) => pattern == opCode;
|
||||
|
||||
public static bool operator !=(OpCodePattern pattern, OpCode opCode) => !(pattern == opCode);
|
||||
public static bool operator !=(OpCode opCode, OpCodePattern pattern) => !(opCode == pattern);
|
||||
}
|
59
NitroxPatcher/PatternMatching/OperandPattern.cs
Normal file
59
NitroxPatcher/PatternMatching/OperandPattern.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace NitroxPatcher.PatternMatching;
|
||||
|
||||
public readonly record struct OperandPattern(string DeclaringClassName, string MemberName, Type[] ArgumentTypes = null)
|
||||
{
|
||||
public bool IsAny => this == default;
|
||||
public bool IsAnyArguments => ArgumentTypes == null;
|
||||
|
||||
public static bool operator ==(OperandPattern pattern, [CanBeNull] object operand)
|
||||
{
|
||||
if (pattern.IsAny)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (operand is MemberInfo member)
|
||||
{
|
||||
if (!pattern.DeclaringClassName.Equals(member.DeclaringType?.FullName, StringComparison.OrdinalIgnoreCase) || !pattern.MemberName.Equals(member.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (member is MethodInfo method)
|
||||
{
|
||||
if (pattern.IsAnyArguments)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ParameterInfo[] parameters = method.GetParameters();
|
||||
if (parameters.Length == pattern.ArgumentTypes.Length)
|
||||
{
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
if (!parameters[i].ParameterType.IsAssignableFrom(pattern.ArgumentTypes[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool operator !=(OperandPattern pattern, object operand)
|
||||
{
|
||||
return !(pattern == operand);
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DeclaringClassName}{(MemberName == null ? "" : $".{MemberName}")}";
|
||||
}
|
Reference in New Issue
Block a user