From 91f35f825a11e937e1fcbabc4b4a32873e0a8eaa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 11:31:18 +0000
Subject: [PATCH 1/6] Initial plan
From 0a4b0f597e3bfd100d76bdaf9c96d67e14b33306 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 6 Dec 2025 11:44:51 +0000
Subject: [PATCH 2/6] Add instance-specific hooking support to ScubaDiver and
RemoteNET
Co-authored-by: theXappy <10898152+theXappy@users.noreply.github.com>
---
src/RemoteNET/ManagedRemoteObject.cs | 25 +++
src/RemoteNET/RemoteHookingManager.cs | 62 ++++++-
src/RemoteNET/UnmanagedRemoteObject.cs | 25 +++
src/ScubaDiver.API/DiverCommunicator.cs | 5 +-
.../Callbacks/FunctionHookRequest.cs | 5 +
src/ScubaDiver/DiverBase.cs | 63 ++++++-
src/ScubaDiver/DotNetDiver.cs | 18 ++
src/ScubaDiver/Hooking/HookingCenter.cs | 167 ++++++++++++++++++
src/ScubaDiver/MsvcDiver.cs | 21 +++
src/ScubaDiver/RegisteredMethodHookInfo.cs | 5 +
.../project_net5/ScubaDiver_Net5.csproj | 1 +
.../ScubaDiver_Net6_x64.csproj | 1 +
.../ScubaDiver_Net6_x86.csproj | 1 +
.../project_netcore/ScubaDiver_NetCore.csproj | 1 +
.../ScubaDiver_NetFramework.csproj | 1 +
15 files changed, 389 insertions(+), 12 deletions(-)
create mode 100644 src/ScubaDiver/Hooking/HookingCenter.cs
diff --git a/src/RemoteNET/ManagedRemoteObject.cs b/src/RemoteNET/ManagedRemoteObject.cs
index 757029c6..54ed98aa 100644
--- a/src/RemoteNET/ManagedRemoteObject.cs
+++ b/src/RemoteNET/ManagedRemoteObject.cs
@@ -132,5 +132,30 @@ public override RemoteObject Cast(Type t)
{
throw new NotImplementedException("Not implemented in Managed context");
}
+
+ ///
+ /// Hooks a method on this specific instance.
+ /// This is a convenience method that calls app.HookingManager.HookMethod with this instance.
+ ///
+ /// The method to hook
+ /// Position of the hook (Prefix, Postfix, or Finalizer)
+ /// The callback to invoke when the method is called
+ /// True on success
+ public bool Hook(System.Reflection.MethodBase methodToHook, ScubaDiver.API.Hooking.HarmonyPatchPosition pos, RemoteNET.Common.DynamifiedHookCallback hookAction)
+ {
+ return _app.HookingManager.HookMethod(methodToHook, pos, hookAction, this);
+ }
+
+ ///
+ /// Patches a method on this specific instance with prefix, postfix, and/or finalizer hooks.
+ /// This is a convenience method that calls app.HookingManager.Patch with this instance.
+ ///
+ public void Patch(System.Reflection.MethodBase original,
+ RemoteNET.Common.DynamifiedHookCallback prefix = null,
+ RemoteNET.Common.DynamifiedHookCallback postfix = null,
+ RemoteNET.Common.DynamifiedHookCallback finalizer = null)
+ {
+ _app.HookingManager.Patch(original, prefix, postfix, finalizer, this);
+ }
}
}
diff --git a/src/RemoteNET/RemoteHookingManager.cs b/src/RemoteNET/RemoteHookingManager.cs
index b425680e..02cb4651 100644
--- a/src/RemoteNET/RemoteHookingManager.cs
+++ b/src/RemoteNET/RemoteHookingManager.cs
@@ -48,8 +48,15 @@ public RemoteHookingManager(RemoteApp app)
/// True on success, false otherwise
- public bool HookMethod(MethodBase methodToHook, HarmonyPatchPosition pos, DynamifiedHookCallback hookAction)
+ public bool HookMethod(MethodBase methodToHook, HarmonyPatchPosition pos, DynamifiedHookCallback hookAction, RemoteObject instance = null)
{
+ // Extract instance address if provided
+ ulong instanceAddress = 0;
+ if (instance != null)
+ {
+ instanceAddress = instance.RemoteToken;
+ }
+
// Wrapping the callback which uses `dynamic`s in a callback that handles `ObjectOrRemoteAddresses`
// and converts them to DROs
LocalHookCallback wrappedHook = WrapCallback(hookAction);
@@ -65,8 +72,9 @@ public bool HookMethod(MethodBase methodToHook, HarmonyPatchPosition pos, Dynami
{
throw new NotImplementedException("Shouldn't use same hook for 2 patches of the same method");
}
- if (methodHooks.Any(existingHook => existingHook.Value.Position == pos))
+ if (instanceAddress == 0 && methodHooks.Any(existingHook => existingHook.Value.Position == pos))
{
+ // Only prevent duplicate hooks if hooking all instances
throw new NotImplementedException("Can not set 2 hooks in the same position on a single target");
}
@@ -86,7 +94,46 @@ public bool HookMethod(MethodBase methodToHook, HarmonyPatchPosition pos, Dynami
methodToHook.GetParameters().Select(prm => prm.ParameterType.FullName).ToList();
}
- return _app.Communicator.HookMethod(methodToHook, pos, wrappedHook, parametersTypeFullNames);
+ return _app.Communicator.HookMethod(methodToHook, pos, wrappedHook, parametersTypeFullNames, instanceAddress);
+ }
+
+ ///
+ /// Hook a method on a specific instance using a dynamic object
+ ///
+ public bool HookMethod(MethodBase methodToHook, HarmonyPatchPosition pos, DynamifiedHookCallback hookAction, dynamic instance)
+ {
+ RemoteObject remoteObj = null;
+
+ // Try to extract RemoteObject from dynamic
+ if (instance != null)
+ {
+ // If it's already a RemoteObject, use it directly
+ if (instance is RemoteObject ro)
+ {
+ remoteObj = ro;
+ }
+ // Otherwise, try to get the underlying RemoteObject from DynamicRemoteObject
+ else
+ {
+ try
+ {
+ // DynamicRemoteObject has a GetRemoteObject method or RemoteObject property
+ var remoteObjProp = instance.GetType().GetProperty("RemoteObject",
+ System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ if (remoteObjProp != null)
+ {
+ remoteObj = remoteObjProp.GetValue(instance) as RemoteObject;
+ }
+ }
+ catch
+ {
+ throw new ArgumentException("Unable to extract RemoteObject from the provided dynamic instance. " +
+ "Please provide a RemoteObject or DynamicRemoteObject.");
+ }
+ }
+ }
+
+ return HookMethod(methodToHook, pos, hookAction, remoteObj);
}
private LocalHookCallback WrapCallback(DynamifiedHookCallback hookAction)
@@ -148,7 +195,8 @@ dynamic DecodeOora(ObjectOrRemoteAddress oora)
public void Patch(MethodBase original,
DynamifiedHookCallback prefix = null,
DynamifiedHookCallback postfix = null,
- DynamifiedHookCallback finalizer = null)
+ DynamifiedHookCallback finalizer = null,
+ RemoteObject instance = null)
{
if (prefix == null &&
postfix == null &&
@@ -159,15 +207,15 @@ public void Patch(MethodBase original,
if (prefix != null)
{
- HookMethod(original, HarmonyPatchPosition.Prefix, prefix);
+ HookMethod(original, HarmonyPatchPosition.Prefix, prefix, instance);
}
if (postfix != null)
{
- HookMethod(original, HarmonyPatchPosition.Postfix, postfix);
+ HookMethod(original, HarmonyPatchPosition.Postfix, postfix, instance);
}
if (finalizer != null)
{
- HookMethod(original, HarmonyPatchPosition.Finalizer, finalizer);
+ HookMethod(original, HarmonyPatchPosition.Finalizer, finalizer, instance);
}
}
diff --git a/src/RemoteNET/UnmanagedRemoteObject.cs b/src/RemoteNET/UnmanagedRemoteObject.cs
index d03028d2..e59e1a57 100644
--- a/src/RemoteNET/UnmanagedRemoteObject.cs
+++ b/src/RemoteNET/UnmanagedRemoteObject.cs
@@ -69,4 +69,29 @@ public override RemoteObject Cast(Type t)
RemoteObjectRef ror = new RemoteObjectRef(_ref.RemoteObjectInfo, dumpType, _ref.CreatingCommunicator);
return new UnmanagedRemoteObject(ror, _app);
}
+
+ ///
+ /// Hooks a method on this specific instance.
+ /// This is a convenience method that calls app.HookingManager.HookMethod with this instance.
+ ///
+ /// The method to hook
+ /// Position of the hook (Prefix, Postfix, or Finalizer)
+ /// The callback to invoke when the method is called
+ /// True on success
+ public bool Hook(System.Reflection.MethodBase methodToHook, ScubaDiver.API.Hooking.HarmonyPatchPosition pos, RemoteNET.Common.DynamifiedHookCallback hookAction)
+ {
+ return _app.HookingManager.HookMethod(methodToHook, pos, hookAction, this);
+ }
+
+ ///
+ /// Patches a method on this specific instance with prefix, postfix, and/or finalizer hooks.
+ /// This is a convenience method that calls app.HookingManager.Patch with this instance.
+ ///
+ public void Patch(System.Reflection.MethodBase original,
+ RemoteNET.Common.DynamifiedHookCallback prefix = null,
+ RemoteNET.Common.DynamifiedHookCallback postfix = null,
+ RemoteNET.Common.DynamifiedHookCallback finalizer = null)
+ {
+ _app.HookingManager.Patch(original, prefix, postfix, finalizer, this);
+ }
}
\ No newline at end of file
diff --git a/src/ScubaDiver.API/DiverCommunicator.cs b/src/ScubaDiver.API/DiverCommunicator.cs
index df244c8c..08446345 100644
--- a/src/ScubaDiver.API/DiverCommunicator.cs
+++ b/src/ScubaDiver.API/DiverCommunicator.cs
@@ -485,7 +485,7 @@ public void EventUnsubscribe(LocalEventCallback callback)
}
}
- public bool HookMethod(MethodBase methodBase, HarmonyPatchPosition pos, LocalHookCallback callback, List parametersTypeFullNames = null)
+ public bool HookMethod(MethodBase methodBase, HarmonyPatchPosition pos, LocalHookCallback callback, List parametersTypeFullNames = null, ulong instanceAddress = 0)
{
if (!_listener.IsOpen)
{
@@ -499,7 +499,8 @@ public bool HookMethod(MethodBase methodBase, HarmonyPatchPosition pos, LocalHoo
TypeFullName = methodBase.DeclaringType.FullName,
MethodName = methodBase.Name,
HookPosition = pos.ToString(),
- ParametersTypeFullNames = parametersTypeFullNames
+ ParametersTypeFullNames = parametersTypeFullNames,
+ InstanceAddress = instanceAddress
};
var requestJsonBody = JsonConvert.SerializeObject(req);
diff --git a/src/ScubaDiver.API/Interactions/Callbacks/FunctionHookRequest.cs b/src/ScubaDiver.API/Interactions/Callbacks/FunctionHookRequest.cs
index 70843eb1..9254a768 100644
--- a/src/ScubaDiver.API/Interactions/Callbacks/FunctionHookRequest.cs
+++ b/src/ScubaDiver.API/Interactions/Callbacks/FunctionHookRequest.cs
@@ -13,6 +13,11 @@ public class FunctionHookRequest
public string HookPosition { get; set; } // FFS: "Pre" or "Post"
+ ///
+ /// Optional: If specified, only hooks on this specific instance (address). 0 means hook all instances.
+ ///
+ public ulong InstanceAddress { get; set; }
+
}
}
\ No newline at end of file
diff --git a/src/ScubaDiver/DiverBase.cs b/src/ScubaDiver/DiverBase.cs
index 6bc0b1ed..15baa262 100644
--- a/src/ScubaDiver/DiverBase.cs
+++ b/src/ScubaDiver/DiverBase.cs
@@ -30,10 +30,12 @@ public abstract class DiverBase : IDisposable
protected bool _monitorEndpoints = true;
private int _nextAvailableCallbackToken;
protected readonly ConcurrentDictionary _remoteHooks;
+ protected readonly HookingCenter _hookingCenter;
public DiverBase(IRequestsListener listener)
{
_listener = listener;
+ _hookingCenter = new HookingCenter();
_responseBodyCreators = new Dictionary>()
{
// Divert maintenance
@@ -202,7 +204,22 @@ protected string MakeUnhookMethodResponse(ScubaDiverMessage arg)
if (_remoteHooks.TryRemove(token, out RegisteredManagedMethodHookInfo rmhi))
{
- rmhi.UnhookAction();
+ // Unregister from HookingCenter
+ if (!string.IsNullOrEmpty(rmhi.UniqueHookId))
+ {
+ _hookingCenter.UnregisterHook(rmhi.UniqueHookId, token);
+
+ // If this was the last hook for this method, unhook from Harmony
+ if (_hookingCenter.GetHookCount(rmhi.UniqueHookId) == 0)
+ {
+ rmhi.UnhookAction();
+ }
+ }
+ else
+ {
+ // Old-style hook without instance filtering
+ rmhi.UnhookAction();
+ }
return "{\"status\":\"OK\"}";
}
@@ -236,11 +253,25 @@ private string HookFunctionWrapper(FunctionHookRequest req, IPEndPoint endpoint)
int token = AssignCallbackToken();
Logger.Debug($"[DiverBase] Hook Method - Assigned Token: {token}");
Logger.Debug($"[DiverBase] Hook Method - endpoint: {endpoint}");
+ Logger.Debug($"[DiverBase] Hook Method - Instance Address: {req.InstanceAddress:X}");
+ // Generate unique hook ID for this method+position combination
+ string uniqueHookId = GenerateHookId(req);
// Preparing a proxy method that Harmony will invoke
HarmonyWrapper.HookCallback patchCallback = (object obj, object[] args, ref object retValue) =>
{
+ // Check if this invocation matches the instance filter (if any)
+ if (req.InstanceAddress != 0)
+ {
+ ulong instanceAddress = ResolveInstanceAddress(obj);
+ if (instanceAddress != req.InstanceAddress)
+ {
+ // Wrong instance, skip this callback
+ return true; // Call original
+ }
+ }
+
object[] parameters = new object[args.Length + 1];
parameters[0] = obj;
Array.Copy(args, 0, parameters, 1, args.Length);
@@ -262,9 +293,26 @@ private string HookFunctionWrapper(FunctionHookRequest req, IPEndPoint endpoint)
Logger.Debug($"[DiverBase] Hooking function {req.MethodName}...");
Action unhookAction;
+
+ // Check if this is the first hook for this method
+ bool isFirstHook = !_hookingCenter.HasHooks(uniqueHookId);
+
try
{
- unhookAction = HookFunction(req, patchCallback);
+ if (isFirstHook)
+ {
+ // First hook for this method - install the actual Harmony hook
+ unhookAction = HookFunction(req, patchCallback);
+ }
+ else
+ {
+ // Additional hook on same method - no need to install Harmony hook again
+ // The existing hook will dispatch to all registered callbacks
+ unhookAction = () => { }; // No-op unhook since we didn't install a new Harmony hook
+ }
+
+ // Register this callback with the hooking center
+ _hookingCenter.RegisterHook(uniqueHookId, req.InstanceAddress, patchCallback, token);
}
catch (Exception ex)
{
@@ -282,13 +330,22 @@ private string HookFunctionWrapper(FunctionHookRequest req, IPEndPoint endpoint)
{
Endpoint = endpoint,
RegisteredProxy = patchCallback,
- UnhookAction = unhookAction
+ UnhookAction = unhookAction,
+ UniqueHookId = uniqueHookId
};
EventRegistrationResults erResults = new() { Token = token };
return JsonConvert.SerializeObject(erResults);
}
+ private string GenerateHookId(FunctionHookRequest req)
+ {
+ string paramsList = string.Join(";", req.ParametersTypeFullNames ?? new List());
+ return $"{req.TypeFullName}:{paramsList}:{req.MethodName}:{req.HookPosition}";
+ }
+
+ protected abstract ulong ResolveInstanceAddress(object instance);
+
public abstract object ResolveHookReturnValue(ObjectOrRemoteAddress oora);
public int AssignCallbackToken() => Interlocked.Increment(ref _nextAvailableCallbackToken);
diff --git a/src/ScubaDiver/DotNetDiver.cs b/src/ScubaDiver/DotNetDiver.cs
index 3eeda4f8..dd9e4908 100644
--- a/src/ScubaDiver/DotNetDiver.cs
+++ b/src/ScubaDiver/DotNetDiver.cs
@@ -1587,5 +1587,23 @@ public override void Dispose()
_remoteHooks.Clear();
Logger.Debug("[DotNetDiver] Removed all event subscriptions & hooks");
}
+
+ protected override ulong ResolveInstanceAddress(object instance)
+ {
+ if (instance == null)
+ return 0;
+
+ // Try to get the pinning address if the object is pinned
+ if (_freezer.TryGetPinningAddress(instance, out ulong pinnedAddress))
+ {
+ return pinnedAddress;
+ }
+
+ // For unpinned objects, we can't reliably get their address
+ // as it can change due to GC. In this case, we use object reference equality
+ // which is handled by comparing object identity in the callback.
+ // Return a pseudo-address based on the object's identity hash code
+ return (ulong)System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(instance);
+ }
}
}
\ No newline at end of file
diff --git a/src/ScubaDiver/Hooking/HookingCenter.cs b/src/ScubaDiver/Hooking/HookingCenter.cs
new file mode 100644
index 00000000..9f617365
--- /dev/null
+++ b/src/ScubaDiver/Hooking/HookingCenter.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Concurrent;
+using System.Reflection;
+
+namespace ScubaDiver.Hooking
+{
+ ///
+ /// Centralized hooking manager that handles instance-specific hooks.
+ /// When a method is hooked with a specific instance, this center wraps callbacks
+ /// to filter invocations based on the instance address.
+ ///
+ public class HookingCenter
+ {
+ ///
+ /// Information about a registered hook
+ ///
+ public class HookRegistration
+ {
+ public ulong InstanceAddress { get; set; }
+ public HarmonyWrapper.HookCallback OriginalCallback { get; set; }
+ public int Token { get; set; }
+ }
+
+ ///
+ /// Key: Unique hook identifier (method + position)
+ /// Value: List of hook registrations for that method
+ ///
+ private readonly ConcurrentDictionary> _instanceHooks;
+
+ public HookingCenter()
+ {
+ _instanceHooks = new ConcurrentDictionary>();
+ }
+
+ ///
+ /// Registers a hook callback for a specific instance (or all instances if instanceAddress is 0)
+ ///
+ /// Unique identifier for the method hook (includes position)
+ /// Address of the instance to hook, or 0 for all instances
+ /// The callback to invoke
+ /// Token identifying this hook registration
+ public void RegisterHook(string uniqueHookId, ulong instanceAddress, HarmonyWrapper.HookCallback callback, int token)
+ {
+ var registrations = _instanceHooks.GetOrAdd(uniqueHookId, _ => new ConcurrentBag());
+ registrations.Add(new HookRegistration
+ {
+ InstanceAddress = instanceAddress,
+ OriginalCallback = callback,
+ Token = token
+ });
+ }
+
+ ///
+ /// Unregisters a hook callback by token
+ ///
+ /// Unique identifier for the method hook
+ /// Token identifying the hook registration to remove
+ public bool UnregisterHook(string uniqueHookId, int token)
+ {
+ if (_instanceHooks.TryGetValue(uniqueHookId, out var registrations))
+ {
+ // We can't efficiently remove from ConcurrentBag, so we'll mark it for filtering
+ // or recreate the bag without the item
+ var newBag = new ConcurrentBag();
+ bool found = false;
+ foreach (var reg in registrations)
+ {
+ if (reg.Token != token)
+ {
+ newBag.Add(reg);
+ }
+ else
+ {
+ found = true;
+ }
+ }
+
+ if (found)
+ {
+ if (newBag.IsEmpty)
+ {
+ _instanceHooks.TryRemove(uniqueHookId, out _);
+ }
+ else
+ {
+ _instanceHooks[uniqueHookId] = newBag;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Creates a unified callback that dispatches to instance-specific callbacks.
+ /// This wraps the individual callbacks to filter by instance.
+ ///
+ /// Unique identifier for the method hook
+ /// Function to resolve an object to its address
+ /// A callback that handles instance filtering
+ public HarmonyWrapper.HookCallback CreateUnifiedCallback(string uniqueHookId, Func