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> <ItemGroup>
<ProjectReference Include="..\SPTarkov.DI\SPTarkov.DI.csproj"/> <ProjectReference Include="..\SPTarkov.DI\SPTarkov.DI.csproj"/>
<ProjectReference Include="..\SPTarkov.Reflection\SPTarkov.Reflection.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,29 +0,0 @@
using System.Reflection;
using HarmonyLib;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Modding;
public class HarmonyBootstrapper
{
public static void LoadAllPatches(List<Assembly> assemblies)
{
if (!ProgramStatics.MODS())
{
return;
}
var hamony = new Harmony("SPT");
foreach (var assembly in assemblies)
{
try
{
hamony.PatchAll(assembly);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
}
}
-2
View File
@@ -29,8 +29,6 @@ public static class Program
// validate and sort mods, this will also discard any mods that are invalid // validate and sort mods, this will also discard any mods that are invalid
var sortedLoadedMods = ValidateMods(mods); var sortedLoadedMods = ValidateMods(mods);
// for harmony, we use the original list, as some mods may only be bepinex patches only
HarmonyBootstrapper.LoadAllPatches(mods.SelectMany(asm => asm.Assemblies).ToList());
var diHandler = new DependencyInjectionHandler(builder.Services); var diHandler = new DependencyInjectionHandler(builder.Services);
// register SPT components // register SPT components
-1
View File
@@ -24,7 +24,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HarmonyX" Version="2.14.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1"/> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.1"/>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1"/>
+7
View File
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideoutCraftQuestIdGenerato
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{6884273A-72E9-4035-B5BE-EE101C69F5F5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{6884273A-72E9-4035-B5BE-EE101C69F5F5}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Reflection", "Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj", "{9073A593-A2F5-471E-9678-B896A7226FD4}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -67,6 +69,10 @@ Global
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU {6884273A-72E9-4035-B5BE-EE101C69F5F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU {6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.Build.0 = Release|Any CPU {6884273A-72E9-4035-B5BE-EE101C69F5F5}.Release|Any CPU.Build.0 = Release|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9073A593-A2F5-471E-9678-B896A7226FD4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -78,5 +84,6 @@ Global
{DB049C81-DEC0-490D-AC06-7AF4DC8C0571} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {DB049C81-DEC0-490D-AC06-7AF4DC8C0571} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {4B973AC0-0C60-4853-9AF7-7CB69127473E} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
{C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289} {C24B1FEB-F8AC-434E-998D-5DA4D1687295} = {587959C2-5AFA-4B77-B327-566610F9A289}
{9073A593-A2F5-471E-9678-B896A7226FD4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal