starlit

events

How to register callbacks, when each one runs, and the exact fields of every game event.

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).

  • fn is a function, or nil to remove the callback. Anything else raises function 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_paint of 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_level is called.
  • The table you receive is new for every call and has no metatable. Changing it affects nothing else.
  • Handlers cannot draw. draw functions raise an error outside on_paint. Save what you want to show and draw it in on_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.

functioncalledargument
events.on_paintevery framenone
events.on_unloadwhen the script is unloadednone
events.on_shotfor each finished ragebot shot at a hinted enemyshot table
events.on_player_hurta player took damagee table
events.on_player_deatha player diede table
events.on_weapon_firea player fired a weapone table
events.on_bullet_impacta bullet hit somethinge table
events.on_round_starta round beganempty table
events.on_round_enda round endede table
events.on_levelthe level changedempty 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)
fieldtypemeaning
useridintegerslot of the player who was hurt, 0 to 64
attackerintegerslot of the attacker, 0 for the world
healthintegerhealth left, 0 to 100000
armorintegerarmor left, 0 to 1000
damageintegerhealth damage, 0 to 100000
damage_armorintegerarmor damage, 0 to 1000
hitgroupintegerwhere 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)
fieldtypemeaning
useridintegerslot of the player who died, 0 to 64
attackerintegerslot of the killer, 0 for the world
assisterintegerslot of the assisting player, 0 for none
headshotbooleanthe kill was a headshot
penetratedintegerhow many surfaces the bullet passed through, 0 to 16
noscopebooleana scoped weapon fired without the scope
thrusmokebooleanthe 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)
fieldtypemeaning
useridintegerslot 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)
fieldtypemeaning
useridintegerslot of the player who fired the bullet
x, y, znumberwhere 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)
fieldtypemeaning
winnerintegerteam number of the winner, 0 to 255
reasonintegerthe 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)

previous: entity · next: draw