Implement module patch abstraction and patch loader (#250)

* Implement patch abstractions and patch loader using an interface

* remove patch loader

* rename patch class
This commit is contained in:
Cj
2025-05-11 15:52:14 -04:00
committed by GitHub
parent 1eb4d55a02
commit 0fda28526f
11 changed files with 348 additions and 32 deletions
@@ -0,0 +1,46 @@
using System.Reflection.Emit;
namespace SPTarkov.Reflection.CodeWrapper;
public class Code
{
public OpCode OpCode { get; }
public Type? CallerType { get; }
public object? OperandTarget { get; }
public Type[]? Parameters { get; }
public bool HasOperand { get; }
public Code(OpCode opCode)
{
OpCode = opCode;
HasOperand = false;
}
public Code(OpCode opCode, object operandTarget)
{
OpCode = opCode;
OperandTarget = operandTarget;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType)
{
OpCode = opCode;
CallerType = callerType;
HasOperand = true;
}
public Code(OpCode opCode, Type callerType, object operandTarget, Type[] parameters = null)
{
OpCode = opCode;
CallerType = callerType;
OperandTarget = operandTarget;
Parameters = parameters;
HasOperand = true;
}
public virtual Label? GetLabel()
{
return null;
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using HarmonyLib;
namespace SPTarkov.Reflection.CodeWrapper;
/// <summary>
/// Helper class to generate IL code for transpilers
/// </summary>
public class CodeGenerator
{
public static List<CodeInstruction> GenerateInstructions(List<Code> codes)
{
var list = new List<CodeInstruction>();
foreach (Code code in codes)
{
list.Add(ParseCode(code));
}
return list;
}
private static CodeInstruction ParseCode(Code code)
{
if (!code.HasOperand)
{
return new CodeInstruction(code.OpCode) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Ldfld || code.OpCode == OpCodes.Ldflda || code.OpCode == OpCodes.Stfld)
{
return new CodeInstruction(code.OpCode, AccessTools.Field(code.CallerType, code.OperandTarget as string)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Call || code.OpCode == OpCodes.Callvirt)
{
return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Box)
{
return new CodeInstruction(code.OpCode, code.CallerType) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Br || code.OpCode == OpCodes.Brfalse || code.OpCode == OpCodes.Brtrue || code.OpCode == OpCodes.Brtrue_S
|| code.OpCode == OpCodes.Brfalse_S || code.OpCode == OpCodes.Br_S)
{
return new CodeInstruction(code.OpCode, code.OperandTarget) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Ldftn)
{
return new CodeInstruction(code.OpCode, AccessTools.Method(code.CallerType, code.OperandTarget as string, code.Parameters)) { labels = GetLabelList(code) };
}
if (code.OpCode == OpCodes.Newobj)
{
return new CodeInstruction(code.OpCode, code.CallerType.GetConstructors().FirstOrDefault(x => x.GetParameters().Length == code.Parameters.Length)) { labels = GetLabelList(code) };
}
throw new ArgumentException($"Code with OpCode {code.OpCode.ToString()} is not supported.");
}
private static List<Label> GetLabelList(Code code)
{
if (code.GetLabel() == null)
{
return new List<Label>();
}
return [ (Label)code.GetLabel() ];
}
}
@@ -0,0 +1,33 @@
using System.Reflection.Emit;
namespace SPTarkov.Reflection.CodeWrapper;
public class CodeWithLabel : Code
{
public Label Label { get; }
public CodeWithLabel(OpCode opCode, Label label) : base(opCode)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, object operandTarget) : base(opCode, operandTarget)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType) : base(opCode, callerType)
{
Label = label;
}
public CodeWithLabel(OpCode opCode, Label label, Type callerType, object operandTarget, Type[] parameters = null) : base(opCode, callerType, operandTarget, parameters)
{
Label = label;
}
public override Label? GetLabel()
{
return Label;
}
}
@@ -0,0 +1,132 @@
using System.Reflection;
using HarmonyLib;
namespace SPTarkov.Reflection.Patching;
public abstract class AbstractPatch
{
private readonly Harmony _harmony;
private readonly List<HarmonyMethod> _prefixList;
private readonly List<HarmonyMethod> _postfixList;
private readonly List<HarmonyMethod> _transpilerList;
private readonly List<HarmonyMethod> _finalizerList;
private readonly List<HarmonyMethod> _ilManipulatorList;
/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Name</param>
protected AbstractPatch(string? name = null)
{
_harmony = new Harmony(name ?? GetType().Name);
_prefixList = GetPatchMethods(typeof(PatchPrefixAttribute));
_postfixList = GetPatchMethods(typeof(PatchPostfixAttribute));
_transpilerList = GetPatchMethods(typeof(PatchTranspilerAttribute));
_finalizerList = GetPatchMethods(typeof(PatchFinalizerAttribute));
_ilManipulatorList = GetPatchMethods(typeof(PatchIlManipulatorAttribute));
if (_prefixList.Count == 0
&& _postfixList.Count == 0
&& _transpilerList.Count == 0
&& _finalizerList.Count == 0
&& _ilManipulatorList.Count == 0)
{
throw new Exception($"{_harmony.Id}: At least one of the patch methods must be specified");
}
}
/// <summary>
/// Get original method
/// </summary>
/// <returns>Method</returns>
protected abstract MethodBase GetTargetMethod();
/// <summary>
/// Get HarmonyMethod from string
/// </summary>
/// <param name="attributeType">Attribute type</param>
/// <returns>Method</returns>
private List<HarmonyMethod> GetPatchMethods(Type attributeType)
{
var T = GetType();
var methods = new List<HarmonyMethod>();
foreach (var method in T.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public |
BindingFlags.DeclaredOnly))
{
if (method.GetCustomAttribute(attributeType) != null)
{
methods.Add(new HarmonyMethod(method));
}
}
return methods;
}
/// <summary>
/// Apply patch to target
/// </summary>
public void Enable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
foreach (var prefix in _prefixList)
{
_harmony.Patch(target, prefix: prefix);
}
foreach (var postfix in _postfixList)
{
_harmony.Patch(target, postfix: postfix);
}
foreach (var transpiler in _transpilerList)
{
_harmony.Patch(target, transpiler: transpiler);
}
foreach (var finalizer in _finalizerList)
{
_harmony.Patch(target, finalizer: finalizer);
}
foreach (var ilmanipulator in _ilManipulatorList)
{
_harmony.Patch(target, ilmanipulator: ilmanipulator);
}
}
catch (Exception ex)
{
throw new Exception($"{_harmony.Id}:", ex);
}
}
/// <summary>
/// Remove applied patch from target
/// </summary>
public void Disable()
{
var target = GetTargetMethod();
if (target == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
try
{
_harmony.Unpatch(target, HarmonyPatchType.All, _harmony.Id);
}
catch (Exception ex)
{
throw new Exception($"{_harmony.Id}:", ex);
}
}
}
@@ -0,0 +1,27 @@
namespace SPTarkov.Reflection.Patching
{
[AttributeUsage(AttributeTargets.Method)]
public class PatchPrefixAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchPostfixAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchTranspilerAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchFinalizerAttribute : Attribute
{
}
[AttributeUsage(AttributeTargets.Method)]
public class PatchIlManipulatorAttribute : Attribute
{
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Build.props"/>
<PropertyGroup>
<PackageId>SPTarkov.Reflection</PackageId>
<Authors>Single Player Tarkov</Authors>
<Description>Reflection library for the Single Player Tarkov server.</Description>
<Copyright>Copyright (c) Single Player Tarkov 2025</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://sp-tarkov.com</PackageProjectUrl>
<RepositoryUrl>https://github.com/sp-tarkov/server-csharp</RepositoryUrl>
<ImplicitUsings>enable</ImplicitUsings>
<OutputType>Library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE" Pack="true" Visible="false" PackagePath=""/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="HarmonyX" Version="2.14.0"/>
</ItemGroup>
</Project>
@@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\SPTarkov.DI\SPTarkov.DI.csproj"/>
<ProjectReference Include="..\SPTarkov.Reflection\SPTarkov.Reflection.csproj"/>
</ItemGroup>
<ItemGroup>