Ограничения
Чего 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, собранные в проекте (установка на сервер).
Сервер в Docker
ghcr.io/amxts/server — образ Docker с сервером Counter-Strike 1.6 на Linux, на котором работает amxts: HLDS с ReHLDS, ReGameDLL, Metamod-R, AMX Mod X 1.10 и ReAPI, модуль amxts и его компилятор. Ваш проект монтируется в него, и сервер запускает ваши плагины с вашими конфигами и файлами — на Windows, macOS или Linux, и ставить не нужно ничего, кроме Docker.
Плагин
Плагин — это один файл .ts в папке плагинов проекта. Работу он делает на верхнем уровне файла: называет себя, добавляет команды и подписывается на события. То, чем он пользуется из ядра, — plugin, server, Player, print — строки импорта не требует (автоимпорты).