Natives
A native is a function one AMX Mod X plugin or module offers the others.
Natives go both ways here: a TypeScript plugin exports its own for Pawn
plugins, and calls the natives of AMX Mod X, ReAPI and other modules where
@amxts/core has nothing of its own.
Exporting natives: export function
Every export function of a plugin's file is a native under the same name,
which a Pawn plugin can call:
plugin({ name: "Points", version: "1.0.0", author: "you" });
const saved = new Storage("myplugin_points");
/** The player's points; 0 for a player who has none. */
export function points_get(player: Player) {
const text = saved.get(player.steamId);
return text == null ? 0 : parseInt(text);
}
/** Adds points and returns the new total. */
export function points_add(player: Player, amount: number) {
const total = points_get(player) + amount;
saved.set(player.steamId, total.toString());
return total;
}
/** The player's rank, as text. */
export function points_rank_name(player: Player) {
return points_get(player) >= 100 ? "Veteran" : "Rookie";
}
/** How much a kill is worth now. */
export function points_multiplier(): Float {
return 1.5;
}
The build writes the include a Pawn plugin needs - points.inc for
points.ts - into the project's dist/, with each function's comment above
its line. amxts build --deploy and amxts dev copy it into the server's
addons/amxmodx/scripting/include:
/** The player's points; 0 for a player who has none. */
native points_get(id);
/** Adds points and returns the new total. */
native points_add(id, amount);
/** The player's rank, as text. */
native points_rank_name(id, out[], len);
/** How much a kill is worth now. */
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 points, %s", points_get(id), rank);
}
The natives are there for Pawn plugins from the start of the map. A function stays an ordinary function as well: the plugin calls it directly.
How types cross
| TypeScript | Pawn |
|---|---|
text: string | const text[], read whole, however long |
count: number | count, a whole number |
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, a player's index; see below |
player?: Player | id = 0: 0 - the server or everyone - arrives as null |
x = 5 | x = 5: the default goes into the include |
text?: string | const text[] = "": left out, the function gets "" |
returns string | out[], len at the end; the native returns the length written |
returns string | null | bool:, and out[], len: true with the text, or false |
returns number[] or Float[] | out[], size at the end; the native returns how many it wrote |
returns number, boolean, Float | the native's result: a number, bool:, Float: |
| returns nothing | the native returns 0 |
Float is number to TypeScript. It marks which numbers Pawn sees as
Float:, on a parameter (speed: Float) or on the result
(points_multiplier(): Float) - the one place a native writes its return
type, since a whole number and a fraction are the same number.
Player is a player's index to Pawn, called id in the include, and the
function gets the Player. An index that is not a player slot - 0, 33,
-1 - never reaches the function: the native answers its result's default,
0, false, 0.0 or "", as a Pawn native's
if (id < 1 || id > MaxClients) return 0 would. An empty slot does reach it:
player.isConnected is the function's to check. With player?: Player,
0 arrives as null, for natives where 0 means the server or everyone.
Text is UTF-8 both ways. A string result is written into the caller's
buffer up to the len it passes (charsmax(out)), never cut in the middle
of a letter.
The rules
- Only the plugin file's own
export functions are natives - notexport const, not a re-export.initand names starting with__are left out. - A parameter or a result of another type -
string[], an object,...args- stops the build, naming the function and the parameter. - A native takes up to 64 arguments as Pawn passes them: an array is two, the
array and its size, and so is a string result, the buffer and its
len. One with more stops the build; pass an array instead. - After
amxts_reload, a native the reload added is there only for Pawn plugins that load later - from the next map.
In a test, server.native(name, ...args) calls a native the way Pawn does
(Testing).
Implementing an existing include: include
A TypeScript plugin can stand in for a Pawn plugin that other plugins already use. It names that plugin's include, and the include decides how every native looks to Pawn:
// shop.inc, the include Pawn plugins are compiled against
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) { // the text arrives formatted
print(player, `!g[Shop]!y ${text}`);
}
export function shop_get_stock(item: string) { // fills &count and &price
const at = items.indexOf(item);
if (at < 0) return null;
const stock: Stock = { count: 5, price: prices[at] };
return stock;
}
The functions keep plain TypeScript types - no Float - and the include says
how each argument crosses:
| In the include | The function |
|---|---|
Float:x, bool:x, Tag:x | x: number, read as a fraction, a yes/no or a whole number |
id, a player | player: Player, or player?: Player when 0 means everyone |
TeamName:team | team: Team: "UNASSIGNED", "TERRORIST", "CT", "SPECTATOR" |
const x[] = "" | x?: string |
const x[] | x: string, or x: number[] with the size after it |
x[], len, not const | an output: the function returns a string |
several outputs, &x | the function returns an object whose fields fill them in order, or null - the native then returns 0 |
const fmt[], any:... | text: string, already formatted as format would, %L included |
any:... after other parameters | the arguments after the fixed ones |
an Array: result | a string[], number[], string[][], number[][], or an object of those |
| a result nobody reads | the function returns nothing |
The include is looked for beside the plugin, in the plugins folder, in the
project's includes/ folder, and among the includes of the server
(the server's includes). Every native it
declares must be exported: the build names the one that is missing. The
plugin may export more natives than the include has; they are added after
the include's own text in the include the build writes, under the include's
name, so Pawn plugins keep their #include <shop>.
Calling natives directly
When @amxts/core has nothing for what you need, call the native. Every native
of AMX Mod X, ReAPI, the other modules the includes describe and other
plugins is a function in @amxts/core/natives, under its own name, with TypeScript
types:
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); // a Float: is a number
const model = get_user_info(player.id, "model"); // the text the native fills is the result
const buckshot = get_member(player.id, m_rgAmmo, 5); // an array member: the element after it
user_slap(player.id, 5);
console.log(`${player.name}: ${model}, buckshot ${buckshot}`);
}
| In the include | Here |
|---|---|
Float:x | number |
bool:x | boolean |
const text[] | string |
out[], len, filled by the native | the returned string |
Float:v[3] | number[] |
x = 5 | an optional parameter with that default |
const fmt[], any:... | ready text: a template string |
any:... | more arguments, each of its own type |
A native takes a player's index, player.id, where Pawn takes id. The
constants of the includes (m_rgAmmo, EV_FL_health, …) are in @amxts/core/constants.
ReAPI's field natives - get_entvar and set_entvar, get_member and
set_member, get_member_game, get_pmove, … - read and write a field as
what it holds: a float field is a number, not its bits. A vector or a text
field names its type, and an array member takes its element after the field:
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); // a Vector
const held = get_member<string>(player.id, m_szAnimExtention); // "knife"
const ammo = get_member(player.id, m_rgAmmo, 3); // the element at 3
set_member(player.id, m_rgAmmo, 90, 3);
A native whose include ends in any:... - engfunc, dllfunc, pev,
ExecuteHam, … - takes up to twelve more arguments, each as what it is: a
number, a boolean, text, a vector ([x, y, z] or a Vector), a Player.
What the native writes back - a vector it fills, text into ret[], len, a
number into &value - lands in the array you gave or in a 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 is [0, 45, 0] now
const reason = new Ref(""); // text the native writes
if (!dllfunc(DLLFunc_ClientConnect, id, "Dummy", "127.0.0.1", reason)) console.log(reason.value);
A number in such a tail is sent as a Float: where the include says the
native reads one there: engfunc and dllfunc by the engine's function
(EngFunc_RunPlayerMove's speeds), ExecuteHam by the Ham function,
pev, set_pev and global_get by the field, get_tr2, get_es, get_uc,
get_cd and their set_ pairs by the member, forward_return with
FMV_FLOAT. Elsewhere it is a whole number.
Use @amxts/core when it has the thing - player.health, not
get_user_health(player.id) - and the native when it does not. The editor
knows every native: hover one for its include's comment.
Text is cut by AMX Mod X and the game, not on the way to them. A native gets a string whole, up to the 16383 bytes AMX Mod X reads, and writes at most 16384 cells into a buffer, however long it is. A native that takes
const fmt[], any:... (client_print, server_print, format, …) formats
a line of at most 4095 bytes, and server_print prints 254 of them. The game
shows about 190 bytes of a chat line - about 95 Cyrillic letters - and 126 of
a console line sent with client_print.::: warning A callback by name
A native that takes a callback by the name of a public - register_think,
set_native_filter - takes the name publicFor(handler, key) gives: key
is any name unique in the plugin. Such a registration cannot be undone and
outlives a reload, so an empty name means it is already made:
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);
:::
Creating a module
A module is TypeScript code the plugins of a project use - greeter.greet(player), without an import line, by the name the module gives itself. What it exports - its functions and types - is its API. It says what it is with defineModule, a project lists it in amxts.config.ts, and the server runs one instance of it. menu-core and config-core are modules. Code without state of its own on the server - a client for some service, helpers - can instead be a library: compiled into each plugin that imports it.
Testing
A test loads the plugin's real code — compiled by the same AssemblyScript, against the same API and natives as the server build — into a fake server written in TypeScript, and drives it the way players and the game would. The library is @amxts/core/test-utils, part of the core: