Skip to content

Game events

You listen, you never subscribe. aqua already shadows the engine’s event dispatcher (CGameEventManager::FireEventClientSide), so every networked event passes through it with AddListener called zero times. aqua.events only tells aqua which fields to copy out - the engine never learns a listener exists, and there is no script-reachable way to register one. That is deliberate: a registration sits in a game-owned, enumerable list, and one that outlives an unload is a call into unmapped memory.

Declare an event and the field keys you want off it. You must name the keys: the engine’s getters are by-name and there is no key enumeration we can walk, so aqua cannot guess what you came for.

Two forms, and you can mix them:

aqua.events.listen("player_hurt", { "userid", "dmg_health" }) -- array: read as int
aqua.events.listen("player_death", { weapon = "string", userid = "int" }) -- map: explicit type
  • Types are "int" (default), "float", "bool", "string" and "player". player returns the identity table described below; other types retain their existing number/string/boolean results.
  • A conflicting type for a key already requested by another script rejects your entire update and returns false. Its previous subscription is retained. int and player can coexist: each script receives its requested representation.
  • Unknown event names and unknown keys are not errors: you get 0 / "". Nothing validates against the game’s event schema.
  • Calling it again for the same event replaces your script’s keys and discards its pending deliveries from the old subscription. {} listens without fields.
  • aqua.events.unlisten(name) removes this script’s subscription to that event.
  • aqua.events.clear() removes this script’s event subscriptions. Other scripts and automatic shot callbacks retain their subscriptions.
  • Subscriptions and pending delivery are removed on stop, error or unload.

Resolve player fields directly, including the local player on the first event:

aqua.events.listen("player_hurt", {
userid = "player", attacker = "player", dmg_health = "int",
})
function on_event(name, data)
if name == "player_hurt" and data.userid and data.userid.isLocalPlayer then
print("Local damage: " .. data.dmg_health)
end
end

A player field always includes userid (the original integer, default 0) and resolved. When resolved it also includes index, handle, pawnHandle, steamId, name, team and isLocalPlayer. Match handle with the local or other get_players() entries when needed. Full handles include serial numbers; SteamIDs stay strings. Bots can resolve with steamId="0", so SteamID alone cannot distinguish bots.

Resolution uses the engine’s event-player getter, not an assumed userid/index formula. Identity is copied at event capture and remains the captured identity even if the player leaves before delivery. A world attacker, unknown player, new player awaiting its first snapshot or recycled identity produces resolved=false with no guessed player fields. Fields unavailable because the native getter is unresolved can be absent entirely. Existing array/int fields continue returning the raw number.

player_hurt - the fields the built-in hitmarker uses:

aqua.events.listen("player_hurt", {
"userid", "attacker", "health", "armor", "dmg_health", "dmg_armor", "hitgroup"
})

player_death:

aqua.events.listen("player_death", {
userid = "int", attacker = "int", assister = "int",
weapon = "string", headshot = "bool", noscope = "bool",
})

These two are examples, not a supported list - any event the server sends works the same way. Name it, name its keys, read them in on_event.

  • No uint64 getter bound. int, float, bool and string are; a 64-bit field (e.g. an xuid) will not read correctly through any of them.
  • userid="int" is the raw event number. Request userid="player" (and likewise attacker/assister) for resolved identity; do not infer an entity index.
  • The queue holds 256 events between frames. Past that, events are dropped and a WARNING naming the count goes to the log rather than the queue growing.
  • Delivery is once per frame, batched - not the instant the engine fires.