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_limitteamsWithout 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.
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
| Method | Does |
|---|---|
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] });
});
| Option | Is |
|---|---|
forward | the speed forward, units a second; back when negative (250 runs with a knife) |
side | to the right; to the left when negative |
up | up, in water and on a ladder; down when negative |
buttons | the buttons held during the move: "attack", "attack2", "jump", "duck", "use", "reload", … (the type Button) |
angles | where it looks, [pitch, yaw, roll] or a Vector; where it looks now when left out |
msec | how 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.
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:
| Option | Default | Meaning |
|---|---|---|
color | [200, 100, 0] | red, green, blue, 0 to 255 |
x, y | -1, 0.35 | the position, 0 to 1 from the top left corner; -1 centres |
hold | 12 | seconds on screen |
effect | "fade" | "fade", "flicker" or "typewriter" (letter by letter) |
fadeIn, fadeOut | 0.1, 0.2 | seconds to appear and to fade away |
effectTime | 6 | seconds the "flicker" and "typewriter" effects take |
channel | -1 | the channel, 1 to 4; -1 takes a free one. A new message on a channel replaces the old one |
large | false | large 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 option | Default | Meaning |
|---|---|---|
color | [0, 0, 0, 255] | red, green, blue and opacity, 0 to 255 |
duration | 1 | seconds the fade takes |
hold | 0 | seconds the full colour holds |
direction | "in" | "in" goes from the colour to a clear view, "out" from a clear view to the colour |
stay | false | the colour stays until the next fade |
modulate | false | tints 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 to16(4by default);durationin seconds (1);frequency- jolts a second (5).statusIcon(sprite, state, color): a sprite of the game'ssprites/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. Theplayer.hideHudproperty (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 afterseconds;0hides it. WithstartPercentsince v0.2 it starts that full and fills the rest ofseconds: at50, 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, anumber, astring, a union of string literals ("red" | "blue", andtrue | 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
glowabove, or as an interface declared in the same file.player.glow.enabled = truewrites that member at once; assigning the whole object writes every member. Player[]is an array whosepushwrites back, like a flag list: to take a player out, assign a filtered array.includesandindexOffind 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 onePlayeralready 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_reloadkeeps 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.fieldis the member's dotted name,"glow.enabled".{ field: "glow.enabled" }hears that member;{ field: "glow" }hears each of them, with the whole object inevent.value. Assigning the whole object is one change for each member whose value differs. Player[]changes when the list does: apush, 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");
| Native | Does |
|---|---|
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".
Players and entities
Everything in the game world is an entity: players, weapons, grenades, doors, the bomb. In a plugin an entity is an object, and its data are properties you read and assign:
Effects
A temporary effect is something the game draws for a moment and forgets: a beam, a shockwave, an explosion, sparks, a light, blood. Each effect is a function of effects, its options are the effect's arguments, and the second argument says who sees it.