first commit

This commit is contained in:
2025-07-06 00:23:46 +02:00
commit 38f50c8819
1788 changed files with 112878 additions and 0 deletions

View 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;
});
}
}

View 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}'" : "")}";
}

View 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();
}

View 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);
}

View 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}")}";
}