events
How to register callbacks, when each one runs, and the exact fields of every game event.
On this page
events is where a script says what it wants to hear about. You give it a function, and we call that function at the right moment.
Registering and delivery
Every registration function has the same shape: events.on_something(fn).
fnis a function, ornilto remove the callback. Anything else raisesfunction expected.- A script has one callback for each kind. Registering again replaces the old function.
- Registering from inside a callback is allowed.
Callbacks are called in this order inside a frame: surface drawing, button presses, on_change functions, ragebot shots (on_shot), game events, then on_paint. For the whole frame, see How scripts run.
Rules for game events:
- An event is delivered once, oldest first, before the
on_paintof the frame in which we receive it. - A script gets at most 32 event calls in a frame, within 1.5 ms. The rest waits for the next frame. An event that is five seconds old is dropped.
- A call has 50,000 instructions and 0.5 ms. A call that raises an error or overruns is a strike.
- A script that registers late, or loads late, sees only events from then on.
- When the level changes, the events of the old level that have not been delivered are thrown away, and
on_levelis called. - The table you receive is new for every call and has no metatable. Changing it affects nothing else.
- Handlers cannot draw.
drawfunctions raise an error outsideon_paint. Save what you want to show and draw it inon_paint.
The fields named userid, attacker and assister hold slots (1 to 64, or 0 for nobody or the world), not the game's user ids. They fit entity.get(slot) and game.players()[i].index.
| function | called | argument |
|---|---|---|
events.on_paint | every frame | none |
events.on_unload | when the script is unloaded | none |
events.on_shot | for each finished ragebot shot at a hinted enemy | shot table |
events.on_player_hurt | a player took damage | e table |
events.on_player_death | a player died | e table |
events.on_weapon_fire | a player fired a weapon | e table |
events.on_bullet_impact | a bullet hit something | e table |
events.on_round_start | a round began | empty table |
events.on_round_end | a round ended | e table |
events.on_level | the level changed | empty table |
Functions
events.on_paint
events.on_paint(fn) fn()
Called once for every rendered frame. Check game.in_game() if you only want to act in a match. It is the callback in which draw functions work, together with the on_draw function of a menu surface. If it raises an error, nothing it recorded is drawn that frame.
Budget 100,000 instructions and 1.5 ms.
events.on_paint(function()
if not game.in_game() then return end
draw.text(20, 20, "frame " .. time.frame(), 255, 255, 255)
end)
events.on_unload
events.on_unload(fn) fn()
Called once, when the script is unloaded by the user, by a reload, or when the client shuts down. It is not called for a script we stopped after two strikes. Errors in it are ignored.
Budget 20,000 instructions and 0.5 ms. Do not start work here. Use it to clear hints with resolver.clear if you want them gone at once. Without that, the host removes them on its own.
events.on_unload(function()
for slot = 1, 64 do resolver.clear(slot) end
end)
events.on_shot
events.on_shot(fn) fn(shot)
Called for every finished ragebot shot at an enemy that carried a resolver hint when the shot was fired. The table has the fields target, hit, hitgroup, damage, hint and age. They are described on the resolver page.
events.on_shot(function(shot)
log.info("shot at " .. shot.target .. ": " .. (shot.hit and "hit" or "miss"))
end)
events.on_player_hurt
events.on_player_hurt(fn) fn(e)
| field | type | meaning |
|---|---|---|
userid | integer | slot of the player who was hurt, 0 to 64 |
attacker | integer | slot of the attacker, 0 for the world |
health | integer | health left, 0 to 100000 |
armor | integer | armor left, 0 to 1000 |
damage | integer | health damage, 0 to 100000 |
damage_armor | integer | armor damage, 0 to 1000 |
hitgroup | integer | where the hit landed, see below |
hitgroup is 0 for generic damage, 1 head, 2 chest, 3 stomach, 4 left arm, 5 right arm, 6 left leg, 7 right leg, 8 neck, 10 gear. Any other number from the game becomes 0.
events.on_player_hurt(function(e)
local me = entity.local_player()
if me and e.attacker == me:index() and e.userid ~= e.attacker then
log.info("hit for " .. e.damage .. (e.hitgroup == 1 and " (head)" or ""))
end
end)
events.on_player_death
events.on_player_death(fn) fn(e)
| field | type | meaning |
|---|---|---|
userid | integer | slot of the player who died, 0 to 64 |
attacker | integer | slot of the killer, 0 for the world |
assister | integer | slot of the assisting player, 0 for none |
headshot | boolean | the kill was a headshot |
penetrated | integer | how many surfaces the bullet passed through, 0 to 16 |
noscope | boolean | a scoped weapon fired without the scope |
thrusmoke | boolean | the shot went through smoke |
events.on_player_death(function(e)
local killer, victim = entity.get(e.attacker), entity.get(e.userid)
if killer and victim then log.info(killer:name() .. " killed " .. victim:name()) end
end)
events.on_weapon_fire
events.on_weapon_fire(fn) fn(e)
| field | type | meaning |
|---|---|---|
userid | integer | slot of the player who fired, 0 to 64 |
We do not pass the weapon name, the spread seed or the recoil index.
local shots = 0
events.on_weapon_fire(function(e)
local me = entity.local_player()
if me and e.userid == me:index() then shots = shots + 1 end
end)
events.on_bullet_impact
events.on_bullet_impact(fn) fn(e)
| field | type | meaning |
|---|---|---|
userid | integer | slot of the player who fired the bullet |
x, y, z | number | where the bullet ended, in game units |
An impact whose coordinates are not finite or lie outside -1e6 to 1e6 is dropped and never reaches you.
events.on_bullet_impact(function(e)
local sx, sy = game.to_screen(e.x, e.y, e.z)
if sx then log.info(string.format("impact at %.0f, %.0f", sx, sy)) end
end)
events.on_round_start
events.on_round_start(fn) fn(e)
e is an empty table.
events.on_round_start(function() log.info("new round") end)
events.on_round_end
events.on_round_end(fn) fn(e)
| field | type | meaning |
|---|---|---|
winner | integer | team number of the winner, 0 to 255 |
reason | integer | the game's code for why the round ended, 0 to 255 |
events.on_round_end(function(e) log.info("round won by team " .. e.winner) end)
events.on_level
events.on_level(fn) fn(e)
e is an empty table.
Called when the level changes. Use it to forget everything you stored about the old level: slots, positions, handles, per-player state.
It can run twice for one change of level, once when the old level shuts down and once when the new one starts, if the two land in different frames. Write the handler so that running twice does no harm.
local seen = {}
events.on_level(function() seen = {} end)