Core

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

ToWrite
let the game go onreturn nothing
stop the game's actionevent.preventDefault()
change what the game getsassign a field: event.damage = 10
answer in the game's placereturn the answer: return true
stop the game and every listener after this oneevent.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 listener always answers or never does
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.

Events Ham Sandwich delivers
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.
A server without ReAPI
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);
MethodOn
spawn, activate, think, use, blocked, heal, killed, restartany entity
addFrags, addTeamScore, addItem, removeItem, giveAmmo, jump, ducka player
deploy, holster, drop, primaryAttack, secondaryAttack, reload, weaponIdle, retireWeapon, addToPlayer, attachToPlayer, extractAmmo, extractClipAmmo, resetEmptySound, sendWeaponAnima weapon

What an event has

  • Fields are the event's data, typed: event.damage is a number, event.damageType a Damage[] (flags), a text a string. 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 a Weapon, anything else an Entity. event.attacker.team, event.player.health.
  • event.player is 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

EventWhenAnswer
takeDamagea player is about to take damage: player, attacker, inflictor, damage, damageType; to change how much, assign event.damagenumber
traceAttacka 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, delayboolean
buyWeapon¹a player buys a weapon: weaponEntity: the weapon bought
itemRestricted¹the game asks whether an item is forbidden to a playerboolean: true forbids
canPlayerHearPlayer¹the game asks whether listener hears sender on voice chatboolean
fallDamage²the game works out a fall's damagenumber
addMoney¹a player's money changes: amount, reason-
chooseTeam¹a player picks a team in the menu: choicenumber
throwHeGrenade¹a player throws an HE grenadeEntity
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");
});
EventFieldNames
roundEndwinnerRoundWinner: "TERRORIST", "CT", "draw", "none"
roundEndreasonRoundEndReason: "ctsWin", "terroristsWin", "targetSaved", "targetBomb", "bombDefused", "endDraw", "gameRestart", "none", …
roundEnddelayseconds until the next round
canSwitchTeamteamTeam
chooseTeamchoiceTeamChoice: "TERRORIST", "CT", "VIP", "auto", "SPECTATOR"
buyWeaponweaponWeaponKind, as weapon.kind names them: "ak47", "awp", …
addMoneyreasonRewardReason: "roundBonus", "enemyKilled", …
itemRestrictedrestrictionItemRestriction: "buying", "touched", "equipped"
itemRestricteditemItemKind: a weapon by its kind ("awp", "hegrenade", …) or "kevlar", "assault", "defusekit", "nvg", …
painlastHitGroupHitGroup: "head", "chest", "stomach", "leftArm", … - as player.lastHitGroup names them
setAnimationanimationPlayerAnimation: "idle", "walk", "jump", "attack1", "reload", …
addResourceresourceTypeResourceType: "sound", "model", "decal", "generic", "eventscript", …
gameEventgameEventBotEvent: "playerDied", "bombPlanted", …
showVguiMenumenuVguiMenu: "team", "classT", "classCT", "buy", …
sendDeathMessageflagsDeathMessageFlag[]: "position", "assistant", "killRarity"
sendDeathMessagerarityKillRarity[]: "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.

EventOn a server without ReAPI
addMoneyHeard 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"
becomeBomberHeard after the game has acted, so preventDefault() and changing a field do nothing; returning an answer does nothing
becomeVipHeard after the game has acted, so preventDefault() and changing a field do nothing
buyAmmoHeard through cstrike's CS_OnBuy: preventDefault() stops the purchase; weapon reads as the world, blinkMoney as true; returning an answer does nothing
buyItemHeard through cstrike's CS_OnBuy: preventDefault() stops the purchase; heard for the equipment menu's items; returning an answer does nothing
buyWeaponHeard through cstrike's CS_OnBuy: preventDefault() stops the purchase; event.result reads as null; returning an answer does nothing
canPlayerHearPlayerAsked 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
changeLevelpreventDefault() does nothing
changeNameHeard after the game has acted, so preventDefault() and changing a field do nothing; info reads as ""; returning an answer does nothing
chooseAppearanceHeard from the player's command: preventDefault() stops it; a model the game picks itself is not heard
chooseTeamHeard from the player's command: preventDefault() stops it; returning an answer does nothing, and a team the game picks itself is not heard
clientConnectedHeard after the game has acted, so preventDefault() and changing a field do nothing
connectClientHeard after the game has acted, so preventDefault() and changing a field do nothing; heard once the player is let in
deathNoticeHeard as its death message is sent: preventDefault() does nothing, and inflictor reads as the world
defuseBombEndHeard 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
defuseBombStartHeard after the game has acted, so preventDefault() and changing a field do nothing
disconnectClientHeard 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
dropPlayerItemHeard from the player's drop command: preventDefault() stops it; event.result reads as null, and a drop the game makes itself is not heard
explodeBombHeard after the game has acted, so preventDefault() and changing a field do nothing; trace and damageType read as 0
gameThinkHeard after the game has acted, so preventDefault() and changing a field do nothing
gibSpawnHeard after the game has acted, so preventDefault() and changing a field do nothing
giveBombHeard after the game has acted, so preventDefault() and changing a field do nothing; returning an answer does nothing
intermissionHeard after the game has acted, so preventDefault() and changing a field do nothing
itemRestrictedAsked only for "buying", through cstrike's CS_OnBuyAttempt: answering true forbids the purchase, false lets the game go on
mapResetHeard after the game has acted, so preventDefault() and changing a field do nothing
newRoundHeard 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
plantBombHeard 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
playerBlindHeard after the game has acted, so preventDefault() and changing a field do nothing; inflictor and attacker read as the world, color as zero
playerGotWeaponHeard after the game has acted, so preventDefault() and changing a field do nothing
playerKilledHeard after the game has acted, so preventDefault() and changing a field do nothing
playerSpawnHeard after the game has acted, so preventDefault() and changing a field do nothing
precacheFilepreventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing
precacheModelpreventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing
precacheSoundpreventDefault() skips the precache, which answers 0; returning an answer or changing file does nothing
roundEndHeard 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
roundStartHeard after the game has acted, so preventDefault() and changing a field do nothing
sendDeathMessagepreventDefault() stops the message; assister and inflictor read as the world, flags as empty, rarity has "headshot" alone; changing a field does nothing
setModelpreventDefault() keeps the model off; changing modelName does nothing
showVguiMenuHeard 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
startSoundHeard for the sounds the game plays through the engine's EmitSound: preventDefault() stops it; recipients reads as 0, and changing a field does nothing
throwFlashbangHeard after the game has acted, so preventDefault() and changing a field do nothing; it is heard as the grenade gets its model
throwGrenadeHeard 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
throwHeGrenadeHeard 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
throwSmokeGrenadeHeard 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
userInfoChangeHeard 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.

Events nothing hears without ReAPI
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
OptionMeaning
winner"TERRORIST", "CT", "draw" or "none" - who scores, and the game's usual message and sound for that end
delayseconds until the next round; 5 by default
messagethe message in the middle of the screen, or a game text such as "#Terrorists_Win", instead of the usual one; "" for none
soundthe radio sound, such as "terwin", instead of the usual one; "" for none
dispatchtrue 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
A server without ReAPI
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.

A server without ReAPI
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

  • A vector field is written whole. event.direction = new Vector(0, 0, 1) turns traceAttack's shot; event.direction.z = 1 changes a copy and the game never sees it.
  • Messages to clients are heard through server.addMessageListener (Messages); temporary effects are effects.