Native Functions and Hooks
SwiftlyS2 provides low-level memory APIs for resolving native addresses, calling unmanaged functions, and attaching runtime hooks.
Native hooks are advanced and unsafe by nature. Incorrect calling conventions, wrong signatures, or invalid register edits can crash the server.
Accessing Services
Native function workflows typically use two services:
Core.GameDatato resolve named signatures/offsets from gamedataCore.Memoryto resolve addresses, wrap unmanaged functions, and install hooks
public override void Load(bool hotReload)
{
var gameData = Core.GameData;
var memory = Core.Memory;
}Resolving Function Addresses
From GameData Signature Name
nint dispatchSpawnAddress = Core.GameData.GetSignature("CBaseEntity::DispatchSpawn");Safe variant:
if (!Core.GameData.TryGetSignature("CBaseEntity::DispatchSpawn", out nint dispatchSpawnAddress))
{
Console.WriteLine("Signature not found.");
return;
}From IDA-Style Pattern
nint? address = Core.Memory.GetAddressBySignature(
Library.Server,
"55 8B EC 83 EC 08 8B 45 08 5D C3"
);
if (address == null)
{
Console.WriteLine("Pattern not found.");
return;
}Declaring Native Delegate Types
Before wrapping an unmanaged function, declare a delegate matching the exact native signature and calling convention.
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nint DispatchSpawnDelegate(nint pEntity, nint pKeyValues);Wrapping Unmanaged Functions
By Address
IUnmanagedFunction<DispatchSpawnDelegate> func =
Core.Memory.GetUnmanagedFunctionByAddress<DispatchSpawnDelegate>(dispatchSpawnAddress);By VTable + Index
// pVTable must point to a valid virtual table.
IUnmanagedFunction<DispatchSpawnDelegate> func =
Core.Memory.GetUnmanagedFunctionByVTable<DispatchSpawnDelegate>(pVTable, 15);Calling Unmanaged Functions
Use Call to invoke the current function pointer (which may include active hooks).
nint result = func.Call(0x1337, 0xDEADBEEF);Use CallOriginal to bypass managed hook chains and call the original function directly.
nint resultOriginal = func.CallOriginal(0x1337, 0xDEADBEEF);Hooking Unmanaged Functions
AddHook installs a managed callback in the call chain and returns a hook Guid.
Guid hookId = func.AddHook(next =>
{
return (pEntity, pKeyValues) =>
{
Console.WriteLine("DispatchSpawn pre");
nint result = next()(pEntity, pKeyValues);
Console.WriteLine("DispatchSpawn post");
return result;
};
});If you need to skip the original function call, do not invoke next().
Unhook Function Hook
func.RemoveHook(hookId);Mid-Hooking Raw Addresses
Mid-hooks are installed on raw addresses via IUnmanagedMemory.
Get Unmanaged Memory Wrapper
nint targetAddress = Core.GameData.GetSignature("CBaseEntity::DispatchSpawn");
IUnmanagedMemory mem = Core.Memory.GetUnmanagedMemoryByAddress(targetAddress);Add Mid-Hook
Guid midHookId = mem.AddHook((ref MidHookContext ctx) =>
{
Console.WriteLine($"RBX={ctx.RBX}, RIP={ctx.RIP}");
// Example register edit
ctx.RAX = 0x1337;
});MidHookContext exposes:
- General registers:
RAX,RBX,RCX,RDX,RSI,RDI,RBP,RSP,R8-R15 - Special registers:
RFLAGS,RIP,TRAMPOLINE_RSP - SIMD registers:
XMM0-XMM15
Unhook Mid-Hook
mem.RemoveHook(midHookId);Hooks are automatically cleaned up on plugin unload, but explicitly removing them is still a good practice when you want deterministic cleanup.
Reference
See IGameDataService for named signatures and offsets.
See IMemoryService for memory APIs.
See IUnmanagedFunction<TDelegate> for function wrapping and hooking.
See IUnmanagedMemory, MidHookDelegate, and MidHookContext for mid-hooks.
See Library for library constants used in signature scans.