Hook Engine
The hook engine lets Rust (and therefore Luau) run code inside engine functions. The design rule: never modify engine code bytes — only vtable pointers and our own allocated memory.
Mechanism: vtable interposition
Section titled “Mechanism: vtable interposition”A Source interface object starts with a vptr to its class vtable. To hook virtual method N of object O:
1. allocate one RWX block: [ copy of vtable | stub area ]2. copy all vtable entries into the copy3. overwrite copy[N] with a stub: pushad ; save every register (incl. ecx=this) call <Rust target> ; cdecl, no args touched on the real stack popad ; restore jmp <original N> ; tail-jump; ret/args behave normally4. write copy's address into O's vptr fieldThe engine calls through the copy, our target observes, the original runs with identical register/stack state. Restore = write the original vptr back. The vptr write is an aligned 4-byte store — atomic on x86, so a call racing the swap sees either the old or new table, never a hybrid.
The counting profiler
Section titled “The counting profiler”Before naming any hook, we measure: usems_hook_profile [seconds]
installs a counting variant of the stub on every client-interface
vtable slot and reports call frequencies.
game console: usems_hook_profile 6
log: [USEMS] hook: slot 35 called 5970 times <- 3x/frame (dispatch query) [USEMS] hook: slot 55 called 1991 times <- frame-rate set [USEMS] hook: slot 11 called 1990 times <- frame-rate set ...Reading the output:
| Signature in counts | Meaning |
|---|---|
| N slots at identical frame-rate counts | per-frame path (HudUpdate family) |
| ~3× frame rate | multi-caller frame function (e.g. engine ReturnValue(int) dispatch) |
| ~60/s steady | think-tick cadence |
| exactly 1 | one-shot init/shutdown |
Finding HudUpdate — worked example
Section titled “Finding HudUpdate — worked example”The profiler found the frame-rate slots; disassembly named them. Slot 11
(client.dll RVA 0xf92f0):
mov eax, [0x104ab294] ; load a global pointer <-- candidate gpGlobalsfld dword [eax+0x10] ; read float at +0x10 <-- frametimelock (0xa0740) ; HudElement lockpush [ebp+8] ; use the bool argument <-- bActiveThe gpGlobals + frametime + bActive signature identified HudUpdate
and handed us gpGlobals’ address as a bonus — that’s where the frame
hook’s dt comes from.
Finding GameFrame — second worked example
Section titled “Finding GameFrame — second worked example”Same recipe on the server interface. ServerGameDLL009’s CreateFn is
mov eax, 0x10676e68; ret → singleton at server.dll RVA 0x676e68
(vtable 0x52da50). The profiler over 10 in-game seconds:
[USEMS] hook: slot 5 called 541 times <- 66/s: the tick rate[USEMS] hook: slot 25 called 541 times <- 66/s: query called by the same loop[USEMS] hook: slot 6 called 288 times <- ~35/s: half-rate path[USEMS] hook: slot 31 called 288 timesDisambiguating the two 66/s slots:
slot 25 (0x18a4c0): mov al, [0x10700cc5] / ret -- a one-byte getter (IsPaused) — called BY the tick loop, not the tick
slot 5 (0x189bd0): cmp byte [0x10700cc5], 0 ; paused check (calls slot 25's global!) call 0x10117280 ; game systems work test byte [eax+0x18], 1 ; state flag branching ret 4 ; exactly one bool arg -- GameFrame(bool simulating): the real per-tick workerSlot 5 it is — cadence and signature both check out.
Rust API
Section titled “Rust API”hooks::install_frame_hook() -> Result<(), String>
Section titled “hooks::install_frame_hook() -> Result<(), String>”Builds the vtable copy with the HudUpdate stub and swaps the vptr. Installs once at boot, after mods load. Refuses if a client profile run owns the swap, or on games without verified caps.
hooks::install_frame_hook()?;// [USEMS] hook: frame hook installed (client.dll slot 11)hooks::install_tick_hook() -> Result<(), String>
Section titled “hooks::install_tick_hook() -> Result<(), String>”Server twin: interposes GameFrame (slot 5) on the ServerGameDLL009
singleton. Independent of the frame hook — different object, different
vptr swap; only same-interface operations conflict.
hooks::install_tick_hook()?;// [USEMS] hook: tick hook installed (server.dll slot 5)hooks::profile_iface(iface, seconds) -> Result<(), String>
Section titled “hooks::profile_iface(iface, seconds) -> Result<(), String>”Installs counting stubs on all slots of Iface::Client or Iface::Server,
restores after seconds (1–60), then logs the frequency table. Blocked
only when the same interface’s hook owns its vptr swap.
hooks::profile_iface(hooks::Iface::Server, 10)?;hooks::stop_profile() -> bool
Section titled “hooks::stop_profile() -> bool”Manual restore if you need the vptr back before the timer fires.
hooks::iface_vtable(iface) -> Option<(object, vtable, slots)>
Section titled “hooks::iface_vtable(iface) -> Option<(object, vtable, slots)>”Resolves an interface object, its vtable, and the slot count (validated: every entry must point inside the interface’s own module). Also logs a one-time slot→RVA map for offline correlation.
hooks::frame_count() / tick_count() / frame_hook_active() / tick_hook_active()
Section titled “hooks::frame_count() / tick_count() / frame_hook_active() / tick_hook_active()”Diagnostics: total invocations and install state for both hooks.
The frame path
Section titled “The frame path”engine thread, every frame └─ HudUpdate(bActive) [client.dll, slot 11] └─ stub: pushad → call → popad → jmp original └─ hooks::frame_hook_target() [Rust, cdecl] ├─ FRAME_COUNT += 1 ├─ dt = *gpGlobals.frametime └─ mod_runtime::dispatch_frame_hooks(dt) └─ your Luau callback(dt) [budget-checked per mod]The tick path
Section titled “The tick path”server thread, 66/s └─ GameFrame(bool simulating) [server.dll, slot 5] └─ stub → hooks::tick_hook_target() ├─ TICK_COUNT += 1 └─ mod_runtime::dispatch_tick_hooks() └─ your Luau callback() [budget-checked per mod]Both Rust targets are deliberately tiny: an atomic increment, at most one pointer read, and the dispatch loop. Everything expensive (console I/O, error formatting) happens only on the failure path, once.
Bridge emitters (C++)
Section titled “Bridge emitters (C++)”Hand-assembled, in source_console_bridge.cpp:
usems_hook_emit_count_stub(slot, original, counter, out, capacity)
Section titled “usems_hook_emit_count_stub(slot, original, counter, out, capacity)”22 bytes: pushad; push slot; call counter; add esp,4; popad; jmp rel32.
Counting target is the Rust usems_hook_count(slot).
usems_hook_emit_call_stub(target, original, out, capacity)
Section titled “usems_hook_emit_call_stub(target, original, out, capacity)”13 bytes: pushad; call target; popad; jmp rel32 — the frame-hook shape
(no arguments marshalled; facts are read inside the Rust target).
Extending
Section titled “Extending”New virtual hooks are three steps: profile to find the slot, disassemble
to confirm the signature, then register a stub in the install routine.
Non-virtual internals (e.g. entity TakeDamage) need inline detours
(patch first bytes + trampoline) — planned with per-build pattern offsets
in caps, the one technique that does touch code bytes.