Core

Plugin: commands, events, timers, chat, translations

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).

plugin
({
name
: "Welcome",
version
: "1.0.0",
author
: "you" });
server
.
addCommand
("/hp", ({
player
}) => showHp(
player
));
server
.
addEventListener
("putInServer", (
event
) => {
print
(
event
.
player
, "Welcome to the server!");
}); function showHp(
player
: Player) {
print
(
player
, `${
player
.
name
}, your HP: ${
player
.
health
}`);
}

Everything on this page is the core's API, @amxts/core, and is auto-imported.

Auto-imports

A plugin uses the core's API and what the project's modules give without importing it. The build finds the names a file uses and does not declare, and adds an import for each one it knows:

semiclip
.
rule
= (
player
,
target
) =>
player
.
team
==
target
.
team
;
server
.
addCommand
("/through", ({
player
}) => showThrough(
player
));
function showThrough(
player
: Player) {
const
names
=
semiclip
.
passesThrough
(
player
).
map
(
other
=>
other
.
name
);
print
(
player
, `You walk through: ${
names
.
join
(", ")}`);
}

server, Player and print are the core's; semiclip is what @amxts/resemiclip gives, in a project whose amxts.config.ts lists it. The imports go after the file's last line, so an error still points at the line you wrote.

  • What is auto-imported: everything @amxts/core exports for plugins - Player, Entity, server, game, print, lang, effects, Cvar, Forward, Storage, the timers, the events' types (TakeDamageEvent), the names of fields and flags (Team, HideHud) - and each module's name (menus, configs, semiclip). Not the natives and constants (@amxts/core/natives, @amxts/core/constants), not files (@amxts/core/fs), not your own files: a plugin that uses them says so with an import.
  • A name of your own wins. A variable, a parameter or a function named server is yours where it is visible; the auto-import is only for the places that do not see one.
  • An explicit import works too: import { Player } from "@amxts/core", import * as menus from "@amxts/menu-core". It is what a plugin writes for a module under another name, and for what is not auto-imported.
  • What an import names. The core's API by the package's name - @amxts/core, @amxts/core/natives, @amxts/core/constants, @amxts/core/fs, @amxts/core/os - a module by its package's name, and your own files by ~/, which is the plugins folder: import { twice } from "~/lib/math" is plugins/lib/math.ts, from any subfolder. ~/ names only your files: an import of the core's API by it does not build.
  • The editor knows the same names: npx amxts prepare - which dev, build, typecheck and npm install run first - writes .amxts/imports.d.ts, where they are globals, so completion, go to definition and the type check work without an import. It is written again when a command runs after the modules in amxts.config.ts change.
  • Turning it off: imports: { autoImport: false } in amxts.config.ts; every plugin then imports what it uses.
// amxts.config.ts
export default 
defineConfig
({
modules
: ["@amxts/menu-core"],
imports
: {
autoImport
: false },
});

A module says what it gives in its defineModule - itself as a namespace, imports: [{ from: "@amxts/menu-core", as: "menus" }], or one of its exports by its own name, [{ from: "@amxts/resemiclip", name: "semiclip" }] (creating a module). Two sources of one name stop the build, which names both.

Only what is used is built

A plugin gets an import only for what it uses, so a module no plugin uses is compiled into none of them. The build goes further: it builds a module's own plugin, and lists it in plugins.ini, only when some plugin of the project uses the module - through an auto-import or an explicit one, itself or through a file it imports. A module amxts.config.ts lists and no plugin uses is left out, and the build says so:

i module "resemiclip" is installed but no plugin uses it - left out

A module whose natives Pawn plugins call - menu-core's mc_*, config-core's cfg_* - is used by plugins the build does not see. pawn keeps it:

// amxts.config.ts
export default 
defineConfig
({
modules
: ["@amxts/config-core", "@amxts/menu-core"],
pawn
: ["@amxts/menu-core"], // Pawn plugins call mc_*: built even when no plugin uses it
});

What a module requires comes with it. In amxts dev a save that starts using a module builds the module with the plugin; a module no plugin uses any more leaves plugins.ini.

Plugin info

plugin
({
name
: "Welcome",
version
: "1.0.0",
author
: "you",
description
: "Greets players" });

The name, version and author the server lists for the plugin: amxts_plugins in the server console shows them. description is optional. Call plugin once, at the top of the file.

Commands since v0.2

interface KickArgs {
    
target
: Player;
reason
?: string;
}
server
.
addCommand
<KickArgs>("/kick <target> [reason]", ({
player
,
target
,
reason
}) => {
print
(0, `${
player
.
name
} kicks ${
target
.
name
}`);
target
.
kick
(
reason
?? "Kicked by admin");
}, {
access
: "kick",
description
: "Kick a player" });
server
.
addCommand
("/hp", ({
player
}) =>
print
(
player
, `${
player
.
health
} HP`));
server
.
addCommand
("myplugin_heal <amount>", ({
player
,
amount
}) =>
print
(
player
, `heal ${
amount
}`)); // no slash: a console command
server
.
addCommand
("say rules", ({
player
}) => showRules(
player
)); // a chat line without a slash

The first argument is the command's usage: its name, then its arguments, <name> one the player must type and [name] one he may leave out. The handler gets them by those names, with player, the player who typed the command.

The arguments' types are an interface, named and passed first, as addCommand<KickArgs> (a type written in place, <{ ... }>, is harder to read and cannot be used twice). Each field is how its word is read:

typethe worde.g.
stringas it is; the last argument takes the rest of the linereason in /kick bob being rude is "being rude"
numbera number/give 5
Player#userid, the whole name or a part of it, any case/kick bo, /kick #12
"on" | "off"one of those words/mode on

[name] in the usage is name?: in the interface, and <name> a field without ?. A name in the usage and not in the interface - or the other way round - does not build, and the error says which. Without the interface every argument is a string: ({ amount }) above is text.

When a word is not what the command takes - a number that is not one, a player nobody's name matches - the player gets the usage in chat, and the handler does not run. A part of a name several players have is answered with their names, so he can type more of it. A word too many is answered the same way, unless the last argument is text, which takes the rest of the line.

  • A name with / is a chat command. say /hp and say_team /hp both run it, and the line does not show in chat.
  • A name without / is a console command: the player types myplugin_heal 50 in his console.
  • "say <phrase>" runs when a player writes exactly that phrase in chat: "say rules" runs on rules, not on rules please. It takes no arguments.

Command names are not case-sensitive: /HP runs /hp.

access is the admin right a player needs, as users.ini gives it: "kick", "ban", "slay", "map", "cvar", "rcon", "levelA" … "levelH" and the rest - the editor suggests them. For a player without the right the command does nothing, and his chat line shows as a normal message. description is what amx_help lists.

A player's own rights are player.access, an array of the same names: player.access.includes("ban").

A help command

server.commands lists the commands the plugin added - each one's usage, description and access - which is what a /help prints:

server
.
addCommand
("/help", ({
player
}) => {
for (const
command
of
server
.
commands
) {
if (
command
.
access
== null ||
player
.
access
.
includes
(
command
.
access
))
print
(
player
, `${
command
.
usage
} - ${
command
.
description
}`);
} });

Server commands

interface ResetArgs {
    
what
?: "scores" | "all";
}
server
.
addServerCommand
<ResetArgs>("myplugin_reset [what]", ({
what
}) => {
console
.
log
(`reset: ${
what
?? "all"}`);
});

A command of the server console: typed there, sent over rcon, or run by another plugin with server_cmd. Its usage and arguments are read as a player's command's are; no player types it, so the handler has no player. What it prints with console.log goes back to whoever sent the command - over rcon, into the rcon reply.

Server events

server
.
addEventListener
("putInServer", (
event
) => {
print
(
event
.
player
, "Welcome!");
});
server
.
addEventListener
("disconnected", (
event
) => {
console
.
log
(`${
event
.
player
.
name
} left: ${
event
.reason}`);
});
server
.
addEventListener
("configsExecuted", loadSettings); // the server has run its configs

server.addEventListener works like addEventListener in the browser: the first argument is the event's name, the second a function the server calls every time the event happens. The event's name decides the type of event, so (event) => ... needs no annotation, and the editor completes the names and marks a misspelt one. The fields are typed: a player is a Player, a text a string, a yes/no a boolean (event.dropped).

EventWhen
"init"the plugin starts, once all plugins are loaded
"pluginsLoaded"every plugin has started: the moment to make a forward other plugins hear
"configsExecuted"the map has loaded and the server has run its configs - server.cfg, amxx.cfg, the map's own: the moment to read cvars
"precache"the map loads: the moment to precache models and sounds
"changeLevel"the map is about to change
"end"the plugin stops: the map changes or the server shuts down
"connect"a player connects, before he is in the game
"authorized"a player's Steam ID is known (event.steamId)
"putInServer"a player is in the game
"disconnected"a player has left (event.dropped, event.reason)
"playerChange"a field plugins added to Player changed (shared player fields)
"command"a player sent a console command
"impulse"a player sent an impulse: 100 is the flashlight, 201 the spray
"suicide"a player typed kill in the console
"frame"every server frame

In "connect", "authorized" and "putInServer" the player has not spawned yet, so event.player is a Client: his id, name, ip, steamId, access, team, muted and command, but no health or weapons. A Player has everything a Client has, so a function that needs only these takes a Client and works with both.

The editor lists every event; its tooltip names the AMX Mod X forward under it (client_putinserver). What happens to a player in the game - he thinks every frame, changes his info, touches something - is a game event: game.addEventListener("preThink", ...), "userInfoChange", "touch".

A listener written as a function of its own names the event's type, which is the forward's name in words: ClientPutinserverEvent, ClientDisconnectedEvent, ClientImpulseEvent.

server
.
addEventListener
("impulse", onImpulse);
function onImpulse(
event
: ClientImpulseEvent) {
if (
event
.
impulse
== 100)
print
(
event
.
player
, "Flashlight!");
}

server.removeEventListener("impulse", onImpulse) stops it: pass the same function that was added.

The name is written out as a string: server.addEventListener(name, ...) with the name in a variable is a build error, because the name picks the event's type.

A listener is a closure, as in JavaScript: it may use the variables of the function it is written in, and they live as long as it does.

server
.
addEventListener
("putInServer", (
event
) => {
const
player
=
event
.
player
;
setTimeout
(() =>
print
(
player
, `Glad to see you, ${
player
.
name
}!`), 2000);
});

Messages to clients since v0.2

server
.
addMessageListener
("death", (
event
) => {
if (
event
.
headshot
)
print
(0, `${
event
.
killer
?.
name
} - headshot - ${
event
.
victim
?.
name
}`);
});

Every message the server sends a client - a chat line, a HUD icon, the round clock, a death notice - is heard through server.addMessageListener, before it leaves, with its arguments as typed fields. Messages has the details and every message's name.

Game events

game
.
addEventListener
("newRound", () =>
console
.
log
("a new round"));
game
.
addEventListener
("takeDamage", (
event
) => {
if (
event
.
attacker
.
team
==
event
.
player
.
team
)
event
.
preventDefault
();
});

What happens in the game - damage, spawns, deaths, round ends, buying - are events on game, the way the server's are on server. A listener can stop the game's action or answer in its place. They come from the ReAPI and Ham Sandwich modules, and most are heard without ReAPI too; see Game events.

Timers

const 
handle
=
setTimeout
(() =>
print
(player, "Go!"), 2000); // once, in 2 s
clearTimeout
(
handle
); // stop it before it fires
function countdown(
player
: Player) {
let
left
= 3;
const
ticking
=
setInterval
(() => {
print
(
player
, `${
left
}`);
left
--;
if (
left
== 0)
clearInterval
(
ticking
);
}, 1000); }

As in the browser: setTimeout runs a function once after a delay in milliseconds, setInterval runs it every so many milliseconds, and each returns a handle that clearTimeout or clearInterval takes. The function may use and change the variables around it. A function without parameters goes as itself: setTimeout(startVote, 3000).

Inside an async function, await sleep(1000) waits the same way (async).

At least 0.1 second
Like every AMX Mod X timer, a timer waits at least 0.1 second and fires on a server frame: setTimeout(f, 10) runs after about 100 ms.

Time and randomness

Date
.
now
(); // milliseconds since 1970
new
Date
().
getHours
(); // the hour on the server's clock
new
Date
().
getUTCHours
(); // the hour, UTC
const
start
= performance.now(); // milliseconds, to the microsecond, for measuring
Math
.
random
(); // 0 <= x < 1, a new sequence every start
game
.
time
; // the game's clock: seconds since the map started

Date works as in JavaScript: new Date() is now, the UTC getters (getUTCHours, ...) and the local ones (getHours, getDate, getMonth, getFullYear, getTimezoneOffset, ...) - local being the server's time zone. A date becomes text as JavaScript writes it, in the server's time zone:

const 
now
= new
Date
();
now
.
toString
(); // "Tue Sep 29 2026 14:05:09 GMT+0300"
now
.
toLocaleString
(); // "9/29/2026, 2:05:09 PM"
now
.
toLocaleString
("ru-RU"); // "29.09.2026, 14:05:09"
now
.
toLocaleDateString
("de"); // "29.9.2026"
now
.
toLocaleTimeString
("en-GB"); // "14:05:09"

toLocaleString, toLocaleDateString and toLocaleTimeString take the language: en-US when none is named, en-GB, de, fr, ru and the languages written as ru (uk, be, kk); another is written as en-US.

game.time is the clock entity fields that hold a moment are on: grenade.damageTime = game.time + 1. The attack timers - a weapon's nextPrimaryAttack, a player's nextAttack - count from now instead: weapon.nextPrimaryAttack = 1 is a second away.

Chat and console

print
(
player
, "Only you see this");
print
(0, "Everyone sees this");
print
(
player
, "Health restored!", "center"); // "chat" (the default), "center", "console", "notify"
print
({
id
: 0,
variant
: "center" }, "Go!"); // everyone, in the middle of the screen
console
.
log
("to the server console");

print takes a player, a player's id, or 0 for everyone. The third argument is where the text shows: "chat", "center" (the middle of the screen), "console" (the player's console) or "notify" (the console too; the game shows it on screen only with developer 1).

console.log writes a line to the server console, after [amxts]; console.warn and console.error mark theirs as a warning and an error. Each takes a string, or an Error, which it writes with its stack (errors).

Colours

A colour in text is a tag: ! and a letter.

print
(
player
, "!g[Shop]!y You bought !rarmor");
TagColourChatMenu
!yyellow, the usual chat colouryesyes
!rredyesyes
!dgreyyesyes
!ggreenyesno
!bblueyesno
!tthe reader's team colouryesno
!wwhitenoyes
!Raligns the rest of the line rightnoyes

A letter means the same colour in chat and in menus.

Where tags are drawn
Each place draws the tags it has and drops the others from the text, and nothing warns: !w and !R are for menus, !g, !b and !t for chat. "center", "console" and "notify" have no colours: there the text shows as written, tags included. Tags are case-sensitive: !R is not red.
One team colour per message
Red, blue, grey and !t are drawn with a team's colour, and a chat message has one: the first of them in the line wins, and the others show in it. "!rRed !bBlue" shows both words red.

Errors since v0.2

A call that fails - a throw nobody catches, x! on a null, an index out of range, a stack overflow - ends that call, not the plugin: the next event, command or timer runs as usual. The server console names the error and where it happened.

function price(
item
: Item | null): number {
return
item
!.price;
} function onSelect(): number { return price(null); }

A plugin amxts dev compiles keeps a record of its calls: the console names each call that led to the error, with its file, line and column in your TypeScript, and shows the line itself under the first call of your own code:

[amxts] myplugin.aot: TypeError: Unexpected 'null' (not assigned or failed cast)
    at price (plugins/myplugin/shop.ts:26:9)
      26 |   return item!.price;
    at onSelect (plugins/myplugin/shop.ts:30:15)
    at timerFired (node_modules/@amxts/core/as/facade.ts:3772:2)

A plugin amxts build compiles - and one a server compiles from the .ts files in its plugins folder - keeps no such record, which would slow a busy plugin down (performance). The console names the error and, for a throw, an x! or a failed check, the place of the statement that failed; a stack overflow says its message alone:

[amxts] myplugin.aot: TypeError: Unexpected 'null' (not assigned or failed cast) (plugins/myplugin/shop.ts:26:9)

An error you catch has the same stack: error.stack is that text, from where the error was made, and console.error(error) writes it. In a plugin amxts build compiles, error.stack is the error's first line.

try {
    buy(
player
, item);
} catch (
error
) {
console
.
error
(
error
); // "Error: ..." and, in a dev build, the calls under it
}

The line under the first call shows when the server can read your .ts file, as it can with amxts dev. Calls inside AssemblyScript's own library

  • an array's methods, String - show their name without a place.
Menus for a server: menu-core
Real menus - a shop, an admin menu, a vote - are what the official module menu-core is for: menus in INI, YAML or JSON files the server owner edits without a build, items shown and enabled by conditions, %placeholders%, lists of players and of your own rows, menus Pawn plugins add items to, and the editor checking a menu file as it is typed. Add it with npx amxts module add menu-core and read Menus.

A quick menu in code

Menu is AMX Mod X's own menu as an object: a title, items, and show(player). It is part of the core and needs no module - for a menu or two a plugin builds in code.

interface ShopData {
    
category
: string;
} const
shop
= new
Menu
<ShopData>(({
data
}) => `!yShop: ${
data
.
category
}`);
shop
.
addItem
({
title
: "Armor - !y$1000",
enabled
: ({
player
}) =>
player
.
armor
< 100,
onSelect
: ({
player
}) => {
player
.
armor
= 100;
}, });
shop
.
addItem
({
title
: ({
player
}) => `Heal !d(${
player
.
health
} HP)`,
visible
: ({
player
}) =>
player
.
isAlive
,
enabled
: ({
player
}) =>
player
.
health
< 100,
onSelect
: ({
player
,
menu
,
data
}) => {
player
.
health
= 100;
menu
.
show
(
player
,
data
); // stays open
}, });
server
.
addCommand
("/shop", ({
player
}) =>
shop
.
show
(
player
, {
category
: "all" }));
  • An item is an object: its title, visible - whether it is shown, enabled - whether it can be chosen, and onSelect, what choosing it does. visible and enabled are true when left out; a hidden item takes no place, one that cannot be chosen is drawn grey and does nothing.
  • Each function gets the context: player, the one the menu is shown to; menu, the menu itself; data, what show was given. Data, the type argument, says what that is - a menu shown without data leaves it out.
  • A title, visible and enabled may be a function, or a plain value: they are worked out at every show, for that player.
  • After a choice the menu closes. onSelect that shows it again keeps it open.
  • Pages, Back, More and Exit are AMX Mod X's: seven items to a page, the rest on the next ones.
  • Colour tags are the ones of chat and menus: !y yellow, !r red, !d grey, !w white, !R to the right edge.

Make a menu once, at the top level of the file, and show it as often as needed. The options are the second argument:

const 
vote
= new
Menu
("!yNext map?", {
perPage
: 0, // every item on one page, no Back and More (up to 10)
exit
: false, // no Exit item
numberColor
: "!y", // the item numbers in yellow, not red
backText
: "Back",
nextText
: "Next",
exitText
: "Close",
});

AMX Mod X's menus

For full control, AMX Mod X's own menu natives are there whole: menu_create, menu_additem, menu_setprop, menu_display and the rest, with their constants (MPROP_*, MEXIT_*) - see calling natives directly. A handler is given by the name of a public, which publicFor from @amxts/core/kit makes for a function. Old-style menus - text and the keys that answer - are showMenu of the same kit.

Translations

Text in the reader's language comes from a dictionary: a file in data/lang, a section per language and a line per key.

[en]
MYPLUGIN_WELCOME = Welcome, ^4%s^1!
MYPLUGIN_ON = \d[\yOn\d]

[ru]
MYPLUGIN_WELCOME = Добро пожаловать, ^4%s^1!
MYPLUGIN_ON = \d[\yВкл\d]
lang
.
load
("myplugin"); // data/lang/myplugin.txt
server
.
addEventListener
("putInServer", (
event
) => {
print
(
event
.
player
,
lang
.
translate
(
event
.
player
, "MYPLUGIN_WELCOME", [
event
.
player
.
name
]));
});

lang.translate(player, key, args) gives the key's line in the player's language - his lang setinfo - and null for the player gives the server's (amx_language). A language the dictionary lacks falls back to the server's. %s, %d and %f (%.1f, %02d) are filled from args, which are strings: a number goes in as text, `${seconds}`. A placeholder with no argument left for it stays as written, and a key no dictionary has comes back as the key itself.

511 bytes a line
AMX Mod X reads a dictionary line up to 511 bytes - about 255 Cyrillic letters; the rest of a longer line is cut.

player.language is that language, for what a dictionary does not hold - a sound, a picture: player.language == "ru". It is his lang setinfo, or the server's language when he has none or amx_client_languages is 0.

A dictionary writes colours as AMX Mod X does - \y in a menu, ^4 in chat - and lang.translate gives them back as tags: \y is !y, \r !r, \d !d, \w !w, \R !R, ^1 !y, ^3 !t, ^4 !g. So one line works in a menu and in chat; each place drops the tags it cannot draw (!w in chat). ^n is a line break.

HUD messages

player
.
showHud
("Round 3", {
color
: [255, 40, 40],
y
: 0.3,
hold
: 2 });
server
.
showHud
("For everyone");

Text on the screen outside chat. The options, the large director letters and HudLine are on Players.

Sounds and precaching

server
.
precache
("myplugin/hit.wav");
server
.
precache
("models/myplugin/box.mdl");
function hit(
player
: Player) {
player
.
emitSound
("myplugin/hit.wav"); // everyone near hears it
player
.
emitSound
("myplugin/hit.wav", {
volume
: 0.5,
pitch
: 120,
channel
: "voice" });
player
.
playSound
("vox/one.wav"); // he alone, as the radio
}

A model, a sprite or a sound the game uses is precached while the map loads: server.precache(path) at the top level of the file precaches it then, and in the "precache" event at once. It also makes players download the file. A sound is written as the game plays it, under sound/ ("myplugin/hit.wav"); a model or a sprite with its folder ("models/myplugin/box.mdl"). It returns the file as a Resource, which an effect takes for a sprite or a model.

entity.emitSound(sample, options) plays a sound from an entity - a player too - heard by everyone near and fading with distance. Every option has a default:

OptionDefaultMeaning
channel"auto"the entity's channel: "auto", "weapon", "voice", "item", "body", "stream", "static"; a new sound on a channel cuts the one playing there, "auto" never cuts
volume10 to 1
attenuation0.8how fast it fades with distance: 0 is heard across the map, 2 only close by
pitch100in percent: 50 is an octave lower, up to 255

player.playSound(sample) plays a sound to one player, heard as it is wherever he stands, as the radio is.

Precaching is possible only while the map loads, and a model the game is given without it stops the server. A plugin reloaded in the middle of a map gets no "precache" event; its top level runs again, and a Resource asked for there finds the index the map gave the file. A file no plugin precached while the map loaded waits for the next map.

The server

server
.
map
; // "de_dust2"
server
.
maxPlayers
; // 32
server
.
configsDir
; // "addons/amxmodx/configs"
server
.
dataDir
; // "addons/amxmodx/data"
server
.
command
("changelevel de_nuke"); // runs in the server console

server.command runs a line in the server console as if it were typed there. To run one in a player's console: player.command("say hi").