SwiftlyS2
Development

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.GameData to resolve named signatures/offsets from gamedata
  • Core.Memory to 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.

On this page