Limitations
What amxts cannot do yet, each with what to write instead. The build, the editor or the server console says so for almost every item below; this page is where to look up what it means.
How a feature behaves because of the game, AMX Mod X, the protocol or a file format - a timer waits at least 0.1 second, a chat message has one team colour - is noted on that feature's own page, beside what it concerns.
The language
A plugin is TypeScript compiled to WebAssembly and then to machine code. What TypeScript has and a plugin does not:
throwtakes anErroror a string, which becomes anErrorwith it as the message:throw 42does not build. Acatchgets anError-error.message,error.name,instanceoffor a class of your own. A crash that is not an error - a stack overflow - ends the call past everycatch.- In tests an error's
stackis its first line: the fake server (amxts test) runs a plugin without the frames a game server keeps. Checkerror.message; the calls show on a server (errors). - No unions of different types.
number | string, andcond ? 1 : "a", do not build. Unions of string literals ("CT" | "TERRORIST"),T | nullandT | undefinedwork, and so dotrue | false | "default"and a function-or-list option such as menu-core'senabled. Use two parameters, two functions, or a common base class. - A type alias does not reach itself.
type Text = string | ((context: Context) => string)does not build ("Recursive types") whenContexthas a field of a class with a field of typeText. Write the type out at that field:title: string | ((context: Context) => string). - No
anyandunknown. Every value has a type when the plugin is built. - An object literal takes its type from its values, so each value needs
one:
{ owner: null }or{ items: [] }alone does not build - declare an interface (const team: Team = { ... }), or write the literal where the type is known. - A plain object goes where another is expected when it has the same
fields:
{ y: 2, x: 1 }is aPointofxandy, and alabelgiven goes wherelabel?is optional; one that leaves an optional field out does not - write the literal where the type is known. - An object literal's methods have no
thisof the object: where an interface has methods, a literal of it writes them as arrows or asrun() { ... }, and reads the object's fields through a variable. - A class goes where an interface of fields is expected when it says
implements, and extends no other class:class Spot implements Pointis aPoint; a class of the same fields withoutimplementsis not. JSON.parseis told what it reads:JSON.parse<Settings>(text), orJSON.parse(text) as Settings; alone it does not build. A field the text leaves out that is neither optional nor has a default is aTypeError, where JavaScript leaves itundefined.JSON.stringifytakes no replacer function (JSON).- Regular expressions have no lookbehind (
(?<=...)), named groups,\p{...}or theuanddflags: such a pattern is aSyntaxError. A group that did not match reads as"", notundefined; a function given toreplacegets the match and up to four groups. A match that takes more than five million steps throws anError, so that a pattern that backtracks without end does not hold the server. - A date knows a few languages and no
Intl:toLocaleStringand the rest take a language (en-US,en-GB,de,fr,ru), not options, andtoStringwrites the time zone as its offset, without its name in brackets (time). - Syntax that does not build:
Does not build Write instead function*,yieldan array, or asyncfunctions andawait - A class field holding an arrow that returns more than a plain value
needs its type:
onTick = () => { ... }anddouble = (n: number) => n * 2build;label = () => this.makeLabel()needslabel: () => string = () => this.makeLabel().
Variables and optional values
- A method that runs early through a base class may read a variable
before its declaration as an empty value, where JavaScript throws a
ReferenceError. - Inside a closure, a boolean left out is
false: aflag?: booleanparameter, or a variable that holds an optional boolean field, read in an arrow function made inside the function does not see it was left out -() => flag ?? trueisfalse. Read it outside:const loud = flag ?? true. undefinedis known by the type written where it is read: an optional field or parameter, a variable or a function's result ofT | undefined,findandmap.get- and a variable that holds one of them - print asundefinedand count asNaN. An item read by index from an array ofT | undefinedprints text asnull, and a number counted with staysundefined: give it a default with??first.x!on anullthrows aTypeErrorright there, where TypeScript's!does nothing and JavaScript throws only where the value is used.
Async and promises
- Async generators (
async function*) do not build. Promise.raceandanyneed one type: promises of anumberand astring, or anumberand asleep, do not build. Await them one by one.Promise.allof different types takes up to 8 promises; promises of one type, or a list, have no such limit (async).- A waiting async function keeps up to 4 KB of its own state; one that needs more is stopped, with a message in the console.
Events, forwards and natives
- A command's handler is written in the call: the object it gets -
playerand the arguments - is a type the build makes for that call. It goes where the interface of its arguments goes,(args) => kick(args)forkick(args: KickArgs); without one it has no name to give a function written apart: take its parts there,({ player, text }) => say(player, text). A command's arguments are written in its usage - in place, or after a name made at run time,`${name} [value]`; a usage held in a variable takes none. - An event name is written out:
server.addEventListener(name, ...)orserver.addMessageListener(name, ...)with the name in a variable does not build - the name picks the event's type. So is a"playerChange"listener's field:{ field: "spawnProtected" }, not a variable. Withoutfield, the event'svaluehas no type: read the field fromevent.player. - A game listener either always answers or never does. One that should
answer only sometimes returns nothing and calls
event.preventDefault(), or always answers, repeating the game's rule for the rest (game events). - A vector field of a game event is written whole:
event.direction = new Vector(x, y, z);event.direction.x = 1changes a copy (game events). - A forward from a Pawn plugin reaches TypeScript only when an include
amxts comes with declares it, and a
Teamor aRoundWinnerargument needs the forward declared in an include (forwards). - A number in a native's
any:...tail is aFloat:only where amxts knows the native reads one -engfunc,dllfunc,ExecuteHam,pevand the others natives lists; elsewhere it goes as a whole number. Where such a native also writes text, read that and convert it:nvault_get(vault, key, text)withconst text = new Ref(""), thenNumber(text.value). The tail takes at most twelve arguments. - A plugin's own natives are its file's
export functions, with the parameter types natives lists; another type stops the build. - A native of your own takes up to 64 arguments as Pawn passes them - an array or a string result counts twice, with its size; more stops the build: pass an array instead (natives).
- A buffer's length is never negative: a native given a buffer and a
length below 0 - the
-1thatArrayGetArray,ArrayPushArrayand othercellarraynatives read as "the whole item" - is not called and answers0. Pass the buffer's own length. lang.translatetakes its arguments as strings: a number goes in as text,`${seconds}`(translations).
Players and entities
- A value no name stands for - another plugin or a mod wrote it - reads
as
"unknown", and assigning"unknown"changes nothing. Read the number with the native:get_entvar(player.id, var_rendermode), or, on a server without ReAPI, the engine module'sentity_get_int(player.id, EV_INT_rendermode). - Plain numbers:
autoSwitchWeapon,shotgunReloadStage. - No property for: array members (
m_rgAmmo, …; the weapons areplayer.items),var_controller,var_blending. Useget_memberandset_member, or fakemeta'sget_ent_dataandset_ent_dataon a server without ReAPI (entities). - A vector or a text field read through a native needs its type:
get_entvar<Vector>(id, var_origin),get_member<string>(id, m_szAnimExtention). Without it the field reads as a number, and a vector or text field reads as0, with a line in the console that says what to write. - A flag list writes back through
pushonly:popandsplicechange the array, not the game. To remove a flag, assign a filtered array (flags). - Shared player fields are
boolean,number,string, a union of string literals,Player[]or an object of those; never optional, and an object's members are not objects. A namePlayeralready has is refused.
Effects
- No effect for decals (a bullet hole, a spray - they need a decal's
index), for the engine's tracer colours or for a text message
(
player.showHudis that); send them withmessage_beginandwrite_*(effects).
Storage
Storageholds text:value.toString()in,parseInt(value)out. A value reads back up to 255 bytes of UTF-8 - about 127 Cyrillic letters (storage).
HTTP
- A request's body is text:
bodytakes a string -JSON.stringify(value), or an object throughuseFetch; not aFormData,Blob,ArrayBufferorURLSearchParams. A form goes as text:body: query.toString()with"Content-Type": "application/x-www-form-urlencoded"(HTTP). - Headers are given as an object of names and values, not as a
Headersor a list of pairs. new URLSearchParamstakes an object, not a query as text: read one withnew URL(address).searchParams.- No
response.bodystream: a response is read whole, so a download or an upload has no progress to follow. useFetch'squerytakes text:{ page: `${page}` }.
FTP and SFTP
- An SSH key is RSA in PEM:
ssh-keygen -t rsa -m PEM. Keys in OpenSSH's own format (BEGIN OPENSSH PRIVATE KEY, whatssh-keygenwrites by default), ECDSA and Ed25519 keys are not read, and a server whose only host key is Ed25519 cannot be reached. Convert an RSA key withssh-keygen -p -m PEM -f key(network requests).
Configs
- The build reads the shape from the source, and refuses, with the place
and the fix: an empty list without a type, a defaults variable without a
declared type, a list of lists of lists or of objects, a
Mapof lists or objects, a union that is not of names (number | string), a generic interface or one thatextendsanother, a field named with a string, a type that contains itself. - A
Map's keys have no.and no[: such a key cannot be read or written. - YAML is the part configs use. Not read: anchors and aliases (
&,*), tags (!), complex keys (?), directives, several documents, a plain value that goes on for several lines (quote it, or use|or>), tabs in indentation, a key written twice, a key inside[ ]. Each is an error with the file, line and column, and the file reads as empty. - Saving writes a YAML list written
[a, b]one item per line, and drops a comment after a value on its line and comments inside{ }and[ ]. Comments on lines of their own and blank lines are kept.
Shared modules
The details are on shared modules.
- What cannot cross between plugins does not build: a
Map, aSet, aRecordwith keys of any string, a generic class, a class from the standard library, a type the module does not export, a computed default, a generic orasyncfunction, a rest parameter, an exported variable (export let count) - an object of a class of the module's own (export const counter = new Counter()) does cross. - An array or object field of a shared object reads as a copy:
pushon it changes the copy. Use the module's method, or assign the whole field.
Modules and the amxts command
- A module gives one name: itself as a namespace (
imports: [{ from, as }]) or one of its exports (imports: [{ from, name }]) - not a list of its functions or types;menus.Menuis a type of the namespace. - A module's
meta,requires,defaultsandimportsare literals: the build reads them without running the module. Besidessetup,defineModulehas no lifecycle hooks: listen to server events insidesetup. - Tests run with
amxts test, which isbun test; other test runners are not supported. - A native the fake server does not answer stops the test with its name;
answer it with
server.defineNative(name, native)in the test, or in a module's test kit.
The editor
- The build and
amxts typecheckuse TypeScript 5, the one the core installs; the editor works with TypeScript 5.9, 6 and 7. - A field the editor accepts may be missing in the build: the editor
sees the fields every file of the plugins folder adds to
Player, the build only those of the files the plugin imports. Add theimportof the file that declares it. - Live help in menu files is for VS Code only, through the amxts extension (menus).
The server
- A
.tsthe server compiles holds the server for the second or two the compile takes: in the middle of a round the game freezes. On a server people play on, deploy.aotfiles built in a project (installing on a server).
A server in Docker
ghcr.io/amxts/server is a Docker image of a Linux Counter-Strike 1.6 server that runs amxts: HLDS with ReHLDS, ReGameDLL, Metamod-R, AMX Mod X 1.10 and ReAPI, the amxts module and its compiler. Your project is mounted into it, and the server runs your plugins with your configs and files - on Windows, macOS or Linux, with nothing to install but Docker.
Plugin
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).