Getting started
amxts runs Counter-Strike 1.6 plugins written in TypeScript inside AMX Mod X,
next to your Pawn plugins. A plugin is an ordinary .ts file: players are
objects, events have types, and the editor knows every field the game has.
This page shows a project, a first plugin, how to reload it while you work,
and what is different from the TypeScript you already know.
A project
A new project starts with one command:
npm create amxts@latest
pnpm create amxts
yarn create amxts
bun create amxts
It asks for the package manager, the folder, the modules, oxlint and oxfmt, git
and the server's folder, writes the project with a first plugin and its test,
installs everything and says what to run next. npx amxts init in a folder
is the same command; every question is also a flag
(the amxts command).
A project is a folder with its plugins and one config file, amxts.config.ts,
which lists the modules the project uses and their options:
my-server/
├── package.json
├── amxts.config.ts
├── tsconfig.json { "extends": "./.amxts/tsconfig.json" }
├── .env AMXTS_SERVER: the server dev deploys to
├── plugins/
│ └── hello.ts
└── test/
└── hello.test.ts
// amxts.config.ts
export default defineConfig({
modules: [
"@amxts/menu-core",
"@amxts/config-core", // needed by menu-core
],
menus: { file: "my-server/menu" },
});
defineConfig is global — no import. modules lists the module packages the
project uses; each module's options go under its own key (menus is
menu-core's), and the editor checks them against the module. pluginsDir
("plugins") and outDir ("dist") say where the plugins are and where the
build writes; target ("rehlds") - which server the project is for, whose
includes the build reads when there is no server
(the server's includes); imports and pawn -
what a plugin uses without an import line and which modules are built
(auto-imports). See
creating a module for the other side.
npx amxts module add menu-core # installs a module and lists it, with the ones it needs, in amxts.config.ts
npx amxts build # the plugins and the modules' plugins into dist/, with plugins.ini
npx amxts build --deploy # and copies them to the server
npx amxts dev # builds, deploys, and again on every save
npx amxts typecheck # checks the project as the editor does
npx amxts test # the project's tests, on a fake server
pnpm amxts module add menu-core # installs a module and lists it, with the ones it needs, in amxts.config.ts
pnpm amxts build # the plugins and the modules' plugins into dist/, with plugins.ini
pnpm amxts build --deploy # and copies them to the server
pnpm amxts dev # builds, deploys, and again on every save
pnpm amxts typecheck # checks the project as the editor does
pnpm amxts test # the project's tests, on a fake server
yarn amxts module add menu-core # installs a module and lists it, with the ones it needs, in amxts.config.ts
yarn amxts build # the plugins and the modules' plugins into dist/, with plugins.ini
yarn amxts build --deploy # and copies them to the server
yarn amxts dev # builds, deploys, and again on every save
yarn amxts typecheck # checks the project as the editor does
yarn amxts test # the project's tests, on a fake server
bunx amxts module add menu-core # installs a module and lists it, with the ones it needs, in amxts.config.ts
bunx amxts build # the plugins and the modules' plugins into dist/, with plugins.ini
bunx amxts build --deploy # and copies them to the server
bunx amxts dev # builds, deploys, and again on every save
bunx amxts typecheck # checks the project as the editor does
bunx amxts test # the project's tests, on a fake server
The project's package.json has them as scripts too: npm run dev,
npm run build, npm test. Every command and its options:
the amxts command.
The build writes plugins.ini itself: the modules the plugins use first, each
after the modules it requires, then the project's plugins. It also writes
.amxts/tsconfig.json, which the project's tsconfig.json extends, and
.amxts/imports.d.ts: that is how the editor knows the core's API
(@amxts/core), the modules by their names, what a plugin uses without an import
(auto-imports) and the options in
amxts.config.ts (npx amxts prepare writes them alone).
The editor's tooltips - on print, Player, events, fields, the modules'
functions - are in English, or in Russian with AMXTS_DOCS_LANG=ru in the
project's .env; npx amxts prepare (and every build) switches them. The
Russian words are a copy of the API in .amxts/api/ that only the editor
reads: the packages in node_modules stay as they were installed, and the
plugins are built the same in either language.
Your first plugin
plugin({ name: "Hello", version: "1.0.0", author: "you", description: "An example" });
server.addCommand("/hp", ({ player }) => sayHp(player));
server.addEventListener("putInServer", (event) => {
print(0, `${event.player.name} joined`);
});
function sayHp(player: Player) {
print(player, `${player.name}, your HP: ${player.health}`);
if (player.health < 50) player.health = 100;
}
Put the file in the project's plugins/ and run npx amxts build --deploy.
A plugin also works without a project: a .ts file in addons/amxts/plugins,
with its name in plugins.ini, is compiled by the server, and again on every
save, without a map change. What the server needs and where each file goes:
installing on a server.
Hot reload
npx amxts dev
pnpm amxts dev
yarn amxts dev
bunx amxts dev
dev builds every plugin and deploys it, then watches your plugins folder
and the modules the project uses. When you save a file, it rebuilds the plugins that import that file,
copies them to the server and reloads the running server over rcon:
✔ my-plugin · 4.1s
✔ 14:03:40 plugins/my-plugin.ts changed: deployed, the server reloaded my-plugin (4.2s)
A change to a shared file rebuilds every plugin that imports it. A compile
error shows as file.ts:line:col - message above the line it points at. The
server keeps the last good build.
AMXTS_SERVERin.envsays where the server is, as it does for--deploy. Without it,devasks for the folder and writes it there.- The plugins are compiled for the server's system, Windows or Linux, which
the build sees in that folder (
hlds.exeorhlds_linux). For a server it cannot see,AMXTS_SERVER_OS=linuxin.envor--os linuxsays it. - The reload goes through rcon to
127.0.0.1:27015(setAMXTS_PORTto use another port). The password is read fromrcon_passwordin the server'scstrike/server.cfg. Without a password the module still reloads a changed.aotby itself, but you do not see its reply. - A new plugin is added to the server's
plugins.iniand loads with the reload. Without an rcon password it loads with the next reload or map change. - If the server is not running, the command only builds and deploys. It never starts or stops hlds.
- For a server in Docker,
npx amxts dev --dockerbuilds into thedist/the server reads, and the server reloads the plugins itself.
Sections
| page | what's inside |
|---|---|
| Plugin | plugin, commands, events, timers, chat |
| Menus: menu-core | menus for a server: from files, with conditions, placeholders and lists - npx amxts module add menu-core |
| Plugin: menus | a quick menu in code: new Menu("!yShop"), addItem({ title, onSelect }), show(player); AMX Mod X's menu natives |
| Players and entities | Player, Entity: typed properties |
| Players and entities | Vector, Entity.findAll / create / remove |
| Players: actions | server.players, hasModule, team, actions (give, respawn, …) |
| Flags | flags as arrays of names: player.hideHud = ["money"] |
| Effects | temporary effects - beams, explosions, sparks: effects.beamCylinder({ ... }) |
| Cvars | server settings: new Cvar("mp_freezetime"), .number, a "change" event |
| Game events | the game's events (ReAPI hookchains and Ham Sandwich): game.addEventListener("takeDamage", ...) |
| Forwards | your own forwards for other plugins: new Forward<number>("name").emit(1), .subscribe(...) |
| Async | async/await, Promise, sleep, cancelling with AbortSignal |
| Storage | storage that survives map changes, like a Map: new Storage("name").get(key) |
| HTTP | web requests: useFetch<T>(url) reads JSON into an interface; fetch, URL, as in the browser |
| Files | files like Node: fs.readFileSync, fs.writeFile, fs.readdir |
| Natives | your own natives for Pawn plugins: export function |
| Natives | AMXX/ReAPI natives directly, when @amxts/core lacks something |
| Shared modules | modules: one instance on the server for every plugin that uses it |
| Testing | a plugin's tests on a fake server, with bun test |
| CLI | the amxts command: init, dev, build, module add, test, info |
| Installing on a server | amxts on a game server: the module, addons/amxts, the plugin list |
| A server in Docker | a Linux server in Docker with your project mounted: amxts dev --docker |
| Limitations | what amxts cannot do yet, and what to write instead |
What's different about the language
A plugin is TypeScript, compiled ahead of time to machine code. Most of it works as you know it; these are the differences you meet first:
numberis JavaScript's number:7 / 2is3.5,`${hp}`is"100", bitwise operators take it as a 32-bit integer. You never convert numbers for natives: a whole-number field or argument drops the fraction.- A union is of string literals or with
null:"CT" | "TERRORIST",Player | null,number | undefined.number | stringdoes not build. - Closures work as in JavaScript: a listener or a timer uses the
variables around it, a
for (let ...)loop gives each closure its owni, andthisin an arrow is the method's. - Options are an interface with optional fields: a field left out is
undefined,??gives the default,?.reads through a missing object, and destructuring takes defaults:interface GreetOptions {times?: number;loud?: boolean;onDone?: (player: Player) => void; } function greet(player: Player, {times= 1,loud= false }: GreetOptions = {}) { for (leti= 0;i<times;i++)print(player,loud? "HELLO!" : "Hello"); } function farewell(player: Player,options: GreetOptions = {}) { if (options.times!==undefined)print(player, `${options.times} times`);options.onDone?.(player); } - A
catchgets anError:throwtakes anErroror a string, andcatch (error)readserror.messagewithout a cast. An error notrycatches ends the current call with its message in the server console, and the plugin runs on. - A
Recordmay not have the key asked for: inRecord<string, number>a key that was never set reads asundefined, so its values arenumber | undefined- give a default with??. ARecordwhose keys are all written out,Record<"red" | "blue", number>, has them all.
What else the language cannot do in a plugin yet, and what to write instead, is on Limitations.
Introduction
amxts lets you write AMX Mod X plugins for Counter-Strike 1.6 in TypeScript. A plugin is an ordinary .ts file: familiar types, classes, closures, async/await, events you listen to. It runs on the server next to your Pawn plugins, and the two call each other.
CLI
amxts is the command a project runs: it creates projects and modules, adds modules, builds, deploys, type-checks and tests. It is the package @amxts/cli, which @amxts/core depends on, so every project has it and runs it through its package manager: