Pawn

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

TypeScriptPawn
text: stringconst text[], read whole, however long
count: numbercount, a whole number
speed: FloatFloat:speed
flag: booleanbool:flag
values: number[]const values[], values_size
values: Float[]const Float:values[], values_size
origin: Vectorconst Float:origin[3]
player: Playerid, a player's index; see below
player?: Playerid = 0: 0 - the server or everyone - arrives as null
x = 5x = 5: the default goes into the include
text?: stringconst text[] = "": left out, the function gets ""
returns stringout[], len at the end; the native returns the length written
returns string | nullbool:, 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, Floatthe native's result: a number, bool:, Float:
returns nothingthe 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 - not export const, not a re-export. init and 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 includeThe function
Float:x, bool:x, Tag:xx: number, read as a fraction, a yes/no or a whole number
id, a playerplayer: Player, or player?: Player when 0 means everyone
TeamName:teamteam: Team: "UNASSIGNED", "TERRORIST", "CT", "SPECTATOR"
const x[] = ""x?: string
const x[]x: string, or x: number[] with the size after it
x[], len, not constan output: the function returns a string
several outputs, &xthe 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 parametersthe arguments after the fixed ones
an Array: resulta string[], number[], string[][], number[][], or an object of those
a result nobody readsthe 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 includeHere
Float:xnumber
bool:xboolean
const text[]string
out[], len, filled by the nativethe returned string
Float:v[3]number[]
x = 5an 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 into a native
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
);

:::