Начало работы

Начало работы

amxts запускает плагины для Counter-Strike 1.6, написанные на TypeScript, внутри AMX Mod X, рядом с вашими плагинами на Pawn. Плагин — обычный файл .ts: игроки — объекты, у событий есть типы, а редактор знает каждое поле игры. На этой странице — проект, первый плагин, как перезагружать его во время работы и чем язык отличается от привычного TypeScript.

Проект

Новый проект начинается с одной команды:

npm create amxts@latest

Она спрашивает менеджер пакетов, папку, модули, oxlint и oxfmt, git и папку сервера, пишет проект с первым плагином и его тестом, всё ставит и говорит, что запускать дальше. npx amxts init в папке — та же команда; у каждого вопроса есть и флаг (команда amxts).

Проект — папка с плагинами и одним файлом настроек, amxts.config.ts, где перечислены модули проекта и их настройки:

my-server/
├── package.json
├── amxts.config.ts
├── tsconfig.json       { "extends": "./.amxts/tsconfig.json" }
├── .env                AMXTS_SERVER: сервер, на который выкладывает dev
├── 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 глобальная — импорт не нужен. modules перечисляет пакеты-модули проекта; настройки каждого модуля пишутся под его ключом (menus — у menu-core), и редактор проверяет их по модулю. pluginsDir ("plugins") и outDir ("dist") говорят, где плагины и куда пишет сборка; target ("rehlds") — для какого сервера проект: его include сборка читает, когда сервера нет (include сервера); imports и pawn — чем плагин пользуется без строки импорта и какие модули собираются (автоимпорты). Другая сторона — свой модуль.

npx amxts module add menu-core   # ставит модуль и вписывает его в amxts.config.ts вместе с теми, что ему нужны
npx amxts build                  # плагины и плагины модулей в dist/, с plugins.ini
npx amxts build --deploy         # и копирует их на сервер
npx amxts dev                    # собирает, выкладывает и снова — на каждое сохранение
npx amxts typecheck              # проверяет проект так же, как редактор
npx amxts test                   # тесты проекта, на поддельном сервере

В package.json проекта они есть и скриптами: npm run dev, npm run build, npm test. Все команды и их параметры — команда amxts.

plugins.ini сборка пишет сама: сначала модули, которыми пользуются плагины, каждый после тех, что ему нужны, потом плагины проекта. Ещё она пишет .amxts/tsconfig.json, который расширяет tsconfig.json проекта, и .amxts/imports.d.ts: так редактор знает API ядра (@amxts/core), модули по именам, то, чем плагин пользуется без импорта (автоимпорты), и настройки в amxts.config.ts (npx amxts prepare пишет только их).

Подсказки редактора — у print, Player, событий, полей, функций модулей — на английском, а с AMXTS_DOCS_LANG=ru в .env проекта — на русском; переключает их npx amxts prepare (и каждая сборка). Русские слова — копия API в .amxts/api/, которую читает только редактор: пакеты в node_modules остаются такими, какими их поставили, а плагины собираются одинаково на любом языке.

Первый плагин

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;
}

Файл кладётся в plugins/ проекта, затем — npx amxts build --deploy. Плагин работает и без проекта: файл .ts в addons/amxts/plugins, с именем в plugins.ini, сервер компилирует сам — и снова при каждом сохранении, без смены карты. Что нужно серверу и куда ложится каждый файл — установка на сервер.

Горячая перезагрузка

npx amxts dev

dev собирает и выкладывает все плагины, затем следит за папкой ваших плагинов и модулями проекта. Когда вы сохраняете файл, она пересобирает плагины, которые его импортируют, копирует их на сервер и перезагружает запущенный сервер через rcon:

✔ my-plugin · 4.1s
✔ 14:03:40 plugins/my-plugin.ts changed: deployed, the server reloaded my-plugin (4.2s)

Изменение общего файла пересобирает все плагины, которые его импортируют. Ошибка компиляции выводится как file.ts:строка:столбец - сообщение, над строкой, на которую она указывает. На сервере остаётся последняя удачная сборка.

  • AMXTS_SERVER в .env указывает, где сервер, как и для --deploy. Без него dev спрашивает папку и записывает её туда.
  • Плагины компилируются под систему сервера, Windows или Linux, — сборка видит её в этой папке (hlds.exe или hlds_linux). Для сервера, которого она не видит, это говорит AMXTS_SERVER_OS=linux в .env или --os linux.
  • Перезагрузка идёт через rcon на 127.0.0.1:27015 (другой порт задаётся через AMXTS_PORT). Пароль берётся из rcon_password в cstrike/server.cfg сервера. Без пароля модуль всё равно сам перезагружает изменённый .aot, но его ответа вы не увидите.
  • Новый плагин добавляется в plugins.ini сервера и загружается вместе с перезагрузкой. Без пароля rcon он загрузится со следующей перезагрузкой или сменой карты.
  • Если сервер не запущен, команда только собирает и выкладывает плагины. Она никогда не запускает и не останавливает hlds.
  • Для сервера в Docker npx amxts dev --docker собирает в dist/, который читает сервер, и сервер перезагружает плагины сам.

Разделы

страницачто внутри
Плагинplugin, команды, события, таймеры, чат
Меню: menu-coreменю для сервера: из файлов, с условиями, плейсхолдерами и списками — npx amxts module add menu-core
Плагин: менюбыстрое меню в коде: new Menu("!yМагазин"), addItem({ title, onSelect }), show(player); нативы меню AMX Mod X
Игроки и сущностиPlayer, Entity: свойства с типами
Игроки и сущностиVector, Entity.findAll / create / remove
Игроки: действияserver.players, hasModule, команда, действия (give, respawn, …)
Флагифлаги как массивы имён: player.hideHud = ["money"]
Эффектывременные эффекты — лучи, взрывы, искры: effects.beamCylinder({ ... })
Кварынастройки сервера: new Cvar("mp_freezetime"), .number, событие "change"
События игрысобытия игры (хукчейны ReAPI и Ham Sandwich): game.addEventListener("takeDamage", ...)
Форвардысвои форварды для других плагинов: new Forward<number>("name").emit(1), .subscribe(...)
Asyncasync/await, Promise, sleep, отмена через AbortSignal
Хранилищехранилище между картами как Map: new Storage("name").get(key)
HTTPвеб-запросы: useFetch<T>(url) читает JSON в интерфейс; fetch, URL — как в браузере
Файлыфайлы как в Node: fs.readFileSync, fs.writeFile, fs.readdir
Нативысвои нативы для Pawn-плагинов: export function
Нативынативы AMXX/ReAPI напрямую, когда в @amxts/core этого нет
Общие модулимодули: один экземпляр на сервере для всех плагинов, которые им пользуются
Тестытесты плагина на поддельном сервере, через bun test
Команда amxtsкоманда amxts: init, dev, build, module add, test, info
Установка на серверamxts на игровом сервере: модуль, addons/amxts, список плагинов
Сервер в Dockerсервер на Linux в Docker со смонтированным проектом: amxts dev --docker
Ограничениячего amxts пока не умеет и что писать вместо этого

Главное о языке

Плагин — это TypeScript, заранее скомпилированный в машинный код. Почти всё работает так, как вы привыкли; вот отличия, которые встречаются первыми:

  • number — число JavaScript: 7 / 2 — это 3.5, `${hp}` — это "100", побитовые операции берут его как 32-битное целое. Для нативов числа переводить не нужно: целочисленное поле или аргумент просто отбрасывает дробную часть.
  • Юнион — из строковых литералов или с null: "CT" | "TERRORIST", Player | null, number | undefined. number | string не собирается.
  • Замыкания работают как в JavaScript: обработчик или таймер пользуется переменными вокруг, цикл for (let ...) даёт каждому замыканию своё i, а this в стрелке — это this метода.
  • Настройки — интерфейс с необязательными полями: незаданное поле — это undefined, ?? даёт значение по умолчанию, ?. читает сквозь отсутствующий объект, а деструктуризация принимает значения по умолчанию:
    interface GreetOptions {
        
    times
    ?: number;
    loud
    ?: boolean;
    onDone
    ?: (
    player
    : Player) => void;
    } function greet(
    player
    : Player, {
    times
    = 1,
    loud
    = false }: GreetOptions = {}) {
    for (let
    i
    = 0;
    i
    <
    times
    ;
    i
    ++)
    print
    (
    player
    ,
    loud
    ? "ПРИВЕТ!" : "Привет");
    } function farewell(
    player
    : Player,
    options
    : GreetOptions = {}) {
    if (
    options
    .
    times
    !==
    undefined
    )
    print
    (
    player
    , `${
    options
    .
    times
    } раз`);
    options
    .
    onDone
    ?.(
    player
    );
    }
  • catch получает Error: throw принимает Error или строку, а catch (error) читает error.message без приведения. Ошибка, которую не поймал ни один try, завершает текущий вызов с сообщением в консоли сервера, а плагин работает дальше.
  • В Record может не быть нужного ключа: в Record<string, number> ключ, который не задавали, читается как undefined, поэтому значения там — number | undefined; значение по умолчанию даёт ??. У Record, чьи ключи все выписаны, — Record<"red" | "blue", number>, — есть все они.

Чего ещё язык в плагине пока не умеет и что писать вместо этого — в Ограничениях.