Нативы
Натив — функция, которую один плагин или модуль AMX Mod X даёт остальным.
Здесь нативы работают в обе стороны: плагин на TypeScript экспортирует свои
для плагинов на Pawn и вызывает нативы AMX Mod X, ReAPI и других модулей там,
где у @amxts/core нет своего способа.
Свои нативы: export function
Каждая export function файла плагина — натив под тем же именем, который
может вызвать плагин на Pawn:
plugin({ name: "Points", version: "1.0.0", author: "you" });
const saved = new Storage("myplugin_points");
/** Очки игрока; 0, если очков нет. */
export function points_get(player: Player) {
const text = saved.get(player.steamId);
return text == null ? 0 : parseInt(text);
}
/** Добавляет очки и возвращает новую сумму. */
export function points_add(player: Player, amount: number) {
const total = points_get(player) + amount;
saved.set(player.steamId, total.toString());
return total;
}
/** Звание игрока текстом. */
export function points_rank_name(player: Player) {
return points_get(player) >= 100 ? "Veteran" : "Rookie";
}
/** Сколько сейчас стоит убийство. */
export function points_multiplier(): Float {
return 1.5;
}
Сборка пишет include, который нужен плагину на Pawn, — points.inc для
points.ts — в dist/ проекта, с комментарием каждой функции над её
строкой. amxts build --deploy и amxts dev копируют его в
addons/amxmodx/scripting/include сервера:
/** Очки игрока; 0, если очков нет. */
native points_get(id);
/** Добавляет очки и возвращает новую сумму. */
native points_add(id, amount);
/** Звание игрока текстом. */
native points_rank_name(id, out[], len);
/** Сколько сейчас стоит убийство. */
native Float:points_multiplier();
#include <points>
public client_putinserver(id)
{
new rank[32];
points_rank_name(id, rank, charsmax(rank));
client_print(0, print_chat, "%d очков, %s", points_get(id), rank);
}
Нативы доступны плагинам на Pawn с самого начала карты. Функция при этом остаётся обычной функцией: плагин вызывает её напрямую.
Как передаются типы
| TypeScript | Pawn |
|---|---|
text: string | const text[], читается целиком, какой бы длины ни был |
count: number | count, целое число |
speed: Float | Float:speed |
flag: boolean | bool:flag |
values: number[] | const values[], values_size |
values: Float[] | const Float:values[], values_size |
origin: Vector | const Float:origin[3] |
player: Player | id, индекс игрока; см. ниже |
player?: Player | id = 0: 0 — сервер или все — приходит как null |
x = 5 | x = 5: значение по умолчанию попадает в include |
text?: string | const text[] = "": если не передали, функция получает "" |
возвращает string | out[], len в конце; натив возвращает записанную длину |
возвращает string | null | bool: и out[], len: true с текстом или false |
возвращает number[] или Float[] | out[], size в конце; натив возвращает, сколько записал |
возвращает number, boolean, Float | результат натива: число, bool:, Float: |
| ничего не возвращает | натив возвращает 0 |
Float для TypeScript — это number. Он помечает, какие числа Pawn видит
как Float:, — у параметра (speed: Float) или у результата
(points_multiplier(): Float). Это единственное место, где натив пишет тип
результата: целое и дробное — одно и то же number.
Player для Pawn — индекс игрока, в include он называется id, а функция
получает Player. Индекс, который не слот игрока, — 0, 33, -1, — до
функции не доходит: натив отвечает значением по умолчанию для своего
результата — 0, false, 0.0 или "", как ответил бы натив на Pawn с
if (id < 1 || id > MaxClients) return 0. Пустой слот до функции доходит:
проверить player.isConnected — её дело. С player?: Player 0 приходит как
null — для нативов, где 0 значит сервер или всех.
Текст в обе стороны — UTF-8. Результат-строка пишется в буфер
вызывающего до переданного им len (charsmax(out)) и никогда не
обрезается посреди буквы.
Правила
- Нативы — только собственные
export functionфайла плагина: неexport constи не реэкспорт.initи имена, начинающиеся с__, пропускаются. - Параметр или результат другого типа —
string[], объект,...args— останавливает сборку, и она называет функцию и параметр. - Натив принимает до 64 аргументов в том виде, в каком их передаёт Pawn:
массив — это два, сам массив и его размер, как и строковый результат —
буфер и его
len. Натив с большим числом останавливает сборку; передайте вместо них массив. - После
amxts_reloadнатив, который добавила перезагрузка, есть только у плагинов на Pawn, загруженных позже, — со следующей карты.
В тесте server.native(name, ...args) вызывает натив так, как его вызывает
Pawn (Тесты).
Свой вариант готового include: include
Плагин на TypeScript может заменить плагин на Pawn, которым уже пользуются другие плагины. Он называет include того плагина, и include решает, как каждый натив выглядит для Pawn:
// shop.inc - include, с которым собраны плагины на Pawn
native Float:shop_get_price(const item[]);
native shop_get_item_name(index, name[], len);
native bool:shop_buy(id, const item[]);
native shop_print(id, const fmt[], any:...);
native shop_get_stock(const item[], &count, &Float:price);
plugin({ name: "Shop", version: "1.0.0", author: "you", include: "shop.inc" });
interface Stock {
count: number;
price: number;
}
const items = ["armor", "hegrenade"];
const prices = [650, 300];
export function shop_get_price(item: string) {
const at = items.indexOf(item);
return at < 0 ? 0 : prices[at];
}
export function shop_get_item_name(index: number) {
return index >= 0 && index < items.length ? items[index] : "";
}
export function shop_buy(player: Player, item: string) {
return player.isAlive && items.includes(item);
}
export function shop_print(player: Player, text: string) { // текст приходит уже отформатированным
print(player, `!g[Магазин]!y ${text}`);
}
export function shop_get_stock(item: string) { // заполняет &count и &price
const at = items.indexOf(item);
if (at < 0) return null;
const stock: Stock = { count: 5, price: prices[at] };
return stock;
}
Функции остаются с обычными типами TypeScript — без Float, — а include
говорит, как передаётся каждый аргумент:
| В include | Функция |
|---|---|
Float:x, bool:x, Tag:x | x: number, читается как дробное, да/нет или целое |
id, игрок | player: Player или player?: Player, когда 0 значит всех |
TeamName:team | team: Team: "UNASSIGNED", "TERRORIST", "CT", "SPECTATOR" |
const x[] = "" | x?: string |
const x[] | x: string или x: number[] с размером после него |
x[], len, не const | выход: функция возвращает строку |
несколько выходов, &x | функция возвращает объект, поля которого заполняют их по порядку, или null — тогда натив возвращает 0 |
const fmt[], any:... | text: string, уже отформатированный, как это сделал бы format, вместе с %L |
any:... после других параметров | аргументы после фиксированных |
результат Array: | string[], number[], string[][], number[][] или объект из них |
| результат, который никто не читает | функция ничего не возвращает |
Include ищется рядом с плагином, в папке плагинов, в папке includes/
проекта и среди include сервера (include сервера).
Каждый объявленный в нём натив должен быть экспортирован: сборка назовёт
недостающий. Плагин может экспортировать и больше нативов, чем есть в
include; они добавляются после собственного текста include в тот include,
который пишет сборка, — под именем include, так что плагины на Pawn
оставляют свой #include <shop>.
Нативы напрямую
Когда в @amxts/core нет того, что нужно, вызовите натив. Каждый натив AMX Mod X,
ReAPI, других модулей, описанных в include, и других плагинов — функция в
@amxts/core/natives, под своим именем и с типами TypeScript:
import { get_member, get_user_info, rg_send_bartime2, user_slap } from "@amxts/core/natives";
import { m_rgAmmo } from "@amxts/core/constants";
server.addCommand("/plant", ({ player }) => plant(player));
function plant(player: Player) {
rg_send_bartime2(player.id, 10, 50.0); // Float: - это число
const model = get_user_info(player.id, "model"); // текст, который заполняет натив, - результат
const buckshot = get_member(player.id, m_rgAmmo, 5); // член-массив: элемент после него
user_slap(player.id, 5);
console.log(`${player.name}: ${model}, дробь ${buckshot}`);
}
| В include | Здесь |
|---|---|
Float:x | number |
bool:x | boolean |
const text[] | string |
out[], len, заполняет натив | возвращённая string |
Float:v[3] | number[] |
x = 5 | необязательный параметр с этим значением |
const fmt[], any:... | готовый текст: шаблонная строка |
any:... | ещё аргументы, каждый своего типа |
Там, где Pawn принимает id, натив принимает индекс игрока, player.id.
Константы из include (m_rgAmmo, EV_FL_health, …) лежат в @amxts/core/constants.
Нативы полей ReAPI — get_entvar и set_entvar, get_member и
set_member, get_member_game, get_pmove, … — читают и пишут поле тем,
что в нём лежит: дробное поле — это number, а не его биты. Векторному или
текстовому полю указывают его тип, а члену-массиву — элемент после поля:
set_entvar(box.id, var_gravity, 0.5);
const gravity = get_entvar(box.id, var_gravity); // 0.5
const origin = get_entvar<Vector>(box.id, var_origin); // Vector
const held = get_member<string>(player.id, m_szAnimExtention); // "knife"
const ammo = get_member(player.id, m_rgAmmo, 3); // элемент 3
set_member(player.id, m_rgAmmo, 90, 3);
Натив, чей include заканчивается на any:..., — engfunc, dllfunc, pev,
ExecuteHam, … — принимает ещё до двенадцати аргументов, каждый тем, что
он есть: число, логическое значение, текст, вектор ([x, y, z] или
Vector), Player. То, что натив возвращает через аргумент, — вектор,
который он заполняет, текст в ret[], len, число в &value, — попадает в
переданный массив или в Ref:
import { dllfunc, engfunc } from "@amxts/core/natives";
import { DLLFunc_ClientConnect, EngFunc_CreateFakeClient, EngFunc_PrecacheModel, EngFunc_TraceLine, EngFunc_VecToAngles, IGNORE_MONSTERS } from "@amxts/core/constants";
const model = engfunc(EngFunc_PrecacheModel, "models/myplugin/box.mdl");
const id = engfunc(EngFunc_CreateFakeClient, "Dummy");
engfunc(EngFunc_TraceLine, [0, 0, 64], player.origin, IGNORE_MONSTERS, player.id);
const angles = [0, 0, 0];
engfunc(EngFunc_VecToAngles, [1, 1, 0], angles); // теперь angles — [0, 45, 0]
const reason = new Ref(""); // текст, который пишет натив
if (!dllfunc(DLLFunc_ClientConnect, id, "Dummy", "127.0.0.1", reason)) console.log(reason.value);
Число в таком хвосте уходит как Float: там, где include говорит, что натив
читает его так: engfunc и dllfunc — по функции движка (скорости
EngFunc_RunPlayerMove), ExecuteHam — по функции Ham, pev, set_pev
и global_get — по полю, get_tr2, get_es, get_uc, get_cd и их пары
set_ — по члену, forward_return — с FMV_FLOAT. В остальных — целое число.
Если это есть в @amxts/core, берите его — player.health, а не
get_user_health(player.id), — а натив — если нет. Редактор знает каждый
натив: наведите на него, чтобы увидеть комментарий из include.
Текст обрезают AMX Mod X и игра, а не путь до них. Натив получает строку целиком, до 16383 байт — столько AMX Mod X читает, и пишет в буфер не больше 16384 ячеек, какой бы длины он ни был. Натив с
const fmt[], any:...
(client_print, server_print, format, …) форматирует строку не длиннее
4095 байт, а server_print печатает из них 254. Игра показывает около 190
байт строки чата — примерно 95 букв кириллицы — и 126 байт строки консоли,
отправленной через client_print.::: warning Обратный вызов по имени
Натив, который берёт обратный вызов по имени паблика, — register_think,
set_native_filter — принимает имя, которое даёт publicFor(handler, key):
key — любое имя, уникальное в плагине. Такую регистрацию не отменить, и
она переживает перезагрузку, поэтому пустое имя значит, что она уже сделана:
import { publicFor } from "@amxts/core/kit";
import { register_think } from "@amxts/core/natives";
const pub = publicFor(onThink, "think:myplugin_box");
if (pub.length > 0) register_think("myplugin_box", pub);
:::
Свой модуль
Модуль — код на TypeScript, которым пользуются плагины проекта, — greeter.greet(player), без строки импорта, по имени, которое модуль даёт себе сам. То, что он экспортирует, — функции и типы — и есть его API. Модуль говорит о себе через defineModule, проект перечисляет его в amxts.config.ts, а на сервере работает один его экземпляр. menu-core и config-core — модули. Код без своего состояния на сервере — клиент какого-то сервиса, помощники — может быть и библиотекой: она компилируется в каждый плагин, который её импортирует.
Тесты
Тест загружает настоящий код плагина — собранный тем же AssemblyScript, с тем же API и теми же нативами, что и сборка для сервера, — в поддельный сервер на TypeScript и управляет им так, как это делали бы игроки и игра. Библиотека — @amxts/core/test-utils, она входит в ядро: