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

Ограничения

Чего amxts пока не умеет — и что писать вместо этого. Почти о каждом пункте ниже скажут сборка, редактор или консоль сервера; здесь можно посмотреть, что это значит.

Как ведёт себя возможность из-за игры, AMX Mod X, протокола или формата файла — таймер ждёт не меньше 0,1 секунды, у сообщения в чате один цвет команды, — написано на странице самой возможности, рядом с тем, чего это касается.

Язык

Плагин — это TypeScript, скомпилированный в WebAssembly, а затем в машинный код. Что есть в TypeScript, но нет в плагине:

  • throw принимает Error или строку, которая становится Error с ней в сообщении: throw 42 не собирается. catch получает Error — error.message, error.name, instanceof для своего класса. Падение, которое не ошибка, — переполнение стека — завершает вызов мимо всех catch.
  • В тестах stack ошибки — её первая строка: поддельный сервер (amxts test) выполняет плагин без кадров, которые хранит игровой сервер. Проверяй error.message; вызовы видны на сервере (ошибки).
  • Нет юнионов из разных типов. number | string и cond ? 1 : "a" не собираются. Юнионы строковых литералов ("CT" | "TERRORIST"), T | null и T | undefined работают, как и true | false | "default" и настройка «функция или список» вроде enabled в menu-core. Берите два параметра, две функции или общий базовый класс.
  • Псевдоним типа не доходит до себя. type Text = string | ((context: Context) => string) не собирается («Recursive types»), если у Context есть поле класса, у которого есть поле типа Text. Запишите тип у этого поля целиком: title: string | ((context: Context) => string).
  • Нет any и unknown. У каждого значения есть тип, когда плагин собирается.
  • Объектный литерал берёт тип от своих значений, поэтому тип нужен каждому: { owner: null } или { items: [] } сами по себе не собираются — объявите интерфейс (const team: Team = { ... }) или пишите литерал там, где тип известен.
  • Простой объект подходит туда, где ждут другой, когда поля те же:{ y: 2, x: 1 } — это Point из x и y, а заданный label подходит туда, где label? необязателен; объект без необязательного поля — нет: пишите литерал там, где тип известен.
  • У методов объектного литерала нет this объекта: где у интерфейса есть методы, литерал пишет их стрелками или как run() { ... }, а поля объекта читает через переменную.
  • Класс подходит туда, где ждут интерфейс из полей, когда он пишет implements и не расширяет другой класс: class Spot implements Point — это Point; класс с теми же полями без implements — нет.
  • JSON.parse говорят, что он читает: JSON.parse<Settings>(text) или JSON.parse(text) as Settings; сам по себе он не собирается. Поле, которого в тексте нет и которое не необязательное и без значения по умолчанию, — это TypeError, а в JavaScript оно было бы undefined. JSON.stringify не принимает функцию-заменитель (JSON).
  • В регулярных выражениях нет ретроспективной проверки ((?<=...)), именованных групп, \p{...} и флагов u и d: такой шаблон — это SyntaxError. Группа, которая не совпала, читается как "", а не undefined; функция, переданная в replace, получает совпадение и до четырёх групп. Совпадение дольше пяти миллионов шагов бросает Error, чтобы шаблон, который перебирает без конца, не держал сервер.
  • Дата знает несколько языков и не знает Intl: toLocaleString и остальные принимают язык (en-US, en-GB, de, fr, ru), а не настройки, а toString пишет часовой пояс смещением, без его имени в скобках (время).
  • Синтаксис, который не собирается:
    Не собираетсяПишите вместо
    function*, yieldмассив или async-функции и await
  • Полю класса со стрелкой, которая возвращает не простое значение, нужен тип: onTick = () => { ... } и double = (n: number) => n * 2 собираются; label = () => this.makeLabel() требует label: () => string = () => this.makeLabel().

Переменные и необязательные значения

  • Метод, который выполняется рано через базовый класс, может прочитать переменную до её объявления как пустое значение, там где JavaScript бросил бы ReferenceError.
  • Внутри замыкания незаданный булев — это false: параметр flag?: boolean или переменная с необязательным логическим полем, прочитанные в стрелочной функции, созданной внутри функции, не видят, что их не задали, — () => flag ?? true даёт false. Прочитайте снаружи: const loud = flag ?? true.
  • undefined узнаётся по типу, написанному там, где значение читают: необязательное поле или параметр, переменная или результат функции типа T | undefined, find и map.get — и переменная, в которой лежит одно из них, — печатаются как undefined и в счёте дают NaN. Элемент массива из T | undefined, прочитанный по индексу, печатает текст как null, а число в счёте остаётся undefined: сначала подставьте значение через ??.
  • x! от null сразу бросает TypeError, а в TypeScript ! ничего не делает, и JavaScript бросает, только когда значение используют.

Async и промисы

  • Async-генераторы (async function*) не собираются.
  • Promise.race и any нужен один тип: промисы number и string или number и sleep не собираются. Дожидайтесь их по одному.
  • Promise.all из разных типов принимает до 8 промисов; у промисов одного типа и у списка такого предела нет (async).
  • Ожидающая async-функция хранит до 4 КБ своего состояния; функция, которой нужно больше, останавливается с сообщением в консоли.

События, форварды и нативы

  • Обработчик команды пишется в вызове: объект, который он получает, — player и аргументы — тип, который сборка делает для этого вызова. Он идёт туда, где ждут интерфейс его аргументов: (args) => kick(args) для kick(args: KickArgs); без интерфейса у него нет имени, чтобы дать его функции, написанной отдельно: возьмите его части там, ({ player, text }) => say(player, text). Аргументы команды пишутся в её использовании — на месте или после имени, которое строится при запуске, `${name} [value]`; использование из переменной аргументов не принимает.
  • Имя события пишется строкой: server.addEventListener(name, ...) или server.addMessageListener(name, ...) с именем в переменной не собирается — по имени выбирается тип события. Так же пишется и поле обработчика "playerChange": { field: "spawnProtected" }, а не переменная. Без field у value события нет типа: читайте поле у event.player.
  • Обработчик события игры отвечает всегда или никогда. Тот, что должен отвечать лишь иногда, ничего не возвращает и вызывает event.preventDefault() — или отвечает всегда, повторяя правило игры для остальных случаев (события игры).
  • Поле-вектор события игры записывается целиком:event.direction = new Vector(x, y, z); event.direction.x = 1 меняет копию (события игры).
  • Форвард плагина на Pawn доходит до TypeScript, только если он объявлен в include, который идёт с amxts, а аргументу Team или RoundWinner нужно объявление форварда в include (форварды).
  • Число в хвосте any:... натива — Float: только там, где amxts знает, что натив читает его так, — engfunc, dllfunc, ExecuteHam, pev и другие, которые перечисляет страница нативы; в остальных оно уходит целым. Если такой натив умеет писать и текст, прочитайте его и переведите: nvault_get(vault, key, text) с const text = new Ref(""), затем Number(text.value). Хвост принимает не больше двенадцати аргументов.
  • Собственные нативы плагина — это export function его файла с типами параметров из списка на странице нативы; другой тип останавливает сборку.
  • У вашего натива до 64 аргументов в том виде, в каком их передаёт Pawn, — массив или строковый результат считается дважды, вместе с размером; больше останавливает сборку: передавайте массив (нативы).
  • Длина буфера не бывает отрицательной: натив, которому передан буфер и длина меньше 0 — -1, который ArrayGetArray, ArrayPushArray и другие нативы cellarray читают как «весь элемент», — не вызывается и отвечает 0. Передавайте длину самого буфера.
  • lang.translate принимает аргументы строками: число передаётся текстом, `${seconds}` (переводы).

Игроки и сущности

  • Значение, для которого нет имени, — его записал другой плагин или мод — читается как "unknown", а присваивание "unknown" ничего не меняет. Число читайте нативом: get_entvar(player.id, var_rendermode), а на сервере без ReAPI — entity_get_int(player.id, EV_INT_rendermode) модуля engine.
  • Простые числа: autoSwitchWeapon, shotgunReloadStage.
  • Нет свойства для: членов-массивов (m_rgAmmo, …; оружие — это player.items), var_controller, var_blending. Берите get_member и set_member, а на сервере без ReAPI — get_ent_data и set_ent_data модуля fakemeta (сущности).
  • Векторному или текстовому полю, прочитанному нативом, нужен его тип:get_entvar<Vector>(id, var_origin), get_member<string>(id, m_szAnimExtention). Без него поле читается числом, а векторное или текстовое поле читается как 0, и в консоли появляется строка о том, что написать.
  • Список флагов пишется обратно только через push: pop и splice меняют массив, а не игру. Чтобы убрать флаг, присвойте отфильтрованный массив (флаги).
  • Общие поля игрока — это boolean, number, string, юнион строковых литералов, Player[] или объект из них; необязательных не бывает, и члены объекта — не объекты. Имя, которое у Player уже есть, не принимается.

Эффекты

  • Нет эффектов для декалей (дырки от пули, рисунка на стене — им нужен индекс декали), для цветов трассеров движка и для текстового сообщения (это player.showHud); их отправляют через message_begin и write_* (эффекты).

Хранилище

  • Storage хранит текст: value.toString() внутрь, parseInt(value) наружу. Значение читается обратно до 255 байт UTF-8 — это около 127 букв кириллицы (хранилище).

HTTP

  • Тело запроса — текст: body принимает строку — JSON.stringify(value) или объект через useFetch; не FormData, Blob, ArrayBuffer или URLSearchParams. Форма уходит текстом: body: query.toString() с "Content-Type": "application/x-www-form-urlencoded" (HTTP).
  • Заголовки задаются объектом имён и значений, не Headers и не списком пар.
  • new URLSearchParams принимает объект, а не запрос текстом: прочитать его можно через new URL(address).searchParams.
  • Нет потока response.body: ответ читается целиком, так что у загрузки и отправки нет прогресса, за которым можно следить.
  • query у useFetch принимает текст: { page: `${page}` }.

FTP и SFTP

  • Ключ SSH — RSA в PEM: ssh-keygen -t rsa -m PEM. Ключи в собственном формате OpenSSH (BEGIN OPENSSH PRIVATE KEY, что ssh-keygen пишет по умолчанию), ключи ECDSA и Ed25519 не читаются, а до сервера, у которого ключ хоста только Ed25519, не достучаться. Ключ RSA переводится в PEM командой ssh-keygen -p -m PEM -f key (сетевые запросы).

Конфиги

  • Сборка читает форму из исходника и отказывает, называя место и исправление: пустой список без типа, переменная со значениями по умолчанию без объявленного типа, список списков списков или объектов, Map списков или объектов, юнион не из имён (number | string), обобщённый интерфейс или интерфейс с extends, поле, названное строкой, тип, содержащий сам себя.
  • В ключах Map нет . и [: такой ключ не прочитать и не записать.
  • Из YAML конфиги берут только часть. Не читаются: якоря и ссылки (&, *), теги (!), сложные ключи (?), директивы, несколько документов, простое значение на несколько строк (возьмите его в кавычки или используйте | или >), табуляция в отступах, ключ, записанный дважды, ключ внутри [ ]. Каждое — ошибка с файлом, строкой и столбцом, и файл читается пустым.
  • Сохранение пишет список YAML, записанный как [a, b], по элементу на строку и выбрасывает комментарий после значения на той же строке и комментарии внутри { } и [ ]. Комментарии на отдельных строках и пустые строки сохраняются.

Общие модули

Подробно — в общих модулях.

  • То, что не передаётся между плагинами, не собирается: Map, Set, Record с ключами из любых строк, обобщённый класс, класс из стандартной библиотеки, тип, который модуль не экспортирует, вычисляемое значение по умолчанию, обобщённая или async-функция, остаточный параметр, экспортированная переменная (export let count) — объект класса самого модуля (export const counter = new Counter()) передаётся.
  • Поле-массив или поле-объект общего объекта читается как копия: push меняет копию. Пользуйтесь методом модуля или присваивайте поле целиком.

Модули и команда amxts

  • Модуль даёт одно имя: себя пространством имён (imports: [{ from, as }]) или один из своих экспортов (imports: [{ from, name }]), а не список своих функций или типов; menus.Menu — тип из пространства имён.
  • meta, requires, defaults и imports модуля — литералы: сборка читает их, не запуская модуль. Кроме setup, у defineModule нет хуков жизненного цикла: подписывайтесь на события сервера внутри setup.
  • Тесты запускает amxts test, то есть bun test; другие тестовые раннеры не поддерживаются.
  • Натив, на который фейковый сервер не отвечает, останавливает тест с его именем; ответьте на него через server.defineNative(name, native) в тесте или в тестовом наборе модуля.

Редактор

  • Сборка и amxts typecheck используют TypeScript 5, который ставит ядро; редактор работает с TypeScript 5.9, 6 и 7.
  • Поле, которое принимает редактор, может не найтись при сборке: редактор видит поля, которые добавляет Player любой файл папки плагинов, а сборка — только поля файлов, которые плагин импортирует. Добавьте import файла, который его объявляет.
  • Живая помощь в файлах меню — только в VS Code, через расширение amxts (меню).

Сервер

  • .ts, который компилирует сервер, задерживает сервер на секунду-две, пока идёт компиляция: посреди раунда игра замирает. На сервер, где играют, выкладывайте .aot, собранные в проекте (установка на сервер).