From 03d8ce2e5a9830ec21c5412061f03194fc68e43b Mon Sep 17 00:00:00 2001
From: Cj <161484149+CJ-SPT@users.noreply.github.com>
Date: Sun, 3 Aug 2025 02:15:16 -0400
Subject: [PATCH] Add patch cache
---
.../Patching/AbstractPatch.cs | 57 ++++++++++--
.../Patching/ModPatchCache.cs | 89 +++++++++++++++++++
2 files changed, 139 insertions(+), 7 deletions(-)
create mode 100644 Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs
diff --git a/Libraries/SPTarkov.Reflection/Patching/AbstractPatch.cs b/Libraries/SPTarkov.Reflection/Patching/AbstractPatch.cs
index c6fe34af..59e49188 100644
--- a/Libraries/SPTarkov.Reflection/Patching/AbstractPatch.cs
+++ b/Libraries/SPTarkov.Reflection/Patching/AbstractPatch.cs
@@ -3,8 +3,29 @@ using HarmonyLib;
namespace SPTarkov.Reflection.Patching;
+///
+/// Harmony patch wrapper class. See mod example 6.1 for usage.
+///
public abstract class AbstractPatch
{
+ ///
+ /// Method this patch targets
+ ///
+ public MethodBase? TargetMethod { get; private set; }
+
+ ///
+ /// Is this patch active?
+ ///
+ public bool IsActive { get; private set; }
+
+ ///
+ /// The harmony Id assigned to this patch, usually the name of the patch class.
+ ///
+ public string HarmonyId
+ {
+ get { return _harmony.Id; }
+ }
+
private readonly Harmony _harmony;
private readonly List _prefixList;
private readonly List _postfixList;
@@ -69,9 +90,15 @@ public abstract class AbstractPatch
///
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
///
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;
}
}
diff --git a/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs b/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs
new file mode 100644
index 00000000..c3dbda21
--- /dev/null
+++ b/Libraries/SPTarkov.Reflection/Patching/ModPatchCache.cs
@@ -0,0 +1,89 @@
+namespace SPTarkov.Reflection.Patching;
+
+///
+/// Cache of active patches for mod developers to use for compatibility reasons
+///
+public static class ModPatchCache
+{
+ private static readonly List _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?
+
+ ///
+ /// Get all active patches
+ ///
+ ///
+ /// List of active patches
+ ///
+ ///
+ /// This should never be called before PreSptLoad is completed, otherwise could be empty.
+ ///
+ public static IReadOnlyList 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();
+ }
+
+ ///
+ /// Get all actively patched target method names
+ ///
+ ///
+ /// List of fully quantified method names; including namespace, type and method name
+ ///
+ ///
+ /// This should never be called before PreSptLoad is completed, otherwise could be empty.
+ ///
+ public static List GetActivePatchedMethodNames()
+ {
+ var result = new List();
+
+ 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;
+ }
+
+ ///
+ /// Add a patch to the cache
+ ///
+ /// Patch to add to cache
+ ///
+ /// DO NOT PATCH THIS METHOD, IT IS INTERNAL FOR A REASON. YOU ARE ONLY HARMING OTHER MOD DEVELOPERS BY DOING SO.
+ ///
+ internal static void AddPatch(AbstractPatch patch)
+ {
+ _activePatches.Add(patch);
+ }
+
+ ///
+ /// Remove a patch from the cache
+ ///
+ /// Patch to remove
+ ///
+ /// True if patch was removed
+ ///
+ ///
+ /// DO NOT PATCH THIS METHOD, IT IS INTERNAL FOR A REASON. YOU ARE ONLY HARMING OTHER MOD DEVELOPERS BY DOING SO.
+ ///
+ internal static bool RemovePatch(AbstractPatch patch)
+ {
+ return _activePatches.Remove(patch);
+ }
+}