AMX Mod X · Counter-Strike 1.6

CS 1.6 plugins in TypeScript.

Write a plugin the way you write any TypeScript: familiar types, classes, Map, async/await, events. amxts compiles it to WebAssembly and then ahead of time to machine code, and runs it inside an AMX Mod X module — next to your Pawn plugins.

import { Player, 
plugin
,
print
,
server
} from "~/facade";
plugin
({
name
: "Say HP",
version
: "1.0.0",
author
: "Author",
description
: "An example",
});
server
.
addCommand
("/hp", sayHp);
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;
}

TypeScript, not a new language

A plugin is ordinary TypeScript. Everything AssemblyScript and Pawn need — cells, float bit patterns, string buffers — is kept under the hood, in the facade and the generated natives.

    Types you already know

    Classes, arrays, Map, template strings, number and string. Set a player's health with player.health = 100 instead of looking up the right native.

    The editor knows the game

    Every reapi field, player member and hookchain comes with its real type. Event names autocomplete, and a typo fails before it reaches the server.

    async and await

    A real Promise: await sleep(1000), await fetch(url), .then and .catch. The rest of the function runs on a later frame while the server gets on with everything else.

    Events like the DOM

    server.addEventListener("putinserver", ...) for AMX Mod X events, game.addEventListener("takeDamage", ...) for reapi hookchains, event.preventDefault() to block one.

    Save and play

    Save a file and the running server rebuilds and reloads it. No amxxpc, no map change, nobody disconnected.

    Tests without a server

    Test plugins with Vitest or bun test: a fake server runs the real compiled plugin. Players join, say commands, and timers advance when the test says so.

Under the hood

Compiled ahead of time, inside AMX Mod X

amxts does not translate anything into Pawn. The plugin is compiled to WebAssembly by AssemblyScript, then to i386 machine code by WAMR's wamrc, and the amxts_amxx module loads the result. A native is looked up by name, so every native of every loaded module — and of any other plugin — is reachable.

    Machine code, not an interpreter

    Plugins are compiled to machine code before the server loads them.

    Any native, typed

    The generator reads the .inc files and writes every native with its signature: reapi, hamsandwich and cstrike are there the moment their include is.

Build say-hp.ts AssemblyScript .wasm wamrc .aot

CS 1.6 server

AMX Mod X

amxts_amxx

runs your .aot plugins with WAMR

say-hp.aot loaded

amxts_host.amxx

generated, holds no logic

natives by name

Modules and Pawn plugins

reapi, hamsandwich, cstrike, your .amxx

Pawn keeps working

Pawn plugins can call TypeScript

Every export function of a plugin is an AMX Mod X native with its real types, and the build writes the .inc a Pawn plugin includes.

/** A value from a config file, or false when the key is not there. */
export function cfg_get_value(
file
: string,
section
: string,
key
: string) {
return lookup(
file
,
section
,
key
); // string | null
}

Menus, configs and HTTP are modules

menu-core, config-core and http are made with the framework, and any module can join the catalog with a pull request to amxts/modules.