Game events
What happens in the game - a player takes damage, spawns, dies, buys a
weapon, throws a grenade, a round ends - are events on game. A listener
hears the event before the game acts, and can let it go on, change it, stop
it, or answer in the game's place:
// no damage between teammates
game.addEventListener("takeDamage", (event) => {
if (event.attacker.team == event.player.team) event.preventDefault();
});
// double damage from the knife
game.addEventListener("takeDamage", onTakeDamage);
function onTakeDamage(event: TakeDamageEvent) {
const weapon = event.attacker.activeItem;
if (weapon && weapon.kind == "knife") event.damage = event.damage * 2;
}
// the game asks, the plugin answers: players hear only their own team
game.addEventListener("canPlayerHearPlayer", (event) => event.listener.team == event.sender.team);
The game events come from the ReAPI and Ham Sandwich modules.
game.addEventListener works like
server.addEventListener (Plugin): the name picks
the type of event, the editor completes the names and marks a misspelt one,
and game.removeEventListener with the same function takes a listener off.
With its last listener off, the event stops coming to the plugin until a
listener is added again.
What a listener can do
| To | Write |
|---|---|
| let the game go on | return nothing |
| stop the game's action | event.preventDefault() |
| change what the game gets | assign a field: event.damage = 10 |
| answer in the game's place | return the answer: return true |
| stop the game and every listener after this one | event.stopImmediatePropagation() |
Changing a field changes what the game and the listeners after this one get; reading the field afterwards gives the new value.
Returning a value answers the game's question - "can this player hear
that one?", "how much fall damage?", "may he buy this?" - and the game does
not run its own code. The answer has the event's type (boolean, number,
Player, Entity): returning another type is an error in the editor and in
the build. An event whose game function answers nothing takes no answer.
preventDefault() stops the game's action without an answer: the
damage is not dealt, the default weapons are not given. Where the game
expects an answer, it gets 0 or false.
A function either returns a value on every path or on none. A listener that should answer only sometimes returns nothing and calls
event.preventDefault() where it has to stop the game - or always answers,
repeating the game's own rule for the other cases.After the game has acted
game.addEventListener("fallDamage", (event) => event.result / 2, true); // half the fall damage
game.addEventListener("playerSpawn", (event) => {
event.player.give("weapon_deagle");
}, true);
With true as the third argument - or { post: true }, which is the same -
the listener runs after the game's function. event.result is what the game
answered, and a returned value replaces it. Changing a field there changes
nothing: the game has already used it. To take such a listener off, pass
true to removeEventListener as well.
Touches
game.addEventListener("touch", onTouch, { toucher: "player", touched: "player" });
function onTouch(event: TouchEvent) {
const player = new Player(event.toucher.id);
const below = new Player(event.touched.id);
if (player.groundEntity == below.id) console.log(`${player.name} stands on ${below.name}`);
}
"touch" is two entities touching: event.toucher moved into
event.touched, both Entity. A touch happens every frame for every player
standing on something, so the third argument names the classes a listener is
about - toucher, touched, or both - and only those touches reach the
plugin: the server filters them before it. A class left out is any class.
event.preventDefault() blocks the touch. Two players touching are two
touches, one each way.
One class of entity
game.addEventListener("primaryAttack", onSlash, { classname: "weapon_knife" });
function onSlash(event: PrimaryAttackEvent) {
const knife = event.weapon;
knife.secondaryAttack(); // the right click instead; every plugin's listeners run on it
event.preventDefault(); // and not the left one
}
game.addEventListener("takeDamage", (event) => {
console.log(`${event.entity.classname} took ${event.damage}`);
}, { classname: "func_breakable" });
A weapon's attack, a door's use, an entity's think are events of one class of
entity: classname in the third argument names it, and only its entities
reach the plugin - the server filters them before it. { classname, post: true } listens after the game has acted.
An event about a player - takeDamage, killed, spawn - is a player's
without classname, and another class's with it: event.player is the
player, event.entity the entity of the class named. An event about a
weapon - canDeploy, itemPostFrame - is every weapon's without classname, one
class's with it. An event about one class of entity alone - primaryAttack,
deploy, think, use - needs classname. A field such an event has is
writable where the game takes it back, an entity or a vector too.
Ham Sandwich hooks a game function one class of entity at a time: a listener of an event about one class alone without
classname is not added, and the
console says so. It calls every plugin's hook whatever one answers, so
stopImmediatePropagation() on an event it delivers blocks the game's
function, as preventDefault() does, but not the other plugins' listeners.On a server without ReAPI - plain HLDS - Ham Sandwich delivers a player's event too, and a weapon's event without
classname is hooked on every
weapon's class: the listener is written the same and hears the same, and what
the note above says of Ham Sandwich's events holds for it. The events of
ReGameDLL's and ReHLDS's own come there from other hooks, some with less
(a server without ReAPI).The same functions are an entity's methods - the game runs them as when it does so itself, and every plugin's listeners hear it:
knife.deploy(); // drawn again: the model and the animation
knife.deploy({ hooks: false }); // the game's function alone, no listeners
player.jump();
door.use(player, player, "toggle", 0);
| Method | On |
|---|---|
spawn, activate, think, use, blocked, heal, killed, restart | any entity |
addFrags, addTeamScore, addItem, removeItem, giveAmmo, jump, duck | a player |
deploy, holster, drop, primaryAttack, secondaryAttack, reload, weaponIdle, retireWeapon, addToPlayer, attachToPlayer, extractAmmo, extractClipAmmo, resetEmptySound, sendWeaponAnim | a weapon |
What an event has
- Fields are the event's data, typed:
event.damageis anumber,event.damageTypeaDamage[](flags), a text astring. A field is named in words:event.trace,event.info,event.direction,event.start; the tooltip names the parameter as ReAPI has it. - Entities are objects: a player is a
Player, a weapon aWeapon, anything else anEntity.event.attacker.team,event.player.health. event.playeris the player the event is about: the one taking damage, spawning, jumping, being blinded.event.result, in a listener that runs after the game, is the game's answer.
Hover over an event's name for what it is, and over a field for what it
holds. The last line of each tooltip names it as ReAPI and Ham Sandwich do
(Pawn: `RG_CBasePlayer_TakeDamage`, `Ham_TakeDamage` ).
Events you will use first
| Event | When | Answer |
|---|---|---|
takeDamage | a player is about to take damage: player, attacker, inflictor, damage, damageType; to change how much, assign event.damage | number |
traceAttack | a shot hits a player, before the damage | - |
playerKilled¹ | a player was killed: victim, killer, inflictor | - |
playerSpawn¹ | a player spawned | - |
giveDefaultItems² | the game gives a spawned player his default weapons | - |
newRound¹ | a new round starts | - |
roundStart¹ | the freeze time at the round's start is over | - |
roundEnd¹ | the round is ending: winner, reason, delay | boolean |
buyWeapon¹ | a player buys a weapon: weapon | Entity: the weapon bought |
itemRestricted¹ | the game asks whether an item is forbidden to a player | boolean: true forbids |
canPlayerHearPlayer¹ | the game asks whether listener hears sender on voice chat | boolean |
fallDamage² | the game works out a fall's damage | number |
addMoney¹ | a player's money changes: amount, reason | - |
chooseTeam¹ | a player picks a team in the menu: choice | number |
throwHeGrenade¹ | a player throws an HE grenade | Entity |
playerBlind¹ | a flashbang blinds a player: fadeTime, fadeHold, alpha | - |
¹ Heard with less on a server without ReAPI, ² not heard there: a server without ReAPI.
The editor lists all of them, about 250.
An event is named after what the game does, in camelCase, whichever module
hears it: RG_CBasePlayer_TakeDamage and Ham_TakeDamage are both
"takeDamage", RG_CSGameRules_PlayerSpawn is "playerSpawn" (the round's
spawn of a player) and RG_CBasePlayer_Spawn "spawn", Ham_Weapon_PrimaryAttack
is "primaryAttack". The event's type is the name in PascalCase with Event:
TakeDamageEvent, PrimaryAttackEvent.
canPlayerHearPlayer is asked only while sv_alltalk is 0: with alltalk on
the game asks nobody. To silence a player whatever alltalk says, use
player.muted (Players).
Enums and flags — names
A field that holds one of a fixed set of values is a name, and a field of bit flags is an array of names:
game.addEventListener("roundEnd", (event) => {
if (event.reason == "targetSaved") console.log("the bomb site was saved");
if (event.winner == "CT") console.log("counter-terrorists win");
});
game.addEventListener("showVguiMenu", (event) => {
if (event.menu == "team") event.preventDefault();
});
game.addEventListener("sendDeathMessage", (event) => {
if (event.rarity.includes("headshot")) console.log("a headshot");
});
| Event | Field | Names |
|---|---|---|
roundEnd | winner | RoundWinner: "TERRORIST", "CT", "draw", "none" |
roundEnd | reason | RoundEndReason: "ctsWin", "terroristsWin", "targetSaved", "targetBomb", "bombDefused", "endDraw", "gameRestart", "none", … |
roundEnd | delay | seconds until the next round |
canSwitchTeam | team | Team |
chooseTeam | choice | TeamChoice: "TERRORIST", "CT", "VIP", "auto", "SPECTATOR" |
buyWeapon | weapon | WeaponKind, as weapon.kind names them: "ak47", "awp", … |
addMoney | reason | RewardReason: "roundBonus", "enemyKilled", … |
itemRestricted | restriction | ItemRestriction: "buying", "touched", "equipped" |
itemRestricted | item | ItemKind: a weapon by its kind ("awp", "hegrenade", …) or "kevlar", "assault", "defusekit", "nvg", … |
pain | lastHitGroup | HitGroup: "head", "chest", "stomach", "leftArm", … - as player.lastHitGroup names them |
setAnimation | animation | PlayerAnimation: "idle", "walk", "jump", "attack1", "reload", … |
addResource | resourceType | ResourceType: "sound", "model", "decal", "generic", "eventscript", … |
gameEvent | gameEvent | BotEvent: "playerDied", "bombPlanted", … |
showVguiMenu | menu | VguiMenu: "team", "classT", "classCT", "buy", … |
sendDeathMessage | flags | DeathMessageFlag[]: "position", "assistant", "killRarity" |
sendDeathMessage | rarity | KillRarity[]: "headshot", "noScope", "penetrated", "inAir", … |
Assigning a name changes the value the game gets:
event.reason = "terroristsWin". A value no name stands for - another plugin
or a mod passed it - reads as "unknown" ("none" for RoundWinner and
WeaponKind, "UNASSIGNED" for Team), and assigning "unknown" leaves the
field as the game passed it. Flags that have no name are kept when a flag
array is assigned.
A server without ReAPI since v0.2
ReGameDLL and ReHLDS have events of their own - a player's spawn into the
round, the round's start and end, money, purchases - which ReAPI delivers.
On a server without them - Valve's own HLDS with AMX Mod X - most of them
are heard the way a Pawn plugin hears them there: a new round through the
HLTV message, the end of the freeze time and of the round through the
game's log, a spawn and a death through Ham Sandwich, money through the
Money message, a purchase through cstrike. The plugin is written the same;
amxts picks the way once, when the server starts.
What such an event cannot give there is in the table below and in the
editor's tooltip of the event. A listener that asks for it anyway -
preventDefault(), an answer, a field written - is one line in the server
console, once for the event; a field the event cannot fill reads as 0,
"", null or the world.
| Event | On a server without ReAPI |
|---|---|
addMoney | Heard after the game has acted, so preventDefault() and changing a field do nothing; amount is how much his money moved since the game last sent it to him, and reason reads as "none" |
becomeBomber | Heard after the game has acted, so preventDefault() and changing a field do nothing; returning an answer does nothing |
becomeVip | Heard after the game has acted, so preventDefault() and changing a field do nothing |
buyAmmo | Heard through cstrike's CS_OnBuy: preventDefault() stops the purchase; weapon reads as the world, blinkMoney as true; returning an answer does nothing |
buyItem | Heard through cstrike's CS_OnBuy: preventDefault() stops the purchase; heard for the equipment menu's items; returning an answer does nothing |
buyWeapon | Heard through cstrike's CS_OnBuy: preventDefault() stops the purchase; event.result reads as null; returning an answer does nothing |
canPlayerHearPlayer | Asked as the game tells the engine who hears whom, with sv_alltalk on too, and for a player who muted the other: the answer overrides both |
changeLevel | preventDefault() does nothing |
changeName | Heard after the game has acted, so preventDefault() and changing a field do nothing; info reads as ""; returning an answer does nothing |
chooseAppearance | Heard from the player's command: preventDefault() stops it; a model the game picks itself is not heard |
chooseTeam | Heard from the player's command: preventDefault() stops it; returning an answer does nothing, and a team the game picks itself is not heard |
clientConnected | Heard after the game has acted, so preventDefault() and changing a field do nothing |
connectClient | Heard after the game has acted, so preventDefault() and changing a field do nothing; heard once the player is let in |
deathNotice | Heard as its death message is sent: preventDefault() does nothing, and inflictor reads as the world |
defuseBombEnd | Heard after the game has acted, so preventDefault() and changing a field do nothing; heard only when the bomb is defused, not when a defuse stops halfway |
defuseBombStart | Heard after the game has acted, so preventDefault() and changing a field do nothing |
disconnectClient | Heard after the game has acted, so preventDefault() and changing a field do nothing; crash reads as false, and reason is the one AMX Mod X was told |
dropPlayerItem | Heard from the player's drop command: preventDefault() stops it; event.result reads as null, and a drop the game makes itself is not heard |
explodeBomb | Heard after the game has acted, so preventDefault() and changing a field do nothing; trace and damageType read as 0 |
gameThink | Heard after the game has acted, so preventDefault() and changing a field do nothing |
gibSpawn | Heard after the game has acted, so preventDefault() and changing a field do nothing |
giveBomb | Heard after the game has acted, so preventDefault() and changing a field do nothing; returning an answer does nothing |
intermission | Heard after the game has acted, so preventDefault() and changing a field do nothing |
itemRestricted | Asked only for "buying", through cstrike's CS_OnBuyAttempt: answering true forbids the purchase, false lets the game go on |
mapReset | Heard after the game has acted, so preventDefault() and changing a field do nothing |
newRound | Heard as the round restarts - the listeners before the game when it announces the round, the ones after it once its players have respawned - but preventDefault() does nothing |
plantBomb | Heard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the bomb gets its model; returning an answer does nothing |
playerBlind | Heard after the game has acted, so preventDefault() and changing a field do nothing; inflictor and attacker read as the world, color as zero |
playerGotWeapon | Heard after the game has acted, so preventDefault() and changing a field do nothing |
playerKilled | Heard after the game has acted, so preventDefault() and changing a field do nothing |
playerSpawn | Heard after the game has acted, so preventDefault() and changing a field do nothing |
precacheFile | preventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing |
precacheModel | preventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing |
precacheSound | preventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing |
roundEnd | Heard after the game has acted, so preventDefault() and changing a field do nothing; returning an answer does nothing; delay is the original game's 5 seconds, 3 for "gameCommence", unless game.endRound set it |
roundStart | Heard after the game has acted, so preventDefault() and changing a field do nothing |
sendDeathMessage | preventDefault() stops the message; assister and inflictor read as the world, flags as empty, rarity has "headshot" alone; changing a field does nothing |
setModel | preventDefault() keeps the model off; changing modelName does nothing |
showVguiMenu | Heard as the menu is sent, to a player with VGUI menus on (not a bot): preventDefault() stops it; oldMenu reads as "", and changing a field does nothing |
startSound | Heard for the sounds the game plays through the engine's EmitSound: preventDefault() stops it; recipients reads as 0, and changing a field does nothing |
throwFlashbang | Heard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the grenade gets its model |
throwGrenade | Heard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the grenade gets its model, and eventIndex reads as 0 |
throwHeGrenade | Heard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the grenade gets its model, and eventIndex reads as 0 |
throwSmokeGrenade | Heard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the grenade gets its model, and eventIndex reads as 0 |
userInfoChange | Heard after the game has acted, so preventDefault() and changing a field do nothing; info reads as "" |
The order of a round is the game's: newRound before the players
respawn, their playerSpawn, newRound after the game once they are
alive, roundStart, roundEnd.
The rest of ReGameDLL's and ReHLDS's events - the game rules' questions (
fallDamage, canHaveItem, canRespawn, …), the
player's movement (jumpMovement, move, …), the engine's own (printf,
addResource, …), giveDefaultItems and the grenades' explosions - nothing
on plain HLDS hears. A listener for one is never called: the console says
so once, when it is added, the editor's tooltip of the event says so, and a
project with target: "hlds" does not build it. Listen for one that is
heard there - playerSpawn for giveDefaultItems, takeDamage for the
fall's damage - or ask hasModule("reapi") first.Ending the round
game.endRound({ winner: "TERRORIST" }); // the terrorists win; next round in 5 s
game.endRound({ winner: "CT", delay: 3 });
game.endRound({ winner: "draw", message: "Nobody won" });
game.endRound({ winner: "none", delay: 0.1, message: "" }); // a quiet restart
| Option | Meaning |
|---|---|
winner | "TERRORIST", "CT", "draw" or "none" - who scores, and the game's usual message and sound for that end |
delay | seconds until the next round; 5 by default |
message | the message in the middle of the screen, or a game text such as "#Terrorists_Win", instead of the usual one; "" for none |
sound | the radio sound, such as "terwin", instead of the usual one; "" for none |
dispatch | true tells the roundEnd listeners of every plugin, Pawn plugins too, as when the game ends a round itself. false by default: a roundEnd listener that ends the round would otherwise call itself |
Without ReAPI the round ends the same - the winner, the delay, the message and the sound - and
dispatch tells it as the game does there: the message
and the sound go to every plugin's message hooks, and the game's log lines
of a round's end to the roundEnd listeners and Pawn plugins' logevents.The game rules
if (game.isFreezeTime) console.log("the round has not begun");
game.isFreezeTime = false; // over, for the game's own checks
game.ctWins = 0; // the score, on the scoreboard at once
if (game.roundWinner == "CT") console.log("the counter-terrorists took the last round");
The game's own state - the freeze time, the score, the round's times, what
the map has - is a field of game, typed as a player's fields are: a number,
a boolean, a text, the round's winner as a name. The names are a player's
words for the member: m_bFreezePeriod is isFreezeTime, m_iNumCTWins is
ctWins, m_iRoundWinStatus is roundWinner, and the editor lists the rest
with their meaning and the member's name. A team's score written - ctWins, terroristWins -
is on the scoreboard at once, as when the game counts a round.
ReGameDLL adds fields to the game rules, which a server without it keeps elsewhere or not at all.
gameName is the game's name in the server list, and
writing it changes it there; timeLimit and gameStartTime
are counted from mp_timelimit and AMX Mod X's time left, which starts when
a restart is announced, and writing timeLimit sets mp_timelimit;
maxPlayers is the server's slots, and writing it does nothing.
teamBalanced, neededPlayers, skipShowMenu, escapeRatio and
updateInterval it does not have: they read 0 and write nothing, and the
console says so once.Async listeners
A listener may be async and await inside (async). What it
returns before its first await is its answer to the game. After an await
the game has moved on: the listener can still act - print, give an item -
but what it returns no longer reaches the game.
Limits
Plugin
A plugin is one .ts file in the project's plugins folder. It does its work at the top level of the file: it names itself, adds commands and listens to events. What it uses from the core - plugin, server, Player, print - needs no import line (auto-imports).
Forwards
A forward is an event one plugin raises and other plugins hear - TypeScript and Pawn alike. It is how a plugin tells the rest of the server that a round of its game mode started, that a player bought something, that a setting changed.