Game

Players: team, actions, lists

A Player is a player on the server. The event or the command that is about him gives him to you (event.player, player in a command's handler), and server.players lists everyone. His game data - gravity, origin, hideHud and the rest - are properties, described on Players and entities. This page is about what you do with a player: find him, move him to a team, give him weapons, show him text, and share his state with other plugins.

server
.
addCommand
("/rearm", rearm, {
access
: "slay" });
function rearm() { for (const
player
of
server
.
players
.
filter
(
other
=>
other
.
isAlive
&&
other
.
team
=== "CT")) {
player
.
removeAllItems
();
player
.
give
("weapon_knife");
player
.
give
("weapon_m4a1");
player
.
setAmmo
("weapon_m4a1", 90);
player
.
showHud
("Rearmed!", {
color
: [0, 200, 0],
hold
: 2 });
} }

Listing players since v0.2

for (const 
player
of
server
.
players
)
console
.
log
(
player
.
name
); // everyone
const
aliveCts
=
server
.
players
.
filter
(
player
=>
player
.
isAlive
&&
player
.
team
=== "CT");
const
people
=
server
.
players
.
filter
(
player
=> !
player
.
isBot
); // no bots
const
bots
=
server
.
players
.
filter
(
player
=>
player
.
isBot
);
const
dead
=
server
.
players
.
filter
(
player
=> !
player
.
isAlive
);

server.players is everyone connected, read anew each time you use it: keep the list in a variable only for as long as the players in it cannot change. It is an array, so a part of it is filter and one player is find, with the player's own properties - isAlive, isBot, team ("TERRORIST", "CT", "SPECTATOR" or "UNASSIGNED"). An HLTV proxy is never in the list.

Team

if (
player
.
team
== "CT")
console
.
log
("a counter-terrorist");
player
.
team
= "TERRORIST";

player.team is "TERRORIST", "CT", "SPECTATOR" or "UNASSIGNED" (the type Team), and it is right at once after a team change. Assigning it moves the player: his model changes to the new team's and the scoreboard shows the new team. The round's win conditions are not checked - if moving a player should end the round, end it yourself with game.endRound (Game events).

server
.
addEventListener
("putInServer", async (
event
) => {
await
sleep
(200, {
signal
:
event
.
player
.
signal
}); // the game is still putting him in the world
event
.
player
.
joinTeam
("CT"); // false if the game refused
});

player.joinTeam(team) joins a side the way the game joins a player who picks it in the team menu, his appearance picked for him. A player who has just arrived has no side and is not in the game yet; assigning player.team gives him a side, joinTeam also brings him into the game, so he can spawn. A living player sent to the spectators dies quietly: no death, no frag.

mp_limitteams
Without ReAPI the game refuses a side that mp_limitteams would stack, as it does in the team menu, and a dead player who has already changed sides this round; joinTeam returns false. With ReAPI the call skips these checks and puts the player on the side he was given.

Voice

player
.
muted
= true; // nobody hears him on the voice chat
if (
player
.
muted
)
console
.
log
(`${
player
.
name
} is muted`);
player
.
muted
= false;
player
.
heardByEveryone
= true; // both sides hear him
player
.
hearsEveryone
= true; // he hears both sides - a spectator, say

The game decides who hears whom by side and sv_alltalk; these three properties change it for one player, each on its own. muted works with sv_alltalk on and off and wins over heardByEveryone. Who hears whom among players on the sides can also be answered case by case, with the canPlayerHearPlayer game event (Game events).

His game's cvars

server
.
addCommand
("/fps", async ({
player
}) => {
const
limit
= await
player
.
queryCvar
("fps_max"); // "100", or null
print
(
player
, `fps_max ${
limit
?? "unknown"}`);
});

player.queryCvar(name) asks the player's game for one of its cvars and gives a promise of the answer: the value as text, or null when his game has no such cvar or will not tell. The answer is what his game says - a claim, which a cheat can change. A bot has no game to ask and answers null at once. When the player leaves before he answers, the promise is rejected with an "AbortError", and an async command of his stops quietly (async).

How his game proved who he is since v0.2

server
.
addCommand
("/game", ({
player
}) => {
if (
player
.
authType
=== "steam")
print
(
player
, "Your game is from Steam");
else
print
(
player
, `Your game: ${
player
.
authType
}, protocol ${
player
.
protocol
}`); // "revEmu2013, protocol 48"
});

Reunion is a ReHLDS plugin that lets players without Steam onto the server. With it, player.authType says how a player's game proved who he is (the type AuthType): "steam" for a game from Steam; for one without Steam the emulator it proved itself with - "steamEmu", "revEmu", "revEmu2013", "oldRevEmu", "sc2009", "avsmp", "sxei", "sse3"; "dproto"; "hltv" for an HLTV proxy. player.protocol is his game's network protocol: 48 for today's game, 47 for an old one Reunion lets in. player.authKey is the key his game proved itself with, what his SteamID (player.steamId) is made from: "STEAM_..." or "VALVE_...", as the server's Reunion settings say.

Without Reunion
Only Reunion knows these, and ReAPI reads them from it. On a server without Reunion or ReAPI, authType is "unknown", protocol is 0 and authKey is "".

Actions

player
.
give
("weapon_flashbang"); // or "item_kevlar", "item_assaultsuit", "item_thighpack"
player
.
setAmmo
("weapon_flashbang", 2); // the ammo he carries for it
player
.
getAmmo
("weapon_flashbang"); // 2
player
.
switchWeapon
("weapon_knife"); // false if he does not have one
player
.
removeAllItems
(); // every weapon
player
.
resetMaxSpeed
(); // his speed back to the weapon's
player
.
respawn
();
player
.
kill
(); // kill({ keepFrags: true }) - without the frag penalty
player
.
command
("stop"); // in his own console, as if he typed it
player
.
deaths
= 0; // the scoreboard updates too
MethodDoes
give(item)gives a weapon or an item; false if the game did not
setAmmo(weapon, amount)sets the ammo the player carries for a weapon he has, besides the clip
getAmmo(weapon)the ammo the player carries for a weapon he has; for a grenade, how many; 0 for a weapon he has not
switchWeapon(weapon)puts a weapon he carries into his hands; false if he has none
removeAllItems(removeSuit?)takes every weapon; the suit stays unless removeSuit is true
resetMaxSpeed()sets his speed back to what the weapon in his hands allows, after a slowdown
respawn()brings him back in the current round, at a spawn point the game picks
kill(options?)kills him as the kill command does; { keepFrags: true } costs no frag
command(text)runs a command in the player's console: his game runs it, not the server

Weapon names are the type WeaponName ("weapon_knife", "weapon_awp", …) and the items give takes are ItemName; the editor suggests them and refuses a typo. A weapon he carries has its name as weapon.classname (weapons): other.give(weapon.classname).

name, health, armor, frags and deaths are properties: read them, and assign health, armor, frags and deaths. A health of 0 or less kills the player.

The actions work on every server. With ReAPI they go through it; without it, through the standard AMX Mod X modules fun, cstrike and hamsandwich. The plugin is written the same either way.

Fake clients since v0.2

A fake client - a bot - is a player the server runs itself: it takes a slot, is in server.players and plays by the game's rules, but no game is behind it. server.addBot(name) adds one:

const 
bot
=
server
.
addBot
("Dummy"); // null when no slot is free
if (
bot
) {
bot
.
joinTeam
("CT");
bot
.
respawn
();
}

It comes as any player does - "connect" and "putInServer" fire for it - bot.isBot is true, and bot.kick() removes it, with "disconnected".

A bot has no mind of its own: it stands where it spawned and does nothing until a plugin moves it. bot.move(options) is one frame of its keys and mouse, so it is called every frame, in the "frame" event, for as long as the bot should move:

server
.
addEventListener
("frame", () => {
bot?.move({
forward
: 250,
buttons
: ["jump"],
angles
: [0, 90, 0] });
});
OptionIs
forwardthe speed forward, units a second; back when negative (250 runs with a knife)
sideto the right; to the left when negative
upup, in water and on a ladder; down when negative
buttonsthe buttons held during the move: "attack", "attack2", "jump", "duck", "use", "reload", … (the type Button)
angleswhere it looks, [pitch, yaw, roll] or a Vector; where it looks now when left out
msechow long the move lasts, 1 to 255 milliseconds; the frame's time when left out

move on a player who is not a bot throws an Error: a plugin moves only its bots.

A bot's AI is the plugin's
addBot gives a body, not a player: it does not walk, aim, shoot or buy by itself. Everything it does is what a plugin's move calls tell it, frame by frame. A move called less often - from a timer every 0.1 second - moves it only for its msec, not until the next call.

Is a module on the server

if (!
hasModule
("reapi"))
console
.
warn
("myplugin: the round's events need ReAPI");

hasModule is true when the server runs that AMX Mod X module: "reapi", "cstrike", "fun", "hamsandwich", "engine" or "fakemeta". Ask it before calling a native that only one module has.

HUD

player
.
showHud
("-35 HP");
player
.
showHud
("Round 3", {
color
: [255, 40, 40],
x
: 0.02,
y
: 0.88,
hold
: 2 });
player
.
showHud
("typed out", {
effect
: "typewriter",
channel
: 2 });
player
.
showHud
("TERRORISTS WIN", {
large
: true,
y
: 0.3,
hold
: 5 }); // large letters
server
.
showHud
("For everyone", {
y
: 0.6 });

A HUD message is text on the screen outside chat. Every option can be left out:

OptionDefaultMeaning
color[200, 100, 0]red, green, blue, 0 to 255
x, y-1, 0.35the position, 0 to 1 from the top left corner; -1 centres
hold12seconds on screen
effect"fade""fade", "flicker" or "typewriter" (letter by letter)
fadeIn, fadeOut0.1, 0.2seconds to appear and to fade away
effectTime6seconds the "flicker" and "typewriter" effects take
channel-1the channel, 1 to 4; -1 takes a free one. A new message on a channel replaces the old one
largefalselarge letters, for a result or a headline; they have no channels

A line that updates - HudLine

const 
countdown
= new
HudLine
();
function tick(
player
: Player,
left
: number) {
countdown
.
show
(
player
, `Bomb: ${
left
}`, {
color
: [255, 50, 50],
hold
: 1.1 });
}

A HudLine is one place on the screen: each show replaces what it showed before, where a plain showHud would take another channel and leave the last number fading under the new one. countdown.clear(player) removes it from one player's screen before its time is up, countdown.clearAll() from everyone's.

Screen effects - player.screen

player
.
screen
.
fade
({
color
: [200, 0, 0, 100],
duration
: 0.5 }); // a red flash that clears
player
.
screen
.
fade
({
color
: [0, 0, 0, 255],
duration
: 0.1,
hold
: 1,
stay
: true }); // black, and it stays
player
.
screen
.
fade
({
color
: [0, 0, 0, 0],
duration
: 0.2 }); // clear again
player
.
screen
.
shake
({
amplitude
: 8,
duration
: 1,
frequency
: 5 });
player
.
screen
.
statusIcon
("dmg_cold", "show", [0, 200, 255]); // "hide", "show", "flash"
player
.
screen
.
roundTime
(90); // the round clock, in seconds
player
.
screen
.
hideHud
(["money", "timer"]); // at once
player
.
screen
.
crosshair
(false);
player
.
screen
.
flashlight
(false); // the flashlight icon
player
.
screen
.
progressBar
(5); // the bar fills up in 5 seconds; 0 hides it
player
.
screen
.
progressBar
(4, {
startPercent
: 50 }); // half full, full in 2 seconds

What one player sees over the world. Times are in seconds. A message listener hears what player.screen sends, as it hears the game's.

fade optionDefaultMeaning
color[0, 0, 0, 255]red, green, blue and opacity, 0 to 255
duration1seconds the fade takes
hold0seconds the full colour holds
direction"in""in" goes from the colour to a clear view, "out" from a clear view to the colour
stayfalsethe colour stays until the next fade
modulatefalsetints the picture rather than painting over it

A fade or a hold longer than 16 seconds is cut to 16.

  • shake: amplitude - how far the view moves, up to 16 (4 by default); duration in seconds (1); frequency - jolts a second (5).
  • statusIcon(sprite, state, color): a sprite of the game's sprites/hud.txt ("dmg_cold", "buyzone", "c4", …), shown, flashing or hidden.
  • roundTime(seconds): the round clock at the top of his HUD.
  • hideHud(parts): hides those parts of his HUD at once. The player.hideHud property (Flags) does the same from the game's next frame.
  • crosshair(shown): the game's own crosshair.
  • flashlight(on, battery?): the flashlight icon and its battery, in percent.
  • progressBar(seconds, { startPercent }): the bar in the middle of his screen, full after seconds; 0 hides it. With startPercent since v0.2 it starts that full and fills the rest of seconds: at 50, in half of them.

Shared player fields

Plugins can add fields to Player for each other: one value per player, which every plugin on the server reads and writes - TypeScript and Pawn alike. It is how one plugin tells the others that a player is protected after his spawn, is frozen, or has a bonus.

The plugin that owns the fields declares them in a small file of its own, in a folder inside the plugins folder:

// plugins/myplugin/player.ts
import "@amxts/core";

declare module "@amxts/core" {
    interface Player {
        
spawnProtected
: boolean;
kills
: number;
bonus
: number;
tag
: string;
glow
: {
enabled
: true | false | "default";
seenBy
: Player[];
}; } }

Every plugin that uses them - the owner too - imports that file, and then the fields are properties of every player:

// plugins/shop.ts
import "~/myplugin/player";

function onKill(
killer
: Player,
victim
: Player) {
if (
victim
.spawnProtected) return;
killer
.kills =
killer
.kills + 1;
killer
.tag = "hunter";
killer
.glow.enabled = true; // one member, written at once
killer
.glow.seenBy.push(
victim
); // push writes back
killer
.glow = {
enabled
: "default",
seenBy
: [] };
}

A file in a folder of the plugins folder is not a plugin of its own: it is code plugins import, by its path after ~/.

  • Types. A field is a boolean, a number, a string, a union of string literals ("red" | "blue", and true | false | "default" - one value that is a yes, a no or a word), Player[], or an object of these. A field is never optional, and an object's members are not objects.
  • A field never written reads false, 0, "" or [], and a union its first string literal ("default" above).
  • An object field is written in place, as glow above, or as an interface declared in the same file. player.glow.enabled = true writes that member at once; assigning the whole object writes every member.
  • Player[] is an array whose push writes back, like a flag list: to take a player out, assign a filtered array. includes and indexOf find a player by his id. A player who leaves the server is taken out of everyone's lists.
  • Checked. A misspelt field (player.gost) or a value of the wrong type is an error in the editor and in the build. So is the same field declared with two types, and a field named like one Player already has (solid, health): give it another name.
  • Only what the plugin imports. The build knows the fields of the files a plugin imports; the editor knows every file in the plugins folder. A field the editor accepts and the build calls missing means a missing import "~/myplugin/player".
  • When they reset. A player's fields are cleared when he leaves, after every plugin's "disconnected" listener has run - a listener can still read them. Everyone's fields are cleared when the map changes. amxts_reload keeps them.

Reacting to a change

When a field changes on a player, the server raises "playerChange". Every write that changes a value - by any plugin, TypeScript or Pawn - reaches every plugin that listens for that field, at once. field in the third argument says which one:

import "~/myplugin/player";

server
.
addEventListener
("playerChange", (
event
) => {
print
(
event
.
player
,
event
.
value
? "You are protected" : "Your spawn protection is over");
}, {
field
: "spawnProtected" });

event.value and event.previous are the field's value after and before the change, of the field's type: a boolean here, the object for glow. A plugin that listens for spawnProtected is not called when kills changes. The field is written out, as the event's name is, and the build checks that it is a field the plugin imports.

Without field every field is heard, and event.field says which; the value is read from the player:

server
.
addEventListener
("playerChange", (
event
) => {
console
.
log
(`${
event
.
player
.
name
}: ${
event
.
field
} changed`);
});

A listener written as a function of its own takes the field's event:

server
.
addEventListener
("playerChange", onProtection, {
field
: "spawnProtected" });
function onProtection(
event
: PlayerChangeEvent<"spawnProtected">) {
event
.
player
.
renderMode
=
event
.
value
? "additive" : "normal";
}
  • An object field changes one member at a time, and event.field is the member's dotted name, "glow.enabled". { field: "glow.enabled" } hears that member; { field: "glow" } hears each of them, with the whole object in event.value. Assigning the whole object is one change for each member whose value differs.
  • Player[] changes when the list does: a push, or a list assigned.
  • Not a change: writing the value a field already has; a player leaving - his fields and his place in the others' lists go without an event, which is what "disconnected" is for; a map change.
  • At once. A listener runs inside the write, before the next line of the plugin that wrote. A listener that writes a field raises the event again, for that field.

From Pawn

A Pawn plugin reads and writes the same fields through amxts.inc. amxts build --deploy and amxts dev copy it into the server's addons/amxmodx/scripting/include. The key is the field's name as the TypeScript declares it:

#include <amxts>

if (amxts_get_player_data(id, "spawnProtected")) { ... }
amxts_set_player_data(id, "spawnProtected", 1);

new Float:bonus = amxts_get_player_data_float(id, "bonus");
amxts_set_player_data_float(id, "bonus", 1.5);

new tag[32];
amxts_get_player_data_string(id, "tag", tag, charsmax(tag));
amxts_set_player_data_string(id, "tag", "VIP");

// a member of an object field: its dotted key
new enabled[16];
amxts_get_player_data_string(id, "glow.enabled", enabled, charsmax(enabled));
amxts_set_player_data_string(id, "glow.enabled", "true");
NativeDoes
amxts_get_player_data(id, const key[])a number field, whole; a boolean is 1 or 0
amxts_set_player_data(id, const key[], value)sets a number or boolean field
Float:amxts_get_player_data_float(id, const key[])a number field as a Float
amxts_set_player_data_float(id, const key[], Float:value)sets a number field
amxts_get_player_data_string(id, const key[], out[], len)a text field; returns the bytes written, cut before a letter that does not fit
amxts_set_player_data_string(id, const key[], const value[])sets a text field

A field read as the other kind (a text field through amxts_get_player_data) reads 0 or "". A union of literals is text: true | false | "default" is "true", "false" or "default", and "" before anyone wrote it (TypeScript reads that as its first literal, "default"). Player[] is text too, the ids joined with commas: "3,5".