Merge branch 'develop' of https://github.com/sp-tarkov/server-csharp into develop

This commit is contained in:
Chomp
2025-08-03 09:35:09 +01:00
2 changed files with 139 additions and 7 deletions
@@ -3,8 +3,29 @@ using HarmonyLib;
namespace SPTarkov.Reflection.Patching;
/// <summary>
/// Harmony patch wrapper class. See mod example 6.1 for usage.
/// </summary>
public abstract class AbstractPatch
{
/// <summary>
/// Method this patch targets
/// </summary>
public MethodBase? TargetMethod { get; private set; }
/// <summary>
/// Is this patch active?
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// The harmony Id assigned to this patch, usually the name of the patch class.
/// </summary>
public string HarmonyId
{
get { return _harmony.Id; }
}
private readonly Harmony _harmony;
private readonly List<HarmonyMethod> _prefixList;
private readonly List<HarmonyMethod> _postfixList;
@@ -69,9 +90,15 @@ public abstract class AbstractPatch
/// </summary>
public void Enable()
{
var target = GetTargetMethod();
// We never want to have duplicated patches, prevent it.
if (IsActive)
{
return;
}
if (target == null)
TargetMethod = GetTargetMethod();
if (TargetMethod == null)
{
throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null");
}
@@ -80,28 +107,31 @@ public abstract class AbstractPatch
{
foreach (var prefix in _prefixList)
{
_harmony.Patch(target, prefix: prefix);
_harmony.Patch(TargetMethod, prefix: prefix);
}
foreach (var postfix in _postfixList)
{
_harmony.Patch(target, postfix: postfix);
_harmony.Patch(TargetMethod, postfix: postfix);
}
foreach (var transpiler in _transpilerList)
{
_harmony.Patch(target, transpiler: transpiler);
_harmony.Patch(TargetMethod, transpiler: transpiler);
}
foreach (var finalizer in _finalizerList)
{
_harmony.Patch(target, finalizer: finalizer);
_harmony.Patch(TargetMethod, finalizer: finalizer);
}
foreach (var ilmanipulator in _ilManipulatorList)
{
_harmony.Patch(target, ilmanipulator: ilmanipulator);
_harmony.Patch(TargetMethod, ilmanipulator: ilmanipulator);
}
ModPatchCache.AddPatch(this);
IsActive = true;
}
catch (Exception ex)
{
@@ -114,6 +144,12 @@ public abstract class AbstractPatch
/// </summary>
public void Disable()
{
// Nothing to disable
if (!IsActive)
{
return;
}
var target = GetTargetMethod();
if (target == null)
@@ -129,5 +165,12 @@ public abstract class AbstractPatch
{
throw new Exception($"{_harmony.Id}:", ex);
}
if (!ModPatchCache.RemovePatch(this))
{
throw new Exception($"{_harmony.Id}: Target patch not present in cache, a mod is likely externally altering it.");
}
IsActive = false;
}
}
@@ -0,0 +1,89 @@
namespace SPTarkov.Reflection.Patching;
/// <summary>
/// Cache of active patches for mod developers to use for compatibility reasons
/// </summary>
public static class ModPatchCache
{
private static readonly List<AbstractPatch> _activePatches = [];
// This class contains tighter access rules than we usually would implement in the project,
// the reason for this is so that the data is a true representation of what's happening with patches without any external interference.
// TODO: Mod GUID/Name associations to patches, need to think on that.
// Required parameter on AbstractPatch ctor maybe?
/// <summary>
/// Get all active patches
/// </summary>
/// <returns>
/// List of active patches
/// </returns>
/// <remarks>
/// This should never be called before PreSptLoad is completed, otherwise could be empty.
/// </remarks>
public static IReadOnlyList<AbstractPatch> GetActivePatches()
{
// We're not exposing _activePatches so it cant be altered outside of this class. Do NOT implement this as a property.
// Mod developers can still enable/disable these patches at will, this is fine, just don't allow external removal from the cache.
return _activePatches.AsReadOnly();
}
/// <summary>
/// Get all actively patched target method names
/// </summary>
/// <returns>
/// List of fully quantified method names; including namespace, type and method name
/// </returns>
/// <remarks>
/// This should never be called before PreSptLoad is completed, otherwise could be empty.
/// </remarks>
public static List<string> GetActivePatchedMethodNames()
{
var result = new List<string>();
foreach (var patch in _activePatches)
{
// Fullname includes namespace
var typeName = patch.TargetMethod?.DeclaringType?.FullName;
var methodName = patch.TargetMethod?.Name;
if (typeName != null && methodName != null)
{
result.Add($"{typeName}.{methodName}");
continue;
}
result.Add($"{patch.HarmonyId}: Type or method is null for this patch.");
}
return result;
}
/// <summary>
/// Add a patch to the cache
/// </summary>
/// <param name="patch">Patch to add to cache</param>
/// <remarks>
/// DO NOT PATCH THIS METHOD, IT IS INTERNAL FOR A REASON. YOU ARE ONLY HARMING OTHER MOD DEVELOPERS BY DOING SO.
/// </remarks>
internal static void AddPatch(AbstractPatch patch)
{
_activePatches.Add(patch);
}
/// <summary>
/// Remove a patch from the cache
/// </summary>
/// <param name="patch">Patch to remove</param>
/// <returns>
/// True if patch was removed
/// </returns>
/// <remarks>
/// DO NOT PATCH THIS METHOD, IT IS INTERNAL FOR A REASON. YOU ARE ONLY HARMING OTHER MOD DEVELOPERS BY DOING SO.
/// </remarks>
internal static bool RemovePatch(AbstractPatch patch)
{
return _activePatches.Remove(patch);
}
}