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); + } +}