Skip to content

Hooks

Hooks let Luau code run when the engine does something, not just when a player types a command. Two hooks exist today: "frame" (client) and "tick" (server).

callback(dt) runs inside Source’s HudUpdate every rendered frame, on the engine thread. dt is the real frametime in seconds, read live from the engine’s own gpGlobals.

-- count frames:
local frames = 0
usems.hook("frame", function(dt)
frames = frames + 1
end)
-- a 1-second heartbeat:
local next_beat = os.clock() + 1.0
usems.hook("frame", function(dt)
if os.clock() >= next_beat then
next_beat = os.clock() + 1.0
print(("beat dt=%.4f"):format(dt))
end
end)
-- a smooth value driven by frametime:
local charge = 0.0
usems.hook("frame", function(dt)
charge = math.min(charge + dt * 0.5, 1.0) -- fills in 2 seconds
if charge == 1.0 then print("charged!") end
end)

Multiple frame subscriptions (and multiple mods) all run, in load order.

callback() runs inside the server’s GameFrame, once per server tick — 66 times per second in single-player Portal, independent of frame rate. This is the simulation clock: the right place for gameplay logic that must run at a fixed rate.

-- a fixed-rate counter (66/s regardless of fps):
local ticks = 0
usems.hook("tick", function()
ticks = ticks + 1
end)
-- one print per simulated second:
local next_report = 66
usems.hook("tick", function()
if ticks == next_report then
next_report = next_report + 66
print("one simulated second elapsed")
end
end)
-- frame vs tick side by side — proves they're independent clocks:
local frames, ticks = 0, 0
usems.hook("frame", function() frames = frames + 1 end)
usems.hook("tick", function() ticks = ticks + 1 end)
local next_report = os.clock() + 1.0
usems.hook("frame", function()
if os.clock() >= next_report then
next_report = os.clock() + 1.0
print(("%d frames, %d ticks"):format(frames, ticks))
end
end)
-- sample log line at ~19 fps: counts: 95 frames, 337 ticks since start

usems.hook — like register_command/register_cvar — only works while your entry script is loading. Once it returns, registration freezes.

usems.hook("frame", function() end) -- ✓ during load
usems.register_command("later", function()
usems.hook("frame", function() end) -- ✗ raises: only available while
end) -- the mod loads
local armed = false
usems.register_command("arm", function() armed = true end, 0, "Arm the timer")
usems.hook("frame", function(dt)
if not armed then return end -- hook exists from load; logic gates here
-- ... timer logic ...
end)

A callback that raises is unsubscribed immediately with one warning line — the game never crashes, other hooks and mods keep running:

usems.hook("frame", function(dt)
error("boom") -- first (and only) invocation
end)
-- [USEMS] my-mod: frame hook removed after error: boom

Frame and tick callbacks share the per-mod execution budget (see Sandbox & Limits) — a spinning callback hits the 5-second wall and is killed the same way.

Hook Source surface Status
frame client HudUpdate live
tick server GameFrame via ServerGameDLL009 live
spawn, damage, death entity events via datamap reflection Phase 7 continues
usercmd, key input path later
console console line interception later