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:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,6 @@ public static class Program
|
||||
|
||||
// validate and sort mods, this will also discard any mods that are invalid
|
||||
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);
|
||||
// register SPT components
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HarmonyX" Version="2.14.0"/>
|
||||
<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" Version="9.0.1"/>
|
||||
|
||||
@@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HideoutCraftQuestIdGenerato
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{6884273A-72E9-4035-B5BE-EE101C69F5F5}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SPTarkov.Reflection", "Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj", "{9073A593-A2F5-471E-9678-B896A7226FD4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -78,5 +84,6 @@ Global
|
||||
{DB049C81-DEC0-490D-AC06-7AF4DC8C0571} = {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}
|
||||
{9073A593-A2F5-471E-9678-B896A7226FD4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Reference in New Issue
Block a user