@amxts/core 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +21 -25
  2. package/README.ru.md +22 -24
  3. package/as/amxts.d.ts +63 -3
  4. package/as/constants.ts +684 -116
  5. package/as/entities.ts +1905 -1364
  6. package/as/events.ts +1696 -907
  7. package/as/facade.ts +1240 -220
  8. package/as/fetch.ts +1519 -0
  9. package/as/flags.ts +159 -145
  10. package/as/hlds.ts +1071 -0
  11. package/as/hooks.ts +5226 -5379
  12. package/as/kit.ts +15 -5
  13. package/as/lib/check.ts +1 -1
  14. package/as/natives.ts +2658 -2499
  15. package/as/remote.ts +39 -15
  16. package/as/tsconfig.json +7 -2
  17. package/as/vector.ts +12 -9
  18. package/includes/order.txt +22 -29
  19. package/includes/sources.json +0 -8
  20. package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
  21. package/package.json +29 -7
  22. package/runtime/deps/assemblyscript/dist/asc.js +86 -16
  23. package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
  24. package/runtime/licenses/MPL-2.0.txt +373 -0
  25. package/runtime/licenses/README.md +5 -1
  26. package/runtime/licenses/curl-COPYING +22 -0
  27. package/runtime/licenses/libssh2-COPYING +43 -0
  28. package/runtime/licenses/mbedTLS-LICENSE +553 -0
  29. package/runtime/natives.txt +108 -105
  30. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
  31. package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
  32. package/scripts/apply-docs.ts +2 -2
  33. package/scripts/auto-imports.ts +2 -2
  34. package/scripts/build-identity.ts +115 -0
  35. package/scripts/build-wasm.ts +101 -50
  36. package/scripts/check.ts +17 -3
  37. package/scripts/client-messages.ts +246 -0
  38. package/scripts/compile-cache.ts +4 -2
  39. package/scripts/compile-pool.ts +128 -0
  40. package/scripts/compile-worker.ts +23 -0
  41. package/scripts/compile.ts +156 -59
  42. package/scripts/docker-server.ts +13 -3
  43. package/scripts/docs/as/amxts.ts +86 -0
  44. package/scripts/docs/as/facade.ts +688 -188
  45. package/scripts/docs/as/fetch.ts +805 -0
  46. package/scripts/docs/entities.ts +43 -144
  47. package/scripts/docs/events.ts +17 -28
  48. package/scripts/docs/game.ts +68 -22
  49. package/scripts/docs/messages.ts +330 -16
  50. package/scripts/editor-api.ts +101 -0
  51. package/scripts/hlds-events.ts +370 -0
  52. package/scripts/includes.ts +12 -10
  53. package/scripts/player-fields.ts +16 -15
  54. package/scripts/prebuilt.ts +65 -23
  55. package/scripts/prepare.ts +42 -28
  56. package/scripts/project.ts +120 -20
  57. package/scripts/reapi-events.ts +98 -0
  58. package/scripts/shared-modules.ts +85 -23
  59. package/scripts/source-map.ts +242 -0
  60. package/scripts/system.ts +1 -1
  61. package/scripts/typed-commands.ts +263 -0
  62. package/scripts/typed-configs.ts +23 -12
  63. package/scripts/ui.ts +3 -1
  64. package/scripts/upgrade-menus.ts +274 -0
  65. package/scripts/upgrade-names.ts +384 -0
  66. package/scripts/upgrade.ts +857 -0
  67. package/src/cli-api.mjs +37 -4
  68. package/src/system.mjs +74 -3
  69. package/src/testing/compile.ts +8 -5
  70. package/src/testing/index.ts +2 -1
  71. package/src/testing/natives.ts +199 -5
  72. package/src/testing/network.ts +531 -0
  73. package/src/testing/server.ts +572 -48
  74. package/src/testing/setup.ts +1 -1
  75. package/src/testing/tables.ts +25 -0
  76. package/as/modules/http.ts +0 -184
  77. package/scripts/docs/as/modules/http.ts +0 -66
package/as/facade.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // includes/*.inc as Pawn sees it, and the bridge natives that
6
6
  // runtime/src/module.cpp registers by hand for the places where a string
7
7
  // crossing as UTF-16 is worth a purpose-built thunk. Both are direct calls —
8
- // an import whose signature wamrc knows costs 2 ns.
8
+ // an import whose signature wamrc knows is far cheaper than its generic call.
9
9
  //
10
10
  // The `// @ts-ignore: decorator` lines are AssemblyScript's own idiom, used
11
11
  // throughout its standard library: an editor reading this as TypeScript would
@@ -32,7 +32,9 @@ import {
32
32
  set_dhudmessage, show_dhudmessage, CreateHudSyncObj, ShowSyncHudMsg, ClearSyncHud,
33
33
  precache_model, precache_sound, precache_generic, query_client_cvar, register_touch,
34
34
  register_message, get_msg_args, get_msg_argtype, get_msg_arg_int, get_msg_arg_float, get_msg_arg_string,
35
- set_msg_arg_int, set_msg_arg_float, set_msg_arg_string
35
+ set_msg_arg_int, set_msg_arg_float, set_msg_arg_string, get_user_userid,
36
+ emessage_begin, ewrite_byte, ewrite_short, ewrite_string, emessage_end, elog_message,
37
+ has_reunion, REU_GetAuthtype, REU_GetProtocol, REU_GetAuthKey, DisableHamForward, EnableHamForward
36
38
  } from "./natives";
37
39
  // Promise, async/await and AbortSignal, as the globals they are in JavaScript.
38
40
  import "./promise";
@@ -61,6 +63,8 @@ import { Vector } from "./vector";
61
63
  // @ts-ignore: decorator
62
64
  @external("env", "slot") declare function _slot(fn: i32, shape: i32, key: string, fallback: i32): i32;
63
65
  // @ts-ignore: decorator
66
+ @external("env", "slot_on") declare function _slotOn(slot: i32, on: i32): void;
67
+ // @ts-ignore: decorator
64
68
  @external("env", "arg") declare function _arg(index: i32): i32;
65
69
  // @ts-ignore: decorator
66
70
  @external("env", "arg_text") declare function _argText(index: i32, out: i32, max: i32): i32;
@@ -74,6 +78,34 @@ import { Vector } from "./vector";
74
78
  @external("env", "argc") declare function _argc(): i32;
75
79
  // @ts-ignore: decorator
76
80
  @external("env", "plugin") declare function _plugin(name: string, version: string, author: string, description: string): void;
81
+ // @ts-ignore: decorator
82
+ @external("env", "stack_frames") declare function _stackFrames(out: usize, max: i32): i32;
83
+ // @ts-ignore: decorator
84
+ @external("env", "stack_text") declare function _stackText(frames: usize, count: i32, out: usize, max: i32): i32;
85
+
86
+ /** The most calls an error's `stack` names. */
87
+ const STACK_FRAMES = 32;
88
+
89
+ /**
90
+ * An error's `stack`, from the module: the frames of the calls that made the
91
+ * error - each function's index and where in it the call is - kept when it
92
+ * is made, and put into words through the plugin's map only when `stack` is
93
+ * read.
94
+ */
95
+ class ModuleStack extends ErrorStack {
96
+ frames(): StaticArray<u32> {
97
+ const frames = new StaticArray<u32>(STACK_FRAMES * 2);
98
+ const count = _stackFrames(changetype<usize>(frames), STACK_FRAMES);
99
+ return StaticArray.slice<u32>(frames, 0, count * 2);
100
+ }
101
+
102
+ text(frames: StaticArray<u32>): string {
103
+ const length = _stackText(changetype<usize>(frames), frames.length / 2, changetype<usize>(stackText), stackText.length);
104
+ return String.UTF8.decodeUnsafe(changetype<usize>(stackText), length);
105
+ }
106
+ }
107
+ const stackText = new StaticArray<u8>(8192);
108
+ __errorStack = new ModuleStack();
77
109
 
78
110
  // call_indirect checks the type on the module's side, so a handler's shape is
79
111
  // fixed at registration and there are exactly two: this one and WideHandler.
@@ -191,14 +223,22 @@ export function ret(value: number): void {
191
223
  * unique within the plugin. `fallback` is the answer when the handler returns
192
224
  * nothing: `0` for most natives, `1` where the native expects the event handled.
193
225
  *
194
- * Register from the `"cfg"` event, not at the top of the file: a console
226
+ * Register from the `"pluginsLoaded"` event, not at the top of the file: a console
195
227
  * command registered that early (`register_concmd`, `register_srvcmd`) crashes
196
228
  * the server when typed.
197
229
  */
198
230
  export function publicFor(handler: WideHandler, key: string, fallback: number = 0): string {
199
- // Must match SLOT_REUSED in runtime/src/module.cpp.
200
- const REUSED: i32 = 0x10000;
231
+ return __switchedPublic(handler, key, null, fallback);
232
+ }
201
233
 
234
+ // Must match SLOT_REUSED in runtime/src/module.cpp.
235
+ const SLOT_REUSED: i32 = 0x10000;
236
+
237
+ /**
238
+ * @hidden `publicFor`, the public switched by `hook`: switched off, a call
239
+ * answers `fallback` in the module, without reaching the plugin.
240
+ */
241
+ export function __switchedPublic(handler: WideHandler, key: string, hook: __Switch | null, fallback: number = 0): string {
202
242
  const slot = _slot(hostIndex(handler, true), SHAPE_WIDE, key, fallback);
203
243
 
204
244
  if (slot < 0) {
@@ -206,7 +246,33 @@ export function publicFor(handler: WideHandler, key: string, fallback: number =
206
246
  return "";
207
247
  }
208
248
 
209
- return (slot & REUSED) ? "" : `__amxts_cb${slot}`;
249
+ const index = slot & ~SLOT_REUSED;
250
+ if (hook != null) hook.add((on: bool): void => _slotOn(index, on ? 1 : 0));
251
+ return (slot & SLOT_REUSED) ? "" : `__amxts_cb${slot}`;
252
+ }
253
+
254
+ /**
255
+ * @hidden A registration with AMX Mod X - a hook, or a public it calls - that
256
+ * is switched off while what it delivers has no listener and back on for the
257
+ * next one, so that an event nobody listens to does not reach the plugin.
258
+ * One switched off before it is made is made switched off.
259
+ */
260
+ export class __Switch {
261
+ private on: bool = true;
262
+ private parts: ((on: bool) => void)[] = [];
263
+
264
+ /** Switches every registration of it on or off. */
265
+ set(on: bool): void {
266
+ if (on == this.on) return;
267
+ this.on = on;
268
+ for (let i = 0; i < this.parts.length; i++) this.parts[i](on);
269
+ }
270
+
271
+ /** What switches one registration, once it is made; run at once while the switch is off. */
272
+ add(part: (on: bool) => void): void {
273
+ this.parts.push(part);
274
+ if (!this.on) part(false);
275
+ }
210
276
  }
211
277
 
212
278
  // AMX Mod X implements a native as a public in some plugin, so this names the
@@ -350,6 +416,18 @@ export function __nativeVector(index: i32): Vector {
350
416
  return new Vector(pawnFloat(cells[0]), pawnFloat(cells[1]), pawnFloat(cells[2]));
351
417
  }
352
418
 
419
+ /**
420
+ * @hidden Writes a `Float:v[3]` argument where it lies: a hookchain's vector,
421
+ * which reapi copies back into the game's once the listener returns.
422
+ */
423
+ export function __setNativeVector(index: i32, value: Vector): void {
424
+ const cells = new StaticArray<i32>(3);
425
+ unchecked(cells[0] = reinterpret<i32>(<f32>value.x));
426
+ unchecked(cells[1] = reinterpret<i32>(<f32>value.y));
427
+ unchecked(cells[2] = reinterpret<i32>(<f32>value.z));
428
+ _setArgArray(index, changetype<usize>(cells), 3);
429
+ }
430
+
353
431
  // @ts-ignore: decorator
354
432
  @external("env", "arg_length") declare function _argLength(index: i32): i32;
355
433
 
@@ -890,7 +968,7 @@ export function argString(pointer: number): string {
890
968
  * zero cell or `max` cells. AMX Mod X, the engine and the game keep text as
891
969
  * bytes - get_amxstring takes the low byte of each cell - so a letter outside
892
970
  * ASCII is two or three cells, and a cell per UTF-16 unit came out as
893
- * mojibake one way and a truncated byte the other ("раз" arrived as "@0").
971
+ * mojibake one way and a truncated byte the other (a Cyrillic word arrived as "@0").
894
972
  */
895
973
  export function __cellText(pointer: usize, max: i32): string {
896
974
  let length = 0;
@@ -925,7 +1003,7 @@ export function __cellsOf(text: string): StaticArray<i32> {
925
1003
  return cells;
926
1004
  }
927
1005
 
928
- /** Reads the text a raw native from `~/natives` wrote into a cell array. `stringToCells` is the other way. */
1006
+ /** Reads the text a raw native from `@amxts/core/natives` wrote into a cell array. `stringToCells` is the other way. */
929
1007
  export function cellsToString(cells: StaticArray<i32>): string {
930
1008
  return __cellText(changetype<usize>(cells), cells.length);
931
1009
  }
@@ -935,10 +1013,10 @@ export function stringToCells(text: string, cells: StaticArray<i32>): void {
935
1013
  __writeCellText(text, cells);
936
1014
  }
937
1015
 
938
- // The natives in ~/natives take addresses, because that is what Pawn pushes: a
1016
+ // The natives in @amxts/core/natives take addresses, because that is what Pawn pushes: a
939
1017
  // string has to be in memory this plugin owns before its address means anything.
940
1018
  /**
941
- * Passes a string to a raw native from `~/natives` without declaring a
1019
+ * Passes a string to a raw native from `@amxts/core/natives` without declaring a
942
1020
  * buffer for it:
943
1021
  *
944
1022
  * cfg_set_base_dir(cells("myplugin"));
@@ -1107,6 +1185,25 @@ export type Team = "TERRORIST" | "CT" | "SPECTATOR" | "UNASSIGNED";
1107
1185
 
1108
1186
  const TEAM_NAMES: Team[] = ["UNASSIGNED", "TERRORIST", "CT", "SPECTATOR"];
1109
1187
 
1188
+ /** The way a player's game proves who he is, as Reunion tells it, e.g. `"steam"` or `"revEmu"`; `"unknown"` on a server without Reunion. */
1189
+ export type AuthType =
1190
+ | "unknown" | "steam" | "steamEmu" | "revEmu" | "revEmu2013" | "oldRevEmu"
1191
+ | "sc2009" | "avsmp" | "sxei" | "sse3" | "dproto" | "hltv";
1192
+
1193
+ // Reunion's client_auth_type, by its number: CA_TYPE_NONE is "unknown".
1194
+ const AUTH_TYPES: AuthType[] = [
1195
+ "unknown", "dproto", "steam", "steamEmu", "revEmu", "oldRevEmu",
1196
+ "hltv", "sc2009", "avsmp", "sxei", "revEmu2013", "sse3",
1197
+ ];
1198
+
1199
+ // Reunion's natives exist only when reapi finds Reunion: asked once, as reapi is.
1200
+ let reunionHere: i32 = -1;
1201
+
1202
+ function hasReunion(): bool {
1203
+ if (reunionHere < 0) reunionHere = __hasReapi() && has_reunion() ? 1 : 0;
1204
+ return reunionHere == 1;
1205
+ }
1206
+
1110
1207
  /** A weapon a player can hold, by its class name, e.g. `"weapon_ak47"` or `"weapon_knife"`. */
1111
1208
  export type WeaponName =
1112
1209
  | "weapon_p228" | "weapon_scout" | "weapon_hegrenade" | "weapon_xm1014" | "weapon_c4"
@@ -1138,21 +1235,9 @@ const WEAPON_IDS = [
1138
1235
  "weapon_sg552", "weapon_ak47", "weapon_knife", "weapon_p90"
1139
1236
  ];
1140
1237
 
1141
- /**
1142
- * The filter of `Player.all`; every field is optional, e.g.
1143
- * `Player.all({ alive: true, team: "CT" })`.
1144
- */
1145
- export interface PlayerFilter {
1146
- /** Only living players. */
1147
- alive?: boolean;
1148
- /** Only dead players. */
1149
- dead?: boolean;
1150
- /** Only players of this team, e.g. `"CT"`. */
1151
- team?: Team;
1152
- /** Only bots. */
1153
- bots?: boolean;
1154
- /** Only people, no bots. */
1155
- humans?: boolean;
1238
+ /** @hidden Every weapon's class name: what Ham Sandwich hooks a weapon's event on for "every weapon". */
1239
+ export function __weaponClassnames(): string[] {
1240
+ return WEAPON_IDS.filter((name) => name.length > 0);
1156
1241
  }
1157
1242
 
1158
1243
  /** An AMX Mod X module a plugin can check for, one of `"reapi"`, `"cstrike"`, `"fun"`, `"hamsandwich"`, `"engine"`, `"fakemeta"`. */
@@ -1178,6 +1263,30 @@ export function hasModule(name: ModuleName): boolean {
1178
1263
  return loaded;
1179
1264
  }
1180
1265
 
1266
+ // The backend the hood picks for a game event, an action or a field of
1267
+ // ReGameDLL's own, asked once rather than on every call: 1 reapi, 0 the
1268
+ // stock modules, -1 not asked yet.
1269
+ let reapiHere: i32 = -1;
1270
+
1271
+ /** @hidden Whether the server has reapi: the hood's choice of backend, made once. */
1272
+ export function __hasReapi(): bool {
1273
+ if (reapiHere < 0) reapiHere = hasModule("reapi") ? 1 : 0;
1274
+ return reapiHere == 1;
1275
+ }
1276
+
1277
+ const saidOnce = new Set<string>();
1278
+
1279
+ /**
1280
+ * @hidden A line in the console the first time it is said: what this server
1281
+ * cannot do, where a plugin asks for it - not on every call, which a field
1282
+ * read in a frame listener makes every frame.
1283
+ */
1284
+ export function __sayOnce(text: string): void {
1285
+ if (saidOnce.has(text)) return;
1286
+ saidOnce.add(text);
1287
+ console.warn(text);
1288
+ }
1289
+
1181
1290
  /** Tells everyone's scoreboard a player's frags and deaths - what cs_set_user_deaths sends. */
1182
1291
  function sendScoreInfo(id: number, frags: number, deaths: number, team: number) {
1183
1292
  message_begin(MSG_ALL, get_user_msgid("ScoreInfo"), [0, 0, 0], 0);
@@ -1195,6 +1304,38 @@ export interface KillOptions {
1195
1304
  keepFrags?: boolean;
1196
1305
  }
1197
1306
 
1307
+ // ---------------------------------------------------------------- bots
1308
+
1309
+ import { dllfunc, engfunc, global_get, set_pev } from "./natives";
1310
+ import { DLLFunc_ClientConnect, DLLFunc_ClientPutInServer, EngFunc_CreateFakeClient, EngFunc_RunPlayerMove, glb_frametime, pev_health } from "./constants";
1311
+ import { BUTTON, Button } from "./flags";
1312
+
1313
+ /** How long the server's current frame lasts, in seconds. */
1314
+ function frameTime(): f64 {
1315
+ const time = new Ref<f64>(0.0);
1316
+ global_get(glb_frametime, time);
1317
+ return time.value;
1318
+ }
1319
+
1320
+ /**
1321
+ * One move of a bot, `bot.move({ ... })`: the speeds are units a second, as
1322
+ * a player's keys give them - `250` runs with a knife, `-250` backs away.
1323
+ */
1324
+ export interface MoveOptions {
1325
+ /** Forward, or back when negative. */
1326
+ forward?: number;
1327
+ /** To the right, or to the left when negative. */
1328
+ side?: number;
1329
+ /** Up, or down when negative: swimming and climbing a ladder. */
1330
+ up?: number;
1331
+ /** The buttons held during the move: `["jump", "duck"]`. */
1332
+ buttons?: Button[];
1333
+ /** The direction the bot looks in, `[pitch, yaw, roll]` or a Vector; where it looks now when left out. */
1334
+ angles?: number[];
1335
+ /** The move's length in milliseconds, `1` to `255`; the server frame's time when left out. */
1336
+ msec?: number;
1337
+ }
1338
+
1198
1339
  // Plugins add fields of their own to this class, shared by every plugin and by
1199
1340
  // Pawn, with `interface Player { spawnProtected: boolean }` in a
1200
1341
  // `declare module "~/facade"` block (docs/en/3.game/02.players.md) - not written
@@ -1202,12 +1343,12 @@ export interface KillOptions {
1202
1343
  // plugin's. The build turns each into a getter and a setter over the module's
1203
1344
  // store and adds them here (scripts/player-fields.ts).
1204
1345
  /**
1205
- * A connecting player, in `"connect"`, `"authorized"` and `"putinserver"`: name,
1346
+ * A connecting player, in `"connect"`, `"authorized"` and `"putInServer"`: name,
1206
1347
  * address, SteamID and team, but no health or weapons yet. Every Player is a
1207
1348
  * Client too.
1208
1349
  *
1209
1350
  * ```ts
1210
- * server.addEventListener("putinserver", (event) => {
1351
+ * server.addEventListener("putInServer", (event) => {
1211
1352
  * print(event.player, `Welcome, ${event.player.name}!`);
1212
1353
  * });
1213
1354
  * ```
@@ -1219,13 +1360,19 @@ export interface Client {
1219
1360
  readonly name: string;
1220
1361
  /** The player's IP address without the port, e.g. `"192.168.0.10"`. */
1221
1362
  readonly ip: string;
1222
- /** The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`. */
1223
- readonly authid: string;
1363
+ /** The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`. With Reunion a game without Steam gets one made from its key (`authKey`): `"STEAM_..."` or `"VALVE_..."`, as the server's Reunion settings say. */
1364
+ readonly steamId: string;
1365
+ /** The way the player's game proved who he is, as Reunion tells it: one of `"steam"` (a Steam game), `"steamEmu"`, `"revEmu"`, `"revEmu2013"`, `"oldRevEmu"`, `"sc2009"`, `"avsmp"`, `"sxei"`, `"sse3"` (a game without Steam, by the emulator it proved itself with), `"dproto"`, `"hltv"`, or `"unknown"` on a server without Reunion. */
1366
+ readonly authType: AuthType;
1367
+ /** The network protocol of the player's game: `48` for today's game, `47` for an old one Reunion lets in. `0` on a server without Reunion. */
1368
+ readonly protocol: number;
1369
+ /** The key the player's game proved itself with, as Reunion read it: what his SteamID is made from. `""` on a server without Reunion. */
1370
+ readonly authKey: string;
1224
1371
  /** `true` for a bot. */
1225
1372
  readonly isBot: boolean;
1226
1373
  /** `true` while the player is on the server. */
1227
1374
  readonly isConnected: boolean;
1228
- /** The player's admin rights, from the letters in `users.ini`: `client.access.includes("Cvar")`. */
1375
+ /** The player's admin rights, from the letters in `users.ini`: `client.access.includes("cvar")`. */
1229
1376
  readonly access: Access[];
1230
1377
  /** The player's team, one of `"TERRORIST"`, `"CT"`, `"SPECTATOR"` or `"UNASSIGNED"` (until the player joins a team). Setting it moves the player, as `player.team` does. */
1231
1378
  team: Team;
@@ -1241,6 +1388,8 @@ export interface Client {
1241
1388
  readonly signal: AbortSignal;
1242
1389
  /** Runs a command in the player's console, as if he had typed it: `client.command("stop")`. */
1243
1390
  command(text: string): void;
1391
+ /** Kicks the player off the server, with the reason he is shown: `client.kick("Spam")`. */
1392
+ kick(reason?: string): void;
1244
1393
  /** Joins a side as the game joins a player who picks it in the team menu: `client.joinTeam("CT")`. `false` if the game refused. */
1245
1394
  joinTeam(team: Team): boolean;
1246
1395
  /** Asks the player's game for one of its cvars: `await client.queryCvar("fps_max")`, the value as text, or `null` when his game has none. */
@@ -1251,7 +1400,7 @@ export interface Client {
1251
1400
  * A player in the game: everything a Client has, plus health, armor, frags,
1252
1401
  * weapons and the screen.
1253
1402
  *
1254
- * An event about a player gives one as `event.player`; `Player.all()`
1403
+ * An event about a player gives one as `event.player`; `server.players`
1255
1404
  * lists everyone on the server.
1256
1405
  */
1257
1406
  export class Player extends PlayerFields implements Client {
@@ -1259,30 +1408,6 @@ export class Player extends PlayerFields implements Client {
1259
1408
  super(id);
1260
1409
  }
1261
1410
 
1262
- /**
1263
- * The players on the server: `Player.all({ alive: true })`.
1264
- *
1265
- * Every field narrows: `{ bots: true }` is bots only, `{ humans: true }`
1266
- * people only. Without a filter, everyone connected, never an HLTV proxy.
1267
- *
1268
- * Pawn: `get_players`
1269
- */
1270
- static all(filter: PlayerFilter = {}): Player[] {
1271
- let flags = "h";
1272
- if (filter.alive) flags += "a";
1273
- if (filter.dead) flags += "b";
1274
- if (filter.humans) flags += "c";
1275
- if (filter.bots) flags += "d";
1276
-
1277
- const team = filter.team;
1278
- if (team != null) flags += "e";
1279
-
1280
- const ids = playerIds(flags, team ?? "");
1281
- const list: Player[] = [];
1282
- for (let i = 0; i < ids.length; i++) list.push(new Player(ids[i]));
1283
- return list;
1284
- }
1285
-
1286
1411
  /**
1287
1412
  * The player's name.
1288
1413
  *
@@ -1324,12 +1449,12 @@ export class Player extends PlayerFields implements Client {
1324
1449
  * Pawn: `cs_get_user_deaths`, `cs_set_user_deaths`
1325
1450
  */
1326
1451
  get deaths(): number {
1327
- return hasModule("reapi") ? get_member(this.id, m_iDeaths) : cs_get_user_deaths(this.id);
1452
+ return __hasReapi() ? get_member(this.id, m_iDeaths) : cs_get_user_deaths(this.id);
1328
1453
  }
1329
1454
 
1330
1455
  /** The deaths on the scoreboard; setting them tells the scoreboard too. */
1331
1456
  set deaths(value: number) {
1332
- if (hasModule("reapi")) {
1457
+ if (__hasReapi()) {
1333
1458
  set_member(this.id, m_iDeaths, value);
1334
1459
  sendScoreInfo(this.id, this.frags, value, get_member(this.id, m_iTeam));
1335
1460
  return;
@@ -1344,7 +1469,7 @@ export class Player extends PlayerFields implements Client {
1344
1469
  * Pawn: `cs_get_user_team`, `rg_set_user_team`
1345
1470
  */
1346
1471
  get team(): Team {
1347
- const index = hasModule("reapi") ? get_member(this.id, m_iTeam) : cs_get_user_team(this.id);
1472
+ const index = __hasReapi() ? get_member(this.id, m_iTeam) : cs_get_user_team(this.id);
1348
1473
  return index >= 0 && index < TEAM_NAMES.length ? TEAM_NAMES[index] : "UNASSIGNED";
1349
1474
  }
1350
1475
 
@@ -1355,7 +1480,7 @@ export class Player extends PlayerFields implements Client {
1355
1480
  */
1356
1481
  set team(value: Team) {
1357
1482
  const index = teamCell(value);
1358
- if (hasModule("reapi")) {
1483
+ if (__hasReapi()) {
1359
1484
  rg_set_user_team(this.id, index, MODEL_AUTO, true, false);
1360
1485
  return;
1361
1486
  }
@@ -1368,16 +1493,23 @@ export class Player extends PlayerFields implements Client {
1368
1493
  * Joins a side the way the game joins a player who picks it in the team
1369
1494
  * menu, appearance picked for him: `player.joinTeam("CT")`. A player who
1370
1495
  * has just arrived is in the game after it and can spawn, which
1371
- * `player.team = ...` does not do for him. `false` if the game refused.
1496
+ * `player.team = ...` does not do for him. A living player sent to the
1497
+ * spectators dies quietly: no death, no frag. `false` if the game refused.
1372
1498
  *
1373
1499
  * Pawn: `rg_join_team`
1374
1500
  */
1375
1501
  joinTeam(team: Team): boolean {
1376
1502
  if (team == "UNASSIGNED") return false;
1377
- if (hasModule("reapi")) return rg_join_team(this.id, teamCell(team)) != 0;
1503
+ if (__hasReapi()) return rg_join_team(this.id, teamCell(team)) != 0;
1378
1504
 
1379
1505
  // Without reapi, what the player would type: the team menu's slot, then
1380
- // the appearance menu's automatic pick.
1506
+ // the appearance menu's automatic pick. The menu takes a living player
1507
+ // to the spectators only in the freeze time, so he dies quietly first,
1508
+ // as rg_join_team has him: no death, no frag.
1509
+ if (team == "SPECTATOR" && this.isAlive) {
1510
+ this.deadFlag = "dead";
1511
+ set_pev(this.id, pev_health, 0);
1512
+ }
1381
1513
  engclient_cmd(this.id, "jointeam", team == "TERRORIST" ? "1" : team == "CT" ? "2" : "6");
1382
1514
  if (team != "SPECTATOR") engclient_cmd(this.id, "joinclass", "5");
1383
1515
  return this.team == team;
@@ -1395,14 +1527,42 @@ export class Player extends PlayerFields implements Client {
1395
1527
  }
1396
1528
 
1397
1529
  /**
1398
- * The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`.
1530
+ * The player's SteamID, e.g. `"STEAM_0:1:12345"`. A bot has `"BOT"`, HLTV has `"HLTV"`; until Steam confirms the player it is `"STEAM_ID_PENDING"` (wait for the `"authorized"` event), and on a LAN server `"STEAM_ID_LAN"`. With Reunion a game without Steam gets one made from its key (`authKey`): `"STEAM_..."` or `"VALVE_..."`, as the server's Reunion settings say.
1399
1531
  *
1400
1532
  * Pawn: `get_user_authid`
1401
1533
  */
1402
- get authid(): string {
1534
+ get steamId(): string {
1403
1535
  return get_user_authid(this.id);
1404
1536
  }
1405
1537
 
1538
+ /**
1539
+ * The way the player's game proved who he is, as Reunion tells it: one of `"steam"` (a Steam game), `"steamEmu"`, `"revEmu"`, `"revEmu2013"`, `"oldRevEmu"`, `"sc2009"`, `"avsmp"`, `"sxei"`, `"sse3"` (a game without Steam, by the emulator it proved itself with), `"dproto"`, `"hltv"`, or `"unknown"` on a server without Reunion.
1540
+ *
1541
+ * Pawn: `REU_GetAuthtype`
1542
+ */
1543
+ get authType(): AuthType {
1544
+ const type = hasReunion() ? REU_GetAuthtype(this.id) : 0;
1545
+ return type > 0 && type < AUTH_TYPES.length ? AUTH_TYPES[type] : "unknown";
1546
+ }
1547
+
1548
+ /**
1549
+ * The network protocol of the player's game: `48` for today's game, `47` for an old one Reunion lets in. `0` on a server without Reunion.
1550
+ *
1551
+ * Pawn: `REU_GetProtocol`
1552
+ */
1553
+ get protocol(): number {
1554
+ return hasReunion() ? REU_GetProtocol(this.id) : 0;
1555
+ }
1556
+
1557
+ /**
1558
+ * The key the player's game proved itself with, as Reunion read it: what his SteamID is made from. `""` on a server without Reunion.
1559
+ *
1560
+ * Pawn: `REU_GetAuthKey`
1561
+ */
1562
+ get authKey(): string {
1563
+ return hasReunion() ? REU_GetAuthKey(this.id) : "";
1564
+ }
1565
+
1406
1566
  /**
1407
1567
  * `true` while the player is alive.
1408
1568
  *
@@ -1523,7 +1683,7 @@ export class Player extends PlayerFields implements Client {
1523
1683
  * Pawn: `rg_give_item`, `give_item`
1524
1684
  */
1525
1685
  give(item: ItemName): boolean {
1526
- if (hasModule("reapi")) return rg_give_item(this.id, item) > 0;
1686
+ if (__hasReapi()) return rg_give_item(this.id, item) > 0;
1527
1687
  return give_item(this.id, item) > 0;
1528
1688
  }
1529
1689
 
@@ -1534,7 +1694,7 @@ export class Player extends PlayerFields implements Client {
1534
1694
  * Pawn: `rg_remove_all_items`, `strip_user_weapons`
1535
1695
  */
1536
1696
  removeAllItems(removeSuit: boolean = false): void {
1537
- if (hasModule("reapi")) {
1697
+ if (__hasReapi()) {
1538
1698
  rg_remove_all_items(this.id, removeSuit);
1539
1699
  return;
1540
1700
  }
@@ -1549,7 +1709,7 @@ export class Player extends PlayerFields implements Client {
1549
1709
  setAmmo(weapon: WeaponName, amount: number): void {
1550
1710
  const id = WEAPON_IDS.indexOf(weapon);
1551
1711
  if (id <= 0) return;
1552
- if (hasModule("reapi")) rg_set_user_bpammo(this.id, id, amount);
1712
+ if (__hasReapi()) rg_set_user_bpammo(this.id, id, amount);
1553
1713
  else cs_set_user_bpammo(this.id, id, amount);
1554
1714
  }
1555
1715
 
@@ -1562,7 +1722,7 @@ export class Player extends PlayerFields implements Client {
1562
1722
  getAmmo(weapon: WeaponName): number {
1563
1723
  const id = WEAPON_IDS.indexOf(weapon);
1564
1724
  if (id <= 0) return 0;
1565
- return hasModule("reapi") ? rg_get_user_bpammo(this.id, id) : cs_get_user_bpammo(this.id, id);
1725
+ return __hasReapi() ? rg_get_user_bpammo(this.id, id) : cs_get_user_bpammo(this.id, id);
1566
1726
  }
1567
1727
 
1568
1728
  /**
@@ -1571,7 +1731,7 @@ export class Player extends PlayerFields implements Client {
1571
1731
  * Pawn: `rg_round_respawn`
1572
1732
  */
1573
1733
  respawn(): void {
1574
- if (hasModule("reapi")) rg_round_respawn(this.id);
1734
+ if (__hasReapi()) rg_round_respawn(this.id);
1575
1735
  else ExecuteHamB(Ham_CS_RoundRespawn, this.id);
1576
1736
  }
1577
1737
 
@@ -1587,7 +1747,7 @@ export class Player extends PlayerFields implements Client {
1587
1747
 
1588
1748
  /**
1589
1749
  * The player's admin rights, from the letters in `users.ini`:
1590
- * `player.access.includes("Cvar")`.
1750
+ * `player.access.includes("cvar")`.
1591
1751
  *
1592
1752
  * Pawn: `get_user_flags`
1593
1753
  */
@@ -1602,7 +1762,7 @@ export class Player extends PlayerFields implements Client {
1602
1762
  * Pawn: `rg_switch_weapon`
1603
1763
  */
1604
1764
  switchWeapon(weapon: WeaponName): boolean {
1605
- if (hasModule("reapi")) {
1765
+ if (__hasReapi()) {
1606
1766
  const entity = rg_find_weapon_bpack_by_name(this.id, weapon);
1607
1767
  return entity > 0 && rg_switch_weapon(this.id, entity) != 0;
1608
1768
  }
@@ -1620,7 +1780,7 @@ export class Player extends PlayerFields implements Client {
1620
1780
  * Pawn: `rg_reset_maxspeed`
1621
1781
  */
1622
1782
  resetMaxSpeed(): void {
1623
- if (hasModule("reapi")) rg_reset_maxspeed(this.id);
1783
+ if (__hasReapi()) rg_reset_maxspeed(this.id);
1624
1784
  else ExecuteHamB(Ham_CS_Player_ResetMaxSpeed, this.id);
1625
1785
  }
1626
1786
 
@@ -1635,6 +1795,34 @@ export class Player extends PlayerFields implements Client {
1635
1795
  new Call(NATIVE_client_cmd).num(this.id).str("%s").str(text).run();
1636
1796
  }
1637
1797
 
1798
+ /**
1799
+ * Kicks the player off the server, with the reason he is shown:
1800
+ * `player.kick("Spam")`; without one, the game's own.
1801
+ *
1802
+ * Pawn: `server_cmd("kick #%d")`
1803
+ */
1804
+ kick(reason: string = ""): void {
1805
+ const said = reason.replaceAll("\"", "'");
1806
+ server.command(said.length > 0 ? `kick #${get_user_userid(this.id)} "${said}"` : `kick #${get_user_userid(this.id)}`);
1807
+ }
1808
+
1809
+ /**
1810
+ * Moves a bot `server.addBot` made, as a player's keys and mouse would for
1811
+ * one frame: `bot.move({ forward: 250, buttons: ["jump"] })`. A bot does
1812
+ * nothing by itself, so it is moved every frame - in the `"frame"` event -
1813
+ * or it stands still. A player who is not a bot is refused with an error.
1814
+ *
1815
+ * Pawn: `engfunc(EngFunc_RunPlayerMove, ...)`
1816
+ */
1817
+ move(options: MoveOptions = {}): void {
1818
+ if (!this.isBot) throw new Error(`move: ${this.name} is not a bot - only a bot is moved by a plugin`);
1819
+
1820
+ const angles: number[] = options.angles ?? this.viewAngle;
1821
+ const buttons = BUTTON.maskOf<Button>(options.buttons ?? []);
1822
+ const msec = options.msec ?? frameTime() * 1000;
1823
+ engfunc(EngFunc_RunPlayerMove, this.id, angles, options.forward ?? 0, options.side ?? 0, options.up ?? 0, buttons, 0, <i32>Math.round(Math.min(Math.max(msec, 1), 255)));
1824
+ }
1825
+
1638
1826
  /**
1639
1827
  * Asks the player's game for one of its cvars: `await
1640
1828
  * player.queryCvar("fps_max")` is the value as text, e.g. `"100"`, or
@@ -1732,8 +1920,8 @@ export function readText(fill: (out: number, max: number) => number, max: number
1732
1920
 
1733
1921
  /**
1734
1922
  * The ids of the players on the server, as an array. `flags`: `"a"` living,
1735
- * `"b"` dead, `"c"` no bots, `"h"` no HLTV, `"e"` only `team`. `Player.all` does the
1736
- * same with readable options.
1923
+ * `"b"` dead, `"c"` no bots, `"h"` no HLTV, `"e"` only `team`. `server.players` is
1924
+ * everyone, as players, to filter as an array.
1737
1925
  *
1738
1926
  * Pawn: `get_players`
1739
1927
  */
@@ -1748,7 +1936,8 @@ export function playerIds(flags: string = "", team: string = ""): number[] {
1748
1936
  return ids;
1749
1937
  }
1750
1938
 
1751
- // Server's event name is typed like a DOM one - `K extends keyof ServerEventMap` -
1939
+ // Server's event name is typed like a DOM one - `K extends keyof ServerEventMap`,
1940
+ // and a message's `K extends keyof ServerMessageMap` -
1752
1941
  // so an editor completes it, refuses a misspelled one, and hands the listener
1753
1942
  // the event's own type. The compiler reads the same signature through a patch
1754
1943
  // (runtime/patches): the name has to be written out as a string literal,
@@ -1757,23 +1946,184 @@ export function playerIds(flags: string = "", team: string = ""): number[] {
1757
1946
 
1758
1947
  // ---------------------------------------------------------------- commands
1759
1948
 
1760
- /** A command's handler: gets the player who typed it and the words after the command's name. */
1761
- export type CommandHandler = (player: Player, args: string[]) => void;
1949
+ // A command is added by its usage - "/kick <target> [reason]" - and its words
1950
+ // are read by code the build writes for that call (scripts/typed-commands.ts):
1951
+ // a parser over __CommandWords, which reads each word as the argument's type
1952
+ // says and answers the one who typed it with the usage when a word is wrong.
1953
+ // The editor's signatures of addCommand and addServerCommand are in amxts.d.ts.
1762
1954
 
1763
1955
  /** The options of a command: who may use it and its description in a listing. */
1764
1956
  export interface CommandOptions {
1765
1957
  /** The admin right a player needs to use the command; left out, everyone may. */
1766
1958
  access?: Access;
1767
- /** The command's description, shown by `amx_help` and the like. */
1959
+ /** The command's description, shown by `amx_help` and in `server.commands`. */
1768
1960
  description?: string;
1769
1961
  }
1770
1962
 
1963
+ /** A command the plugin added, as `server.commands` lists it: what a `/help` shows. */
1964
+ export class CommandInfo {
1965
+ constructor(
1966
+ /** The command's usage, as it is typed, e.g. `"/kick <target> [reason]"`. */
1967
+ readonly usage: string,
1968
+ /** The command's description, as its `description` option gave it; `""` without one. */
1969
+ readonly description: string,
1970
+ /** The admin right the command needs; `null` when everyone may use it. */
1971
+ readonly access: Access | null,
1972
+ /** Whether it is a command of the server console rather than a player's. */
1973
+ readonly server: boolean
1974
+ ) {}
1975
+ }
1976
+
1977
+ /** A command's name: the usage's first word - or, for a chat phrase, `"say <phrase>"` whole. */
1978
+ function commandName(usage: string): string {
1979
+ const text = usage.trim();
1980
+ if (text.startsWith("say ")) return text;
1981
+ const space = text.indexOf(" ");
1982
+ return space < 0 ? text : text.substring(0, space);
1983
+ }
1984
+
1985
+ /** The players a command's word names: `#userid`; else the whole name, in any case; else a part of it. */
1986
+ function playersNamed(word: string): Player[] {
1987
+ const players = server.players;
1988
+ if (word.startsWith("#")) {
1989
+ const userid = <i32>Number(word.substring(1));
1990
+ return players.filter((player: Player) => get_user_userid(player.id) == userid);
1991
+ }
1992
+ const lower = word.toLowerCase();
1993
+ const whole = players.filter((player: Player) => player.name.toLowerCase() == lower);
1994
+ return whole.length > 0 ? whole : players.filter((player: Player) => player.name.toLowerCase().includes(lower));
1995
+ }
1996
+
1997
+ let commonLoaded = false;
1998
+
1999
+ /** A line of AMX Mod X's `common.txt` in the player's language: the words of its own admin commands. */
2000
+ function commonText(player: Player | null, key: string): string {
2001
+ if (!commonLoaded) commonLoaded = lang.load("common");
2002
+ return lang.translate(player, key);
2003
+ }
2004
+
2005
+ /**
2006
+ * @hidden The words a command was typed with, as the build's code for one
2007
+ * command reads its arguments (scripts/typed-commands.ts): each by its place,
2008
+ * as its type says. A word that is not what the command takes answers the
2009
+ * one who typed it with the usage, and sets `failed`: the handler does not run.
2010
+ */
2011
+ export class __CommandWords {
2012
+ /** Set once a word is wrong: the one who typed it has been told. */
2013
+ failed: bool = false;
2014
+
2015
+ constructor(
2016
+ /** The player who typed it; `null` for the server's console. */
2017
+ readonly player: Player | null,
2018
+ readonly usage: string,
2019
+ readonly words: string[],
2020
+ /** The line from each word on, as typed: what the last text argument takes. */
2021
+ readonly rests: string[]
2022
+ ) {}
2023
+
2024
+ get count(): i32 {
2025
+ return this.words.length;
2026
+ }
2027
+
2028
+ /** The word at `at`; `""` when it was not typed. */
2029
+ text(at: i32): string {
2030
+ return at < this.words.length ? this.words[at] : "";
2031
+ }
2032
+
2033
+ /** The rest of the line from the word at `at`. */
2034
+ rest(at: i32): string {
2035
+ return at < this.rests.length ? this.rests[at] : "";
2036
+ }
2037
+
2038
+ /** The word at `at` as a number; a word that is not one fails. */
2039
+ number(at: i32): number {
2040
+ const word = this.text(at);
2041
+ const value = Number(word);
2042
+ if (word.length == 0 || isNaN(value)) this.fail("");
2043
+ return value;
2044
+ }
2045
+
2046
+ /** The word at `at`, one of `names`; another fails. */
2047
+ name(at: i32, names: string[]): string {
2048
+ const word = this.text(at);
2049
+ if (!names.includes(word)) this.fail("");
2050
+ return word;
2051
+ }
2052
+
2053
+ /** The player the word at `at` names - `#userid`, the whole name or a part of it. None, or several, fails. */
2054
+ target(at: i32): Player | null {
2055
+ const found = playersNamed(this.text(at));
2056
+ if (found.length == 1) return found[0];
2057
+ this.fail(found.length == 0
2058
+ ? commonText(this.player, "CL_NOT_FOUND")
2059
+ : `${commonText(this.player, "MORE_CL_MATCHT")}: ${found.map<string>((one: Player) => one.name).join(", ")}`);
2060
+ return null;
2061
+ }
2062
+
2063
+ /** Whether at least `count` words were typed; fewer fails. */
2064
+ need(count: i32): bool {
2065
+ if (this.words.length < count) this.fail("");
2066
+ return !this.failed;
2067
+ }
2068
+
2069
+ /** Whether no word is left over after the first `count`; one more fails. */
2070
+ done(count: i32): bool {
2071
+ if (this.words.length > count) this.fail("");
2072
+ return !this.failed;
2073
+ }
2074
+
2075
+ /** Tells the one who typed it what was wrong, and the usage. */
2076
+ private fail(why: string): void {
2077
+ if (this.failed) return;
2078
+ this.failed = true;
2079
+ const usage = `${commonText(this.player, "USAGE")}: ${this.usage}`;
2080
+ const player = this.player;
2081
+ if (player == null) {
2082
+ if (why.length > 0) console.log(why);
2083
+ console.log(usage);
2084
+ return;
2085
+ }
2086
+ if (why.length > 0) print(player, why);
2087
+ print(player, usage);
2088
+ }
2089
+ }
2090
+
2091
+ /** A chat line's words - one in double quotes is one word - and the line from each word on. */
2092
+ function chatWords(player: Player, usage: string, line: string): __CommandWords {
2093
+ const words: string[] = [];
2094
+ const rests: string[] = [];
2095
+ let i = 0;
2096
+ while (i < line.length) {
2097
+ while (i < line.length && line.charAt(i) == " ") i++;
2098
+ if (i >= line.length) break;
2099
+ const quoted = line.charAt(i) == "\"";
2100
+ const close = quoted ? line.indexOf("\"", i + 1) : line.indexOf(" ", i);
2101
+ const end = close < 0 ? line.length : close;
2102
+ const word = line.substring(quoted ? i + 1 : i, end);
2103
+ // A quoted last word is the rest without its quotes.
2104
+ rests.push(quoted && close >= 0 && line.substring(close + 1).trim().length == 0 ? word : line.substring(i).trim());
2105
+ words.push(word);
2106
+ i = end + 1;
2107
+ }
2108
+ return new __CommandWords(player, usage, words, rests);
2109
+ }
2110
+
2111
+ /** A console command's words, as the engine split them. */
2112
+ function consoleWords(player: Player | null, usage: string): __CommandWords {
2113
+ const words: string[] = [];
2114
+ const count = read_argc();
2115
+ for (let i = 1; i < count; i++) words.push(read_argv(i));
2116
+ const rests: string[] = [];
2117
+ for (let i = 0; i < words.length; i++) rests.push(words.slice(i).join(" "));
2118
+ return new __CommandWords(player, usage, words, rests);
2119
+ }
2120
+
1771
2121
  // The commands of this plugin, by name. One trampoline serves them all - the
1772
- // host calls it by its table index - and it finds the handler by the name the
2122
+ // host calls it by its table index - and it finds the command by the name the
1773
2123
  // player typed.
1774
2124
  const commandNames: string[] = [];
1775
- const commandHandlers: CommandHandler[] = [];
1776
- const commandAccess: string[] = [];
2125
+ const commandRuns: ((words: __CommandWords) => void)[] = [];
2126
+ const commandInfos: CommandInfo[] = [];
1777
2127
  let chatHooked = false;
1778
2128
 
1779
2129
  function findCommand(name: string): i32 {
@@ -1786,7 +2136,7 @@ function findCommand(name: string): i32 {
1786
2136
 
1787
2137
  /**
1788
2138
  * Converts `users.ini` letters to rights: `accessOf("abc")` is
1789
- * [`"Immunity"`, `"Reservation"`, `"Kick"`]. An unknown letter is skipped.
2139
+ * [`"immunity"`, `"reservation"`, `"kick"`]. An unknown letter is skipped.
1790
2140
  *
1791
2141
  * Pawn: `read_flags`
1792
2142
  */
@@ -1796,23 +2146,20 @@ export function accessOf(letters: string): Access[] {
1796
2146
 
1797
2147
  /** Whether a player holds the admin flag a command asks for. */
1798
2148
  function mayRun(id: i32, at: i32): bool {
1799
- const access = commandAccess[at];
1800
- if (access.length == 0) return true;
2149
+ const access = commandInfos[at].access;
2150
+ if (access == null) return true;
1801
2151
  return (get_user_flags(id) & ACCESS.bitOf(access)) != 0;
1802
2152
  }
1803
2153
 
1804
- /** say /name a b - the chat text split on spaces, the first word the name. */
2154
+ /** say /name a b - the chat text, its first word the name; or a phrase added as "say <phrase>", the whole line. */
1805
2155
  function chatCommand(player: number, level: number, cid: number, unused: number): void {
1806
2156
  const id = <i32>player;
1807
2157
  const text = read_argv(1).trim();
2158
+ const space = text.indexOf(" ");
1808
2159
  const slash = text.startsWith("/");
1809
-
1810
- // "/name a b" - or a phrase added as "say <phrase>", which is the whole line.
1811
- const words = text.split(" ").filter((word: string) => word.length > 0);
1812
- const at = slash && words.length > 0 ? findCommand(words[0]) : findCommand("say " + text);
2160
+ const at = slash ? findCommand(space < 0 ? text : text.substring(0, space)) : findCommand("say " + text);
1813
2161
  if (at < 0 || !mayRun(id, at)) { _outcome(0); return; }
1814
- if (!slash) words.length = 1;
1815
- runCommand(at, id, words.slice(1));
2162
+ runCommand(at, id, chatWords(new Player(id), commandInfos[at].usage, slash && space >= 0 ? text.substring(space + 1) : ""));
1816
2163
  }
1817
2164
 
1818
2165
  /** name a b - a console command, its arguments as the engine split them. */
@@ -1820,7 +2167,7 @@ function consoleCommand(player: number, level: number, cid: number, unused: numb
1820
2167
  const id = <i32>player;
1821
2168
  const at = findCommand(read_argv(0));
1822
2169
  if (at < 0 || !mayRun(id, at)) { _outcome(0); return; }
1823
- runCommand(at, id, commandArgs());
2170
+ runCommand(at, id, consoleWords(new Player(id), commandInfos[at].usage));
1824
2171
  }
1825
2172
 
1826
2173
  /**
@@ -1828,36 +2175,28 @@ function consoleCommand(player: number, level: number, cid: number, unused: numb
1828
2175
  * (see __co_ambient_player); the command is handled, so a chat command is not
1829
2176
  * repeated in chat, as a Pawn command's PLUGIN_HANDLED does.
1830
2177
  */
1831
- function runCommand(at: i32, id: i32, args: string[]): void {
2178
+ function runCommand(at: i32, id: i32, words: __CommandWords): void {
1832
2179
  const ambient = __co_ambient_player;
1833
2180
  __co_ambient_player = id;
1834
- commandHandlers[at](new Player(id), args);
2181
+ commandRuns[at](words);
1835
2182
  __co_ambient_player = ambient;
1836
2183
  handled();
1837
2184
  }
1838
2185
 
1839
- /** The words after the command's name, as the engine split them. */
1840
- function commandArgs(): string[] {
1841
- const args: string[] = [];
1842
- const count = read_argc();
1843
- for (let i = 1; i < count; i++) args.push(read_argv(i));
1844
- return args;
1845
- }
1846
-
1847
- /** A server command's handler: gets the words after the command's name. */
1848
- export type ServerCommandHandler = (args: string[]) => void;
1849
-
1850
2186
  // The server commands of this plugin, by name, and the ones still waiting for
1851
2187
  // plugin_init. One trampoline serves them all, as it does the player commands.
1852
2188
  const serverCommandNames: string[] = [];
1853
- const serverCommandHandlers: ServerCommandHandler[] = [];
2189
+ const serverCommandRuns: ((words: __CommandWords) => void)[] = [];
2190
+ const serverCommandUsages: string[] = [];
1854
2191
  const waitingServerCommands: string[] = [];
1855
2192
 
1856
2193
  // A touch the engine module filters by class, so a touch nobody listens for -
1857
2194
  // and there is one every frame for a player on the ground - never reaches the
1858
- // plugin. One register_touch per pair of classes, its listeners behind it.
2195
+ // plugin. One register_touch per pair of classes, its listeners behind it,
2196
+ // switched off while it has none.
1859
2197
  class TouchFilter {
1860
2198
  listeners: TouchListener[] = [];
2199
+ hook: __Switch = new __Switch();
1861
2200
  constructor(public toucher: string, public touched: string) {}
1862
2201
  }
1863
2202
 
@@ -1867,21 +2206,27 @@ const waitingTouches: TouchFilter[] = [];
1867
2206
  // A Ham Sandwich hook waiting for plugin_init: RegisterHam makes an entity of
1868
2207
  // the class to find its function, which is not for the moment a plugin loads.
1869
2208
  class HamRegistration {
1870
- constructor(public fn: i32, public classname: string, public handler: WideHandler, public post: bool) {}
2209
+ constructor(public fn: i32, public classname: string, public handler: WideHandler, public post: bool, public hook: __Switch | null) {}
1871
2210
  }
1872
2211
 
1873
2212
  const waitingHams: HamRegistration[] = [];
1874
2213
 
1875
2214
  function registerHam(registration: HamRegistration): void {
1876
- _ham(registration.fn, registration.classname, hostIndex(registration.handler, true), registration.post ? 1 : 0);
2215
+ const handle = _ham(registration.fn, registration.classname, hostIndex(registration.handler, true), registration.post ? 1 : 0);
2216
+ const hook = registration.hook;
2217
+ if (hook == null || handle == 0) return;
2218
+ hook.add((on: bool): void => {
2219
+ if (on) EnableHamForward(handle);
2220
+ else DisableHamForward(handle);
2221
+ });
1877
2222
  }
1878
2223
 
1879
2224
  /**
1880
2225
  * @hidden The hood of a game event Ham Sandwich delivers (as/hooks.ts):
1881
- * `fn` hooked on the class, a reload taking its slot back.
2226
+ * `fn` hooked on the class, a reload taking its slot back, switched by `hook`.
1882
2227
  */
1883
- export function __ham(fn: i32, classname: string, handler: WideHandler, post: bool): void {
1884
- const registration = new HamRegistration(fn, classname, handler, post);
2228
+ export function __ham(fn: i32, classname: string, handler: WideHandler, post: bool, hook: __Switch | null = null): void {
2229
+ const registration = new HamRegistration(fn, classname, handler, post, hook);
1885
2230
  if (serverUp) registerHam(registration);
1886
2231
  else waitingHams.push(registration);
1887
2232
  }
@@ -1891,7 +2236,7 @@ function serverCommand(a: number, b: number, c: number, d: number): void {
1891
2236
  const at = serverCommandNames.indexOf(read_argv(0).toLowerCase());
1892
2237
  if (at < 0) { _outcome(0); return; }
1893
2238
 
1894
- serverCommandHandlers[at](commandArgs());
2239
+ serverCommandRuns[at](consoleWords(null, serverCommandUsages[at]));
1895
2240
  handled();
1896
2241
  }
1897
2242
 
@@ -2040,6 +2385,12 @@ export interface ShakeOptions {
2040
2385
  frequency?: number;
2041
2386
  }
2042
2387
 
2388
+ /** The options of `player.screen.progressBar`. */
2389
+ export interface ProgressBarOptions {
2390
+ /** The bar's fill at the start, in percent; `0`, empty, by default. */
2391
+ startPercent?: number;
2392
+ }
2393
+
2043
2394
  /** A status icon's state, one of `"hide"`, `"show"` (lit) or `"flash"`. */
2044
2395
  export type StatusIconState = "hide" | "show" | "flash";
2045
2396
 
@@ -2058,15 +2409,17 @@ function fixed(value: number, units: number): i32 {
2058
2409
  * player.screen.statusIcon("dmg_cold", "show", [0, 160, 255]);
2059
2410
  * ```
2060
2411
  *
2061
- * Times are in seconds.
2412
+ * Times are in seconds. A message listener hears what the screen sends, as
2413
+ * it hears the game's: `"progressBar"` hears `progressBar(seconds)`.
2062
2414
  *
2063
2415
  * Pawn: `ScreenFade`, `ScreenShake`, `StatusIcon`, ...
2064
2416
  */
2065
2417
  export class Screen {
2066
2418
  constructor(private id: i32) {}
2067
2419
 
2420
+ // Through the engine (emessage_*): AMX Mod X's message hooks do not see a plugin's message_begin.
2068
2421
  private begin(name: string, reliable: bool = true): void {
2069
- message_begin(reliable ? MSG_ONE : MSG_ONE_UNRELIABLE, get_user_msgid(name), [0, 0, 0], this.id);
2422
+ emessage_begin(reliable ? MSG_ONE : MSG_ONE_UNRELIABLE, get_user_msgid(name), [0, 0, 0], this.id);
2070
2423
  }
2071
2424
 
2072
2425
  /**
@@ -2081,11 +2434,11 @@ export class Screen {
2081
2434
  if (options.stay) flags |= FFADE_STAYOUT;
2082
2435
 
2083
2436
  this.begin("ScreenFade");
2084
- write_short(fixed(options.duration ?? 1.0, 4096.0));
2085
- write_short(fixed(options.hold ?? 0.0, 4096.0));
2086
- write_short(flags);
2087
- for (let i = 0; i < 4; i++) write_byte(i < color.length ? <i32>color[i] : 255);
2088
- message_end();
2437
+ ewrite_short(fixed(options.duration ?? 1.0, 4096.0));
2438
+ ewrite_short(fixed(options.hold ?? 0.0, 4096.0));
2439
+ ewrite_short(flags);
2440
+ for (let i = 0; i < 4; i++) ewrite_byte(i < color.length ? <i32>color[i] : 255);
2441
+ emessage_end();
2089
2442
  }
2090
2443
 
2091
2444
  /**
@@ -2095,10 +2448,10 @@ export class Screen {
2095
2448
  */
2096
2449
  shake(options: ShakeOptions = {}): void {
2097
2450
  this.begin("ScreenShake");
2098
- write_short(fixed(options.amplitude ?? 4.0, 4096.0));
2099
- write_short(fixed(options.duration ?? 1.0, 4096.0));
2100
- write_short(fixed(options.frequency ?? 5.0, 256.0));
2101
- message_end();
2451
+ ewrite_short(fixed(options.amplitude ?? 4.0, 4096.0));
2452
+ ewrite_short(fixed(options.duration ?? 1.0, 4096.0));
2453
+ ewrite_short(fixed(options.frequency ?? 5.0, 256.0));
2454
+ emessage_end();
2102
2455
  }
2103
2456
 
2104
2457
  /**
@@ -2110,12 +2463,12 @@ export class Screen {
2110
2463
  statusIcon(sprite: string, state: StatusIconState, color: number[] = [0, 160, 0]): void {
2111
2464
  const status = state == "show" ? 1 : state == "flash" ? 2 : 0;
2112
2465
  this.begin("StatusIcon");
2113
- write_byte(status);
2114
- write_string(sprite);
2466
+ ewrite_byte(status);
2467
+ ewrite_string(sprite);
2115
2468
  if (status != 0) {
2116
- for (let i = 0; i < 3; i++) write_byte(i < color.length ? <i32>color[i] : 0);
2469
+ for (let i = 0; i < 3; i++) ewrite_byte(i < color.length ? <i32>color[i] : 0);
2117
2470
  }
2118
- message_end();
2471
+ emessage_end();
2119
2472
  }
2120
2473
 
2121
2474
  /**
@@ -2127,8 +2480,8 @@ export class Screen {
2127
2480
  */
2128
2481
  roundTime(seconds: number): void {
2129
2482
  this.begin("RoundTime", false);
2130
- write_short(<i32>seconds);
2131
- message_end();
2483
+ ewrite_short(<i32>seconds);
2484
+ emessage_end();
2132
2485
  }
2133
2486
 
2134
2487
  /**
@@ -2139,8 +2492,8 @@ export class Screen {
2139
2492
  */
2140
2493
  hideHud(parts: HideHud[]): void {
2141
2494
  this.begin("HideWeapon");
2142
- write_byte(HIDE_HUD.maskOf(parts));
2143
- message_end();
2495
+ ewrite_byte(HIDE_HUD.maskOf(parts));
2496
+ emessage_end();
2144
2497
  }
2145
2498
 
2146
2499
  /**
@@ -2150,8 +2503,8 @@ export class Screen {
2150
2503
  */
2151
2504
  crosshair(shown: boolean): void {
2152
2505
  this.begin("Crosshair");
2153
- write_byte(shown ? 1 : 0);
2154
- message_end();
2506
+ ewrite_byte(shown ? 1 : 0);
2507
+ emessage_end();
2155
2508
  }
2156
2509
 
2157
2510
  /**
@@ -2162,21 +2515,28 @@ export class Screen {
2162
2515
  */
2163
2516
  flashlight(on: boolean, battery: number = 100): void {
2164
2517
  this.begin("Flashlight");
2165
- write_byte(on ? 1 : 0);
2166
- write_byte(<i32>battery);
2167
- message_end();
2518
+ ewrite_byte(on ? 1 : 0);
2519
+ ewrite_byte(<i32>battery);
2520
+ emessage_end();
2168
2521
  }
2169
2522
 
2170
2523
  /**
2171
2524
  * Shows the progress bar in the middle of the player's screen, filling up
2172
- * over `seconds`; `0` hides it.
2525
+ * over `seconds`; `0` hides it. With `startPercent` it starts part of the
2526
+ * way full and fills the rest of `seconds`:
2527
+ *
2528
+ * ```ts
2529
+ * player.screen.progressBar(4, { startPercent: 50 }); // half full, full in 2 seconds
2530
+ * ```
2173
2531
  *
2174
- * Pawn: `BarTime`, `rg_send_bartime`
2532
+ * Pawn: `BarTime`, `BarTime2`, `rg_send_bartime`, `rg_send_bartime2`
2175
2533
  */
2176
- progressBar(seconds: number): void {
2177
- this.begin("BarTime");
2178
- write_short(fixed(seconds, 1.0));
2179
- message_end();
2534
+ progressBar(seconds: number, options: ProgressBarOptions = {}): void {
2535
+ const startPercent = options.startPercent ?? 0;
2536
+ this.begin(startPercent == 0 ? "BarTime" : "BarTime2");
2537
+ ewrite_short(fixed(seconds, 1.0));
2538
+ if (startPercent != 0) ewrite_short(fixed(startPercent, 1.0));
2539
+ emessage_end();
2180
2540
  }
2181
2541
  }
2182
2542
 
@@ -2250,6 +2610,52 @@ function precacheWaiting(event: PluginPrecacheEvent): void {
2250
2610
  precacheOpen = true;
2251
2611
  for (let i = 0; i < waitingPrecaches.length; i++) precacheNow(waitingPrecaches[i]);
2252
2612
  waitingPrecaches.length = 0;
2613
+ runWaiting(waitingAtPrecache);
2614
+ }
2615
+
2616
+ // The stock hooks a server without reapi hears ReGameDLL's events through
2617
+ // (as/hlds.ts), asked for before natives may be called - at a plugin's top
2618
+ // level, during plugin_natives: a precache's hook at plugin_precache, before
2619
+ // the game's own precaches, and every other at plugin_init, as a command is.
2620
+ const waitingAtPrecache: (() => void)[] = [];
2621
+ const waitingAtInit: (() => void)[] = [];
2622
+
2623
+ function runWaiting(list: (() => void)[]): void {
2624
+ const now = list.slice(0);
2625
+ list.length = 0;
2626
+ for (let i = 0; i < now.length; i++) now[i]();
2627
+ }
2628
+
2629
+ // @ts-ignore: decorator
2630
+ @external("env", "on_cell") declare function _onCell(event: string, fn: i32, shape: i32, arg: i32, value: i32): void;
2631
+
2632
+ /**
2633
+ * @hidden A forward the host relays, heard by `fn` (a one-cell handler) only
2634
+ * when its argument `arg` is `value`: the module compares it, so a forward
2635
+ * that comes often crosses into the plugin for that value alone.
2636
+ */
2637
+ export function __onCell(event: string, fn: i32, arg: i32, value: i32): void {
2638
+ _onCell(event, fn, 0, arg, value);
2639
+ }
2640
+
2641
+ // @ts-ignore: decorator
2642
+ @external("env", "off") declare function _off(event: string, fn: i32): void;
2643
+
2644
+ /** @hidden Takes the handler `fn` of a forward the host relays off again: once its event has no listener left. */
2645
+ export function __off(event: string, fn: i32): void {
2646
+ _off(event, fn);
2647
+ }
2648
+
2649
+ /** @hidden Runs `register` from plugin_init on: now, or when it comes. */
2650
+ export function __whenUp(register: () => void): void {
2651
+ if (serverUp) register();
2652
+ else waitingAtInit.push(register);
2653
+ }
2654
+
2655
+ /** @hidden Runs `register` from plugin_precache on - or plugin_init, after a reload mid-map. */
2656
+ export function __whenPrecache(register: () => void): void {
2657
+ if (precacheOpen || serverUp) register();
2658
+ else waitingAtPrecache.push(register);
2253
2659
  }
2254
2660
  addServerListener<PluginPrecacheEvent>(precacheWaiting);
2255
2661
 
@@ -2266,8 +2672,8 @@ const FFADE_STAYOUT = 0x0004;
2266
2672
  * or a text, as the message wrote it.
2267
2673
  *
2268
2674
  * ```ts
2269
- * server.addEventListener("message:TeamScore", (event) => {
2270
- * console.log(`${event.args.text(0)} ${event.args.number(1)}`);
2675
+ * server.addMessageListener("botProgress", (event) => {
2676
+ * console.log(`${event.args.length} ${event.args.number(0)}`);
2271
2677
  * });
2272
2678
  * ```
2273
2679
  *
@@ -2315,24 +2721,23 @@ export class MessageArgs {
2315
2721
  * HUD icon - heard on its way, before it leaves:
2316
2722
  *
2317
2723
  * ```ts
2318
- * server.addEventListener("message:TextMsg", (event) => {
2724
+ * server.addMessageListener("text", (event) => {
2319
2725
  * if (event.text == "#Round_Draw") event.preventDefault();
2320
2726
  * });
2321
2727
  * ```
2322
2728
  *
2323
- * A message the editor knows has typed fields (`event.text`); every one has
2324
- * its arguments in `event.args`. Writing a field changes what the client
2325
- * gets.
2729
+ * A message whose layout is known has a typed field for each argument
2730
+ * (`event.text`), and writing one changes what the client gets; a message
2731
+ * without a known layout is read by place, through `event.args`, which
2732
+ * every message has.
2326
2733
  *
2327
2734
  * Pawn: `register_message`
2328
2735
  */
2329
2736
  export class ClientMessage {
2330
- /** @hidden What a message's event is told apart by, at compile time. */
2331
- __message: bool = true;
2332
2737
  /** @hidden The player it goes to: the message's msg_entity. */
2333
2738
  __receiver: i32 = 0;
2334
2739
 
2335
- /** The message's name, e.g. `"TextMsg"`. */
2740
+ /** The game's name of the message, e.g. `"TextMsg"` for `text`. */
2336
2741
  name: string = "";
2337
2742
 
2338
2743
  /** The player the message goes to; `null` for a message to everyone. */
@@ -2354,39 +2759,97 @@ export class ClientMessage {
2354
2759
  handled();
2355
2760
  }
2356
2761
 
2357
- /** @hidden An argument by its number, 1 for the first, as a number. */
2762
+ // A field reads an argument by its number, 1 for the first. One the
2763
+ // message did not write - StatusIcon's colour when it hides the icon, a
2764
+ // game text without words to put in - reads as 0 or "", and writing it
2765
+ // does nothing: AMX Mod X reports an argument past the last as an error.
2766
+
2767
+ /** @hidden Whether the message has the argument. */
2768
+ protected __has(arg: i32): bool {
2769
+ return arg <= get_msg_args();
2770
+ }
2771
+
2772
+ /** @hidden An argument as a number. */
2358
2773
  protected __number(arg: i32): f64 {
2359
- return this.args.number(arg - 1);
2774
+ return this.__has(arg) ? this.args.number(arg - 1) : 0;
2360
2775
  }
2361
2776
 
2362
2777
  /** @hidden */
2363
2778
  protected __setNumber(arg: i32, value: f64): void {
2364
- this.args.setNumber(arg - 1, value);
2779
+ if (this.__has(arg)) this.args.setNumber(arg - 1, value);
2365
2780
  }
2366
2781
 
2367
2782
  /** @hidden */
2368
2783
  protected __text(arg: i32): string {
2369
- return get_msg_arg_string(arg);
2784
+ return this.__has(arg) ? get_msg_arg_string(arg) : "";
2370
2785
  }
2371
2786
 
2372
2787
  /** @hidden */
2373
2788
  protected __setText(arg: i32, value: string): void {
2374
- set_msg_arg_string(arg, value);
2789
+ if (this.__has(arg)) set_msg_arg_string(arg, value);
2790
+ }
2791
+
2792
+ /** @hidden Every text from the argument on. */
2793
+ protected __texts(arg: i32): string[] {
2794
+ const texts: string[] = [];
2795
+ for (let at = arg; this.__has(at); at++) texts.push(get_msg_arg_string(at));
2796
+ return texts;
2797
+ }
2798
+
2799
+ /** @hidden Writes the texts from the argument on, as many as the message has. */
2800
+ protected __setTexts(arg: i32, value: string[]): void {
2801
+ for (let i = 0; i < value.length && this.__has(arg + i); i++) set_msg_arg_string(arg + i, value[i]);
2375
2802
  }
2376
2803
 
2377
2804
  /** @hidden A player's number argument; `0` or past the players is none. */
2378
2805
  protected __player(arg: i32): Player | null {
2379
- const id = get_msg_arg_int(arg);
2806
+ const id = <i32>this.__number(arg);
2380
2807
  return id >= 1 && id <= get_maxplayers() ? new Player(id) : null;
2381
2808
  }
2809
+
2810
+ /** @hidden Three coordinates from the argument on. */
2811
+ protected __vector(arg: i32): Vector {
2812
+ return new Vector(this.__number(arg), this.__number(arg + 1), this.__number(arg + 2));
2813
+ }
2814
+
2815
+ /** @hidden */
2816
+ protected __setVector(arg: i32, value: number[]): void {
2817
+ for (let i = 0; i < 3; i++) this.__setNumber(arg + i, value[i]);
2818
+ }
2819
+
2820
+ /** @hidden `count` bytes from the argument on: a colour. */
2821
+ protected __bytes(arg: i32, count: i32): number[] {
2822
+ const bytes: number[] = [];
2823
+ for (let i = 0; i < count; i++) bytes.push(this.__number(arg + i));
2824
+ return bytes;
2825
+ }
2826
+
2827
+ /** @hidden */
2828
+ protected __setBytes(arg: i32, value: number[]): void {
2829
+ for (let i = 0; i < value.length; i++) this.__setNumber(arg + i, value[i]);
2830
+ }
2831
+
2832
+ /** @hidden Whether the argument has the bit. */
2833
+ protected __bit(arg: i32, bit: i32): bool {
2834
+ return (<i32>this.__number(arg) & bit) != 0;
2835
+ }
2836
+
2837
+ /** @hidden Sets or clears one bit of the argument, keeping the others. */
2838
+ protected __setBit(arg: i32, bit: i32, on: bool): void {
2839
+ const mask = <i32>this.__number(arg);
2840
+ this.__setNumber(arg, on ? mask | bit : mask & ~bit);
2841
+ }
2382
2842
  }
2383
2843
 
2384
2844
  // One register_message per message name, its listeners behind it: the engine
2385
2845
  // hands the plugin only the messages it listens to. Registered when the
2386
2846
  // server is up - a message's id is known from plugin_init - and a reload
2387
- // takes the same public back.
2847
+ // takes the same public back. AMX Mod X's unregister_message can take
2848
+ // another plugin's hook off instead, so with no listener left the public is
2849
+ // switched off in the module.
2388
2850
  class MessageChannel {
2389
2851
  listeners: ((event: ClientMessage) => void)[] = [];
2852
+ hook: __Switch = new __Switch();
2390
2853
  constructor(public name: string, public make: () => ClientMessage) {}
2391
2854
  }
2392
2855
 
@@ -2400,7 +2863,7 @@ function messageChannel(name: string): MessageChannel | null {
2400
2863
  return null;
2401
2864
  }
2402
2865
 
2403
- function addMessageListener<E>(name: string, listener: (event: E) => void): void {
2866
+ function listenToMessage<E>(name: string, listener: (event: E) => void): void {
2404
2867
  let channel = messageChannel(name);
2405
2868
 
2406
2869
  if (channel == null) {
@@ -2411,25 +2874,27 @@ function addMessageListener<E>(name: string, listener: (event: E) => void): void
2411
2874
  }
2412
2875
 
2413
2876
  channel.listeners.push(changetype<(event: ClientMessage) => void>(listener));
2877
+ channel.hook.set(true);
2414
2878
  }
2415
2879
 
2416
- function removeMessageListener<E>(name: string, listener: (event: E) => void): void {
2880
+ function stopListeningToMessage<E>(name: string, listener: (event: E) => void): void {
2417
2881
  const channel = messageChannel(name);
2418
2882
  if (channel == null) return;
2419
2883
  const at = channel.listeners.indexOf(changetype<(event: ClientMessage) => void>(listener));
2420
2884
  if (at >= 0) channel.listeners.splice(at, 1);
2885
+ channel.hook.set(channel.listeners.length > 0);
2421
2886
  }
2422
2887
 
2423
2888
  function registerMessage(channel: MessageChannel): void {
2424
2889
  const id = get_user_msgid(channel.name);
2425
2890
 
2426
2891
  if (id == 0) {
2427
- console.error(`message:${channel.name} - the game has no message by that name`);
2892
+ console.error(`${channel.name} - the game has no message by that name`);
2428
2893
  return;
2429
2894
  }
2430
2895
 
2431
2896
  const fired = (message: number, dest: number, receiver: number, d: number): void => messageFired(channel, <i32>receiver);
2432
- const pub = publicFor(fired, `msg:${channel.name}`);
2897
+ const pub = __switchedPublic(fired, `msg:${channel.name}`, channel.hook);
2433
2898
  if (pub.length > 0) register_message(id, pub);
2434
2899
  }
2435
2900
 
@@ -2457,7 +2922,7 @@ function messageFired(channel: MessageChannel, receiver: i32): void {
2457
2922
  * plugin, TypeScript or Pawn:
2458
2923
  *
2459
2924
  * ```ts
2460
- * server.addEventListener("playerchange", (event) => {
2925
+ * server.addEventListener("playerChange", (event) => {
2461
2926
  * print(event.player, event.value ? "You are protected" : "Your spawn protection is over");
2462
2927
  * }, { field: "spawnProtected" });
2463
2928
  * ```
@@ -2492,14 +2957,14 @@ export class PlayerChangeEvent<F extends string = string> {
2492
2957
  /** The third argument of `server.addEventListener`. */
2493
2958
  export interface ServerListenerOptions {
2494
2959
  /**
2495
- * For `"playerchange"`: the field listened for, e.g. `"spawnProtected"`, or
2960
+ * For `"playerChange"`: the field listened for, e.g. `"spawnProtected"`, or
2496
2961
  * an object field's member, `"glow.enabled"`; an object field's name
2497
2962
  * hears each of its members. Left out, every field.
2498
2963
  */
2499
2964
  field?: string;
2500
2965
  }
2501
2966
 
2502
- // This plugin's playerchange listeners. The module wakes the plugin only for
2967
+ // This plugin's playerChange listeners. The module wakes the plugin only for
2503
2968
  // the fields they are for (player_change_listen), and the trampoline hands a
2504
2969
  // change to the ones it concerns, each as its own event class - the field's
2505
2970
  // typed one, which the build makes (scripts/player-fields.ts).
@@ -2613,6 +3078,8 @@ function attachWaitingCvars(event: PluginInitEvent): void {
2613
3078
  waitingHams.length = 0;
2614
3079
  for (let i = 0; i < waitingMessages.length; i++) registerMessage(waitingMessages[i]);
2615
3080
  waitingMessages.length = 0;
3081
+ runWaiting(waitingAtPrecache);
3082
+ runWaiting(waitingAtInit);
2616
3083
  }
2617
3084
  addServerListener<PluginInitEvent>(attachWaitingCvars);
2618
3085
 
@@ -2740,7 +3207,7 @@ export class Cvar {
2740
3207
  * the folders AMX Mod X keeps. Used through `server`:
2741
3208
  *
2742
3209
  * ```ts
2743
- * server.addEventListener("putinserver", (event) => {
3210
+ * server.addEventListener("putInServer", (event) => {
2744
3211
  * print(event.player, "Welcome!"); // event is a PutinserverEvent
2745
3212
  * });
2746
3213
  * server.map; // "de_dust2"
@@ -2754,27 +3221,51 @@ export class Cvar {
2754
3221
  */
2755
3222
  export class Server {
2756
3223
  /**
2757
- * Calls `listener` every time the server raises the event `type` - or,
2758
- * for `"message:<Name>"`, every time it sends that message to a client.
2759
- * `"playerchange"` takes the field it is for: `{ field: "spawnProtected" }`.
3224
+ * Calls `listener` every time the server raises the event `type`.
3225
+ * `"playerChange"` takes the field it is for: `{ field: "spawnProtected" }`.
2760
3226
  */
2761
3227
  addEventListener<K extends keyof ServerEventMap>(type: K, listener: (event: ServerEventMap[K]) => void, options: ServerListenerOptions = {}): void {
2762
- // @ts-ignore: a message's event is told apart by its field, at compile time
2763
- if (isDefined(changetype<ServerEventMap[K]>(0).__message)) addMessageListener<ServerEventMap[K]>(type.slice(8), listener);
2764
- // @ts-ignore: and so is a field's change
2765
- else if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) addPlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
3228
+ // @ts-ignore: a field's change is told apart by its field, at compile time
3229
+ if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) addPlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
2766
3230
  else addServerListener<ServerEventMap[K]>(listener);
2767
3231
  }
2768
3232
 
2769
3233
  /** Stops calling a listener added with `addEventListener` - the same function and the same options. */
2770
3234
  removeEventListener<K extends keyof ServerEventMap>(type: K, listener: (event: ServerEventMap[K]) => void, options: ServerListenerOptions = {}): void {
2771
3235
  // @ts-ignore: as in addEventListener
2772
- if (isDefined(changetype<ServerEventMap[K]>(0).__message)) removeMessageListener<ServerEventMap[K]>(type.slice(8), listener);
2773
- // @ts-ignore: as in addEventListener
2774
- else if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) removePlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
3236
+ if (isDefined(changetype<ServerEventMap[K]>(0).__playerChange)) removePlayerChangeListener<ServerEventMap[K]>(options.field ?? "", listener);
2775
3237
  else removeServerListener<ServerEventMap[K]>(listener);
2776
3238
  }
2777
3239
 
3240
+ /**
3241
+ * Calls `listener` every time the server sends the message `name` to a
3242
+ * client, before it leaves: the listener reads its fields, changes them, or
3243
+ * stops it with `preventDefault()`.
3244
+ *
3245
+ * ```ts
3246
+ * server.addMessageListener("death", (event) => {
3247
+ * if (event.headshot) console.log(`${event.killer?.name} - headshot - ${event.victim?.name}`);
3248
+ * });
3249
+ * ```
3250
+ *
3251
+ * The editor lists the names, each with the game's own one in its words:
3252
+ * `death` is the game's `DeathMsg`. A name may hear a few of the game's
3253
+ * messages that are one thing: `progressBar` is `BarTime` and
3254
+ * `BarTime2`, and `event.name` says which one came.
3255
+ *
3256
+ * Pawn: `register_message`
3257
+ */
3258
+ addMessageListener<K extends keyof ServerMessageMap>(name: K, listener: (event: ServerMessageMap[K]) => void): void {
3259
+ const messages = protocolMessageNames(name);
3260
+ for (let i = 0; i < messages.length; i++) listenToMessage<ServerMessageMap[K]>(messages[i], listener);
3261
+ }
3262
+
3263
+ /** Stops calling a listener added with `addMessageListener` - the same name and the same function. */
3264
+ removeMessageListener<K extends keyof ServerMessageMap>(name: K, listener: (event: ServerMessageMap[K]) => void): void {
3265
+ const messages = protocolMessageNames(name);
3266
+ for (let i = 0; i < messages.length; i++) stopListeningToMessage<ServerMessageMap[K]>(messages[i], listener);
3267
+ }
3268
+
2778
3269
  /**
2779
3270
  * The current map's name, e.g. `"de_dust2"`.
2780
3271
  *
@@ -2793,6 +3284,51 @@ export class Server {
2793
3284
  return get_maxplayers();
2794
3285
  }
2795
3286
 
3287
+ /**
3288
+ * The players on the server, every one connected - never an HLTV proxy -
3289
+ * read anew each time. Narrow them with the array's `filter`:
3290
+ *
3291
+ * ```ts
3292
+ * const alive = server.players.filter(player => player.isAlive);
3293
+ * const cts = server.players.filter(player => player.team === "CT" && !player.isBot);
3294
+ * ```
3295
+ *
3296
+ * Pawn: `get_players`
3297
+ */
3298
+ get players(): Player[] {
3299
+ const ids = playerIds("h");
3300
+ const list: Player[] = [];
3301
+ for (let i = 0; i < ids.length; i++) list.push(new Player(ids[i]));
3302
+ return list;
3303
+ }
3304
+
3305
+ /**
3306
+ * Adds a bot under `name`: a player the server runs, with no game behind
3307
+ * it and no mind of its own - it stands where it spawns until a plugin
3308
+ * moves it with `bot.move()`. `null` when no slot is free. `"putInServer"`
3309
+ * fires for it as for anyone, `bot.isBot` is `true` and `bot.kick()`
3310
+ * removes it.
3311
+ *
3312
+ * ```ts
3313
+ * const bot = server.addBot("Dummy");
3314
+ * bot?.joinTeam("CT");
3315
+ * ```
3316
+ *
3317
+ * Pawn: `engfunc(EngFunc_CreateFakeClient)`, `dllfunc(DLLFunc_ClientConnect)`, `dllfunc(DLLFunc_ClientPutInServer)`
3318
+ */
3319
+ addBot(name: string): Player | null {
3320
+ // The engine takes a slot and marks the client FL_FAKECLIENT; AMX Mod X
3321
+ // connects it then and there. The game is told after, as the engine
3322
+ // tells it of anyone: it connects and puts the player in the server.
3323
+ const id = <i32>engfunc(EngFunc_CreateFakeClient, name);
3324
+ if (id <= 0) return null;
3325
+
3326
+ const rejected = new Ref<string>("");
3327
+ dllfunc(DLLFunc_ClientConnect, id, name, "127.0.0.1", rejected);
3328
+ dllfunc(DLLFunc_ClientPutInServer, id);
3329
+ return new Player(id);
3330
+ }
3331
+
2796
3332
  /**
2797
3333
  * The AMX Mod X configs folder, relative to the game folder, as `fs` takes it:
2798
3334
  * `addons/amxmodx/configs` unless the server moved it.
@@ -2831,27 +3367,34 @@ export class Server {
2831
3367
  }
2832
3368
 
2833
3369
  /**
2834
- * Adds a command players type, e.g. `"/hp"` in chat, or a name without the slash
2835
- * in the console.
3370
+ * The commands this plugin added, players' and the server's, in the order
3371
+ * they were added: each one's `usage`, `description` and `access` - what a
3372
+ * `/help` prints.
2836
3373
  *
2837
3374
  * ```ts
2838
- * server.addCommand("/hp", (player) => print(player, `${player.health} HP`));
2839
- * server.addCommand("/kick", kick, { access: "Kick", description: "Kick a player" });
3375
+ * server.addCommand("/help", ({ player }) => {
3376
+ * for (const command of server.commands) {
3377
+ * if (command.access == null || player.access.includes(command.access)) print(player, command.usage);
3378
+ * }
3379
+ * });
2840
3380
  * ```
2841
- *
2842
- * `args` are the words after the command. A chat command is not repeated in
2843
- * chat, and it does not run for a player without the `access` right.
2844
- * `"say time"` runs when a player writes exactly `"time"` in chat.
2845
- *
2846
- * Pawn: `register_clcmd`
2847
3381
  */
2848
- addCommand(name: string, handler: CommandHandler, options: CommandOptions = {}): void {
3382
+ get commands(): CommandInfo[] {
3383
+ return commandInfos.slice(0);
3384
+ }
3385
+
3386
+ /**
3387
+ * @hidden A player's command, its words read by `run` - the parser the
3388
+ * build writes for each `addCommand` call (scripts/typed-commands.ts).
3389
+ */
3390
+ __addCommand(usage: string, run: (words: __CommandWords) => void, options: CommandOptions = {}): void {
3391
+ const name = commandName(usage);
2849
3392
  const chat = name.startsWith("/") || name.startsWith("say ");
2850
3393
  const access = options.access;
2851
3394
 
2852
3395
  commandNames.push(name.toLowerCase());
2853
- commandHandlers.push(handler);
2854
- commandAccess.push(access ?? "");
3396
+ commandRuns.push(run);
3397
+ commandInfos.push(new CommandInfo(usage, options.description ?? "", access ?? null, false));
2855
3398
 
2856
3399
  if (!chat) {
2857
3400
  const flags = access != null ? ACCESS.bitOf(access) : 0;
@@ -2865,23 +3408,13 @@ export class Server {
2865
3408
  _clcmd("say_team", chatCommand.index, 0, "", SHAPE_WIDE);
2866
3409
  }
2867
3410
 
2868
- /**
2869
- * Adds a command of the server console - typed there, sent over rcon or run by
2870
- * another plugin. Players cannot use it.
2871
- *
2872
- * ```ts
2873
- * server.addServerCommand("myplugin_reset", (args) => reset(args.length > 0 ? args[0] : "all"));
2874
- * ```
2875
- *
2876
- * `args` are the words after the command. It may be added at the top level of
2877
- * the file.
2878
- *
2879
- * Pawn: `register_srvcmd`
2880
- */
2881
- addServerCommand(name: string, handler: ServerCommandHandler): void {
2882
- const lower = name.toLowerCase();
3411
+ /** @hidden A command of the server console, its words read by `run`, as `__addCommand`'s are. */
3412
+ __addServerCommand(usage: string, run: (words: __CommandWords) => void): void {
3413
+ const lower = commandName(usage).toLowerCase();
2883
3414
  serverCommandNames.push(lower);
2884
- serverCommandHandlers.push(handler);
3415
+ serverCommandRuns.push(run);
3416
+ serverCommandUsages.push(usage);
3417
+ commandInfos.push(new CommandInfo(usage, "", null, true));
2885
3418
 
2886
3419
  if (serverUp) registerServerCommand(lower);
2887
3420
  else waitingServerCommands.push(lower);
@@ -2923,7 +3456,7 @@ export const server = new Server();
2923
3456
  * if (event.player.isBot) event.preventDefault();
2924
3457
  * });
2925
3458
  * game.addEventListener("canPlayerHearPlayer", (event) => event.listener.team == event.sender.team);
2926
- * game.addEventListener("flPlayerFallDamage", (event) => event.result / 2, true);
3459
+ * game.addEventListener("fallDamage", (event) => event.result / 2, true);
2927
3460
  * ```
2928
3461
  *
2929
3462
  * The event's type follows from its name. What a listener returns is the
@@ -2932,7 +3465,7 @@ export const server = new Server();
2932
3465
  * nothing leaves it to the game; `event.preventDefault()` blocks without an
2933
3466
  * answer. A value of the wrong type is an error in the editor and in the build.
2934
3467
  *
2935
- * The game rules are its fields: `game.freezePeriod`, `game.numCtWins`,
3468
+ * The game rules are its fields: `game.isFreezeTime`, `game.ctWins`,
2936
3469
  * `game.roundWinner`.
2937
3470
  *
2938
3471
  * Pawn: `RegisterHookChain`, `RegisterHam`, `get_member_game`
@@ -3001,7 +3534,50 @@ export class Game extends GameFields {
3001
3534
  */
3002
3535
  endRound(options: EndRoundOptions): void {
3003
3536
  const status = max(WINNER_NAMES.indexOf(options.winner), 0);
3004
- rg_round_end(options.delay ?? 5.0, status, ROUND_REASONS[status], options.message ?? "default", options.sound ?? "default", options.dispatch ?? false);
3537
+ const delay = options.delay ?? 5.0;
3538
+ const message = options.message ?? "default";
3539
+ const sound = options.sound ?? "default";
3540
+ if (__hasReapi()) {
3541
+ rg_round_end(delay, status, ROUND_REASONS[status], message, sound, options.dispatch ?? false);
3542
+ return;
3543
+ }
3544
+
3545
+ // Without reapi, what rg_round_end does through the game's
3546
+ // TerminateRound: the winner, the moment the next round starts, and the
3547
+ // round marked as ending, so the game does not end it again meanwhile;
3548
+ // then the message and the sound.
3549
+ this.roundWinner = options.winner;
3550
+ this.roundEnding = true;
3551
+ this.newRoundTime = this.time + delay;
3552
+ const text = message == "default" ? ROUND_MESSAGES[status] : message;
3553
+ const radio = sound == "default" ? ROUND_SOUNDS[status] : sound;
3554
+ if (!(options.dispatch ?? false)) {
3555
+ if (text.length > 0) client_print(0, print_center, text);
3556
+ if (radio.length > 0) broadcastAudio(`%!MRAD_${radio}`);
3557
+ return;
3558
+ }
3559
+
3560
+ // Told as the game tells a round's end - the message and the sound
3561
+ // through the engine, where every plugin's hooks hear them, then its
3562
+ // log lines, which the roundEnd listeners hear on plain HLDS - Pawn
3563
+ // plugins' logevents too.
3564
+ if (text.length > 0) {
3565
+ emessage_begin(MSG_ALL, get_user_msgid("TextMsg"));
3566
+ ewrite_byte(print_center);
3567
+ ewrite_string(text);
3568
+ emessage_end();
3569
+ }
3570
+ if (radio.length > 0) {
3571
+ emessage_begin(MSG_ALL, get_user_msgid("SendAudio"));
3572
+ ewrite_byte(0);
3573
+ ewrite_string(`%!MRAD_${radio}`);
3574
+ ewrite_short(100);
3575
+ emessage_end();
3576
+ }
3577
+ const score = `(CT "${this.ctWins}") (T "${this.terroristWins}")`;
3578
+ if (status == 3) elog_message(`World triggered "Round_Draw" ${score}`);
3579
+ else if (status > 0) elog_message(`Team "${status == 1 ? "CT" : "TERRORIST"}" triggered "${status == 1 ? "CTs_Win" : "Terrorists_Win"}" ${score}`);
3580
+ elog_message(`World triggered "Round_End"`);
3005
3581
  }
3006
3582
  }
3007
3583
 
@@ -3028,7 +3604,7 @@ export interface GameListenerOptions {
3028
3604
  */
3029
3605
  export type UseType = "off" | "on" | "set" | "toggle";
3030
3606
 
3031
- /** The options of an entity's action such as `weapon.deploy()` or `entity.takeHealth(...)`. */
3607
+ /** The options of an entity's action such as `weapon.deploy()` or `entity.heal(...)`. */
3032
3608
  export interface ActionOptions {
3033
3609
  /**
3034
3610
  * Whether the game's listeners run too - this plugin's and every other's,
@@ -3086,6 +3662,7 @@ function addTouchListener(listener: TouchListener, toucher: string, touched: str
3086
3662
  }
3087
3663
 
3088
3664
  filter.listeners.push(listener);
3665
+ filter.hook.set(true);
3089
3666
  }
3090
3667
 
3091
3668
  function removeTouchListener(listener: TouchListener, toucher: string, touched: string): void {
@@ -3093,12 +3670,13 @@ function removeTouchListener(listener: TouchListener, toucher: string, touched:
3093
3670
  if (filter == null) return;
3094
3671
  const at = filter.listeners.indexOf(listener);
3095
3672
  if (at >= 0) filter.listeners.splice(at, 1);
3673
+ filter.hook.set(filter.listeners.length > 0);
3096
3674
  }
3097
3675
 
3098
3676
  /** Hands one pair of classes to the engine module; a reload takes the same public back. */
3099
3677
  function registerTouch(filter: TouchFilter): void {
3100
3678
  const fired = (touched: number, toucher: number, c: number, d: number): void => touchFired(filter, touched, toucher);
3101
- const pub = publicFor(fired, `touch:${filter.toucher}:${filter.touched}`);
3679
+ const pub = __switchedPublic(fired, `touch:${filter.toucher}:${filter.touched}`, filter.hook);
3102
3680
  if (pub.length > 0) register_touch(filter.touched, filter.toucher, pub);
3103
3681
  }
3104
3682
 
@@ -3175,15 +3753,15 @@ export { Entity, Weapon, WeaponKind, weaponKindOf } from "./entities";
3175
3753
  // The names an enum field takes and gives: `entity.renderMode = "additive"`.
3176
3754
  export {
3177
3755
  RenderMode, RenderFx, MoveType, Solid, TakeDamage, DeadFlag, WaterLevel, Contents, FixAngle, HitGroup,
3178
- ArmorType, ObserverMode, JoinState, GameMenu, PlayerModel, IgnoredChat, ThrowDirection, BloodColor, MonsterState, MusicState
3756
+ ArmorType, ObserverMode, JoinState, GameMenu, PlayerModel, IgnoredChat, ThrowDirection, BloodColor, MusicState
3179
3757
  } from "./entities";
3180
3758
  import {
3181
3759
  NATIVE_server_cmd, NATIVE_client_cmd, NATIVE_CreateMultiForward, NATIVE_ExecuteForward, get_gametime, get_mapname,
3182
- nvault_open, nvault_set, nvault_remove, rg_round_end
3760
+ nvault_open, nvault_set, nvault_remove, rg_round_end, client_print
3183
3761
  } from "./natives";
3184
3762
  import { ET_IGNORE, ET_STOP, FP_ARRAY, FP_CELL, FP_FLOAT, FP_STRING } from "./constants";
3185
- import { ROUND_NONE, ROUND_CTS_WIN, ROUND_TERRORISTS_WIN, ROUND_END_DRAW } from "./constants";
3186
- import { PluginInitEvent, PluginPrecacheEvent, ServerEventMap, addServerListener, removeServerListener } from "./events";
3763
+ import { ROUND_NONE, ROUND_CTS_WIN, ROUND_TERRORISTS_WIN, ROUND_END_DRAW, print_center } from "./constants";
3764
+ import { PluginInitEvent, PluginPrecacheEvent, ServerEventMap, ServerMessageMap, addServerListener, protocolMessageNames, removeServerListener } from "./events";
3187
3765
 
3188
3766
  function variantOf(variant: VariantName): number {
3189
3767
  if (variant == Variant.center) return 4;
@@ -3394,7 +3972,7 @@ export namespace cvar {
3394
3972
  *
3395
3973
  * Pawn: `register_clcmd`
3396
3974
  */
3397
- export function cmd(pattern: string, handler: Handler, flag: FlagName = "ALL", info: string = ""): void {
3975
+ export function cmd(pattern: string, handler: Handler, flag: FlagName = "all", info: string = ""): void {
3398
3976
  _clcmd(pattern, hostIndex(handler, false), flagOf(flag), info, SHAPE_NARROW);
3399
3977
  }
3400
3978
 
@@ -3405,7 +3983,7 @@ export function cmd(pattern: string, handler: Handler, flag: FlagName = "ALL", i
3405
3983
  *
3406
3984
  * Pawn: `register_clcmd`
3407
3985
  */
3408
- export function cmdWide(pattern: string, handler: WideHandler, flag: FlagName = "ALL", info: string = ""): void {
3986
+ export function cmdWide(pattern: string, handler: WideHandler, flag: FlagName = "all", info: string = ""): void {
3409
3987
  _clcmd(pattern, hostIndex(handler, true), flagOf(flag), info, SHAPE_WIDE);
3410
3988
  }
3411
3989
 
@@ -3513,11 +4091,11 @@ export function clearInterval(handle: number): void {
3513
4091
  @external("env", "call") declare function _call(id: i32, args: i32, mask: i32, argc: i32): i32;
3514
4092
 
3515
4093
  // WebAssembly fixes an import's arity, so a native with a `...` tail cannot be
3516
- // declared in ~/natives at all - only its id is - and the module cannot infer
4094
+ // declared in @amxts/core/natives at all - only its id is - and the module cannot infer
3517
4095
  // the tail's types.
3518
4096
  /**
3519
4097
  * A call of a Pawn native with a `...` tail, built one argument at a time -
3520
- * the low-level way. A native from `~/natives` is an ordinary function and
4098
+ * the low-level way. A native from `@amxts/core/natives` is an ordinary function and
3521
4099
  * needs none of this.
3522
4100
  *
3523
4101
  * new Call(NATIVE_server_print).str("%s").str(text).run();
@@ -3651,6 +4229,28 @@ export class Call {
3651
4229
  return unchecked(this.cells[index]);
3652
4230
  }
3653
4231
 
4232
+ /** The number of arguments added so far: the position the next one takes. */
4233
+ get count(): i32 {
4234
+ return this.n;
4235
+ }
4236
+
4237
+ /**
4238
+ * Adds room for text the native writes, in a `...` tail, holding `text` to
4239
+ * begin with; its length follows by address, as `ret[], len` wants it.
4240
+ * After `run` the text is in `cellsAt` of this position.
4241
+ */
4242
+ textInto(text: string, length: i32): Call {
4243
+ const cells = new StaticArray<i32>(length + 1);
4244
+ __writeCellText(text, cells);
4245
+ this.hold(cells);
4246
+ return this.push(changetype<i32>(cells), 0x62).ref(length); // b
4247
+ }
4248
+
4249
+ /** The cells at the address of the argument at this position, after `run`: a vector or text the native wrote. */
4250
+ cellsAt(index: i32): StaticArray<i32> {
4251
+ return changetype<StaticArray<i32>>(unchecked(this.args[index]));
4252
+ }
4253
+
3654
4254
  /** Calls the native with the arguments added so far and returns its result. */
3655
4255
  run(): number {
3656
4256
  return _call(this.id, changetype<i32>(this.args), changetype<i32>(this.mask), this.n);
@@ -3668,10 +4268,139 @@ function arrayCells(values: number[], floats: bool): StaticArray<i32> {
3668
4268
  return cells;
3669
4269
  }
3670
4270
 
4271
+ // ---------------------------------------------------------------- a native's `...` tail
4272
+
4273
+ /** The longest text a native writes into a `Ref<string>`, in bytes. */
4274
+ const REF_TEXT: i32 = 1023;
4275
+
4276
+ /**
4277
+ * A value a native writes back through its argument, where Pawn passes a
4278
+ * variable for the native to fill: text into `ret[], len`, a number into
4279
+ * `&value`. Give it where the native takes one; after the call, `value` is
4280
+ * what the native wrote.
4281
+ *
4282
+ * ```ts
4283
+ * const reason = new Ref("");
4284
+ * if (!dllfunc(DLLFunc_ClientConnect, id, "Bot", "127.0.0.1", reason)) console.log(reason.value);
4285
+ * ```
4286
+ */
4287
+ export class Ref<T> {
4288
+ constructor(/** The value the native wrote; before the call, the one it starts with. */ public value: T) {}
4289
+
4290
+ /** @hidden Adds this to a call: text as room to write in with its length, a number or a boolean by address. */
4291
+ __push(call: Call, float: bool): void {
4292
+ const value = this.value;
4293
+ if (isString<T>()) call.textInto(changetype<string>(value), REF_TEXT);
4294
+ else if (isBoolean<T>()) call.ref(value ? 1 : 0);
4295
+ else if (isFloat<T>() || isInteger<T>()) call.ref(float ? floatCell(<f64>value) : <i32><f64>value);
4296
+ else ERROR("a Ref holds text, a number or a boolean");
4297
+ }
4298
+
4299
+ /** @hidden Reads what the native wrote at this position of the call. */
4300
+ __back(call: Call, at: i32, float: bool): void {
4301
+ if (isString<T>()) this.value = changetype<T>(cellsToString(call.cellsAt(at)));
4302
+ else if (isBoolean<T>()) this.value = <T>(call.out(at) != 0);
4303
+ else this.value = <T>(float ? cellFloat(call.out(at)) : <f64>call.out(at));
4304
+ }
4305
+ }
4306
+
4307
+ /**
4308
+ * One argument of a native's `...` tail, by its type: a number or a boolean
4309
+ * by address (a number as a Float where `float` says), text as it is, a
4310
+ * Vector or `[x, y, z]` as three Floats, a Player or an Entity as its index,
4311
+ * a Ref as room for what the native writes; nothing for an argument left
4312
+ * out. Each branch is decided as it compiles, so the conditions are the
4313
+ * compiler's own. Returns the argument's position in the call, for tailBack.
4314
+ */
4315
+ function tailArgument<T>(call: Call, value: T, float: bool): i32 {
4316
+ const at = call.count;
4317
+ if (isString<T>()) {
4318
+ call.str(changetype<string>(value));
4319
+ } else if (isBoolean<T>()) {
4320
+ call.ref(value ? 1 : 0);
4321
+ } else if (!isReference<T>()) {
4322
+ call.ref(float ? floatCell(<f64>value) : <i32><f64>value);
4323
+ } else if (isArray<T>()) {
4324
+ // @ts-ignore: valueof is AssemblyScript's
4325
+ if (!isFloat<valueof<T>>()) ERROR("a native's `...` takes a vector as a Vector or [x, y, z]");
4326
+ const values = changetype<number[]>(value);
4327
+ call.vec(values.length > 0 ? values[0] : 0, values.length > 1 ? values[1] : 0, values.length > 2 ? values[2] : 0);
4328
+ // @ts-ignore: a Ref's method, looked for at compile time
4329
+ } else if (isDefined(changetype<T>(0).__push)) {
4330
+ // @ts-ignore: T is a Ref here
4331
+ changetype<T>(value).__push(call, float);
4332
+ // @ts-ignore: a Player's or an Entity's index, looked for at compile time
4333
+ } else if (isDefined(changetype<T>(0).id)) {
4334
+ // @ts-ignore: T has an id here
4335
+ call.ref(value ? <i32>changetype<T>(value).id : 0);
4336
+ } else if (idof<T>() != idof<NoArgument>()) {
4337
+ ERROR("a native's `...` takes a number, a boolean, text, a Vector or [x, y, z], a Player, an Entity or a Ref");
4338
+ }
4339
+ return at;
4340
+ }
4341
+
4342
+ /** What the native wrote back into the argument at `at`: a vector's numbers, a Ref's value. */
4343
+ function tailBack<T>(call: Call, value: T, at: i32, float: bool): void {
4344
+ if (isArray<T>()) {
4345
+ const values = changetype<number[]>(value);
4346
+ const cells = call.cellsAt(at);
4347
+ for (let i = 0; i < min(values.length, 3); i++) values[i] = cellFloat(unchecked(cells[i]));
4348
+ // @ts-ignore: a Ref's method, looked for at compile time
4349
+ } else if (isDefined(changetype<T>(0).__back)) {
4350
+ // @ts-ignore: T is a Ref here
4351
+ changetype<T>(value).__back(call, at, float);
4352
+ }
4353
+ }
4354
+
4355
+ /** @hidden What an argument left out of a native's `...` tail stands at: nothing is sent for it. */
4356
+ export function __noArgument<T>(): T {
4357
+ return zeroOf<T>();
4358
+ }
4359
+
4360
+ /** In a native's float table: its result is a Float. Bit `i` below it: the tail's argument `i` is one. */
4361
+ const TAIL_FLOAT_RESULT: i32 = 1 << 16;
4362
+
4363
+ /**
4364
+ * @hidden A native's `...` tail of up to twelve arguments of any kind, onto
4365
+ * `call`, and the call run: what the generated wrappers of @amxts/core/natives call.
4366
+ * `floats` is the native's float table for this call (bit `i`: the tail's
4367
+ * argument `i` is a Float; TAIL_FLOAT_RESULT: so is the result) - a
4368
+ * plugin's number cannot say whether it is one. What the native wrote into
4369
+ * a vector or a Ref is read back into it.
4370
+ */
4371
+ export function __callTail<A, B, C, D, E, F, G, H, I, J, K, L>(call: Call, floats: i32, a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J, k: K, l: L): number {
4372
+ const at0 = tailArgument<A>(call, a, (floats & 1) != 0);
4373
+ const at1 = tailArgument<B>(call, b, (floats & 2) != 0);
4374
+ const at2 = tailArgument<C>(call, c, (floats & 4) != 0);
4375
+ const at3 = tailArgument<D>(call, d, (floats & 8) != 0);
4376
+ const at4 = tailArgument<E>(call, e, (floats & 16) != 0);
4377
+ const at5 = tailArgument<F>(call, f, (floats & 32) != 0);
4378
+ const at6 = tailArgument<G>(call, g, (floats & 64) != 0);
4379
+ const at7 = tailArgument<H>(call, h, (floats & 128) != 0);
4380
+ const at8 = tailArgument<I>(call, i, (floats & 256) != 0);
4381
+ const at9 = tailArgument<J>(call, j, (floats & 512) != 0);
4382
+ const at10 = tailArgument<K>(call, k, (floats & 1024) != 0);
4383
+ const at11 = tailArgument<L>(call, l, (floats & 2048) != 0);
4384
+ const result = call.run();
4385
+ tailBack<A>(call, a, at0, (floats & 1) != 0);
4386
+ tailBack<B>(call, b, at1, (floats & 2) != 0);
4387
+ tailBack<C>(call, c, at2, (floats & 4) != 0);
4388
+ tailBack<D>(call, d, at3, (floats & 8) != 0);
4389
+ tailBack<E>(call, e, at4, (floats & 16) != 0);
4390
+ tailBack<F>(call, f, at5, (floats & 32) != 0);
4391
+ tailBack<G>(call, g, at6, (floats & 64) != 0);
4392
+ tailBack<H>(call, h, at7, (floats & 128) != 0);
4393
+ tailBack<I>(call, i, at8, (floats & 256) != 0);
4394
+ tailBack<J>(call, j, at9, (floats & 512) != 0);
4395
+ tailBack<K>(call, k, at10, (floats & 1024) != 0);
4396
+ tailBack<L>(call, l, at11, (floats & 2048) != 0);
4397
+ return (floats & TAIL_FLOAT_RESULT) != 0 ? cellFloat(result) : result;
4398
+ }
4399
+
3671
4400
  // ---------------------------------------------------------------- field natives
3672
4401
 
3673
4402
  // What a field of reapi's field natives - get_entvar, get_member, get_pmove -
3674
- // holds, as the generated __<native>_kind tables of ~/natives say
4403
+ // holds, as the generated __<native>_kind tables of @amxts/core/natives say
3675
4404
  // (scripts/generate-wasm-api.ts): a whole number, a Float, a vector or text,
3676
4405
  // and FIELD_ELEMENT on top for an array member.
3677
4406
  const FIELD_FLOAT: i32 = 1;
@@ -3834,6 +4563,58 @@ export interface ModuleOptions {}
3834
4563
  return definition;
3835
4564
  }
3836
4565
 
4566
+ // A shared module's side of the calls other plugins make to it: as/remote.ts
4567
+ // sets the plugin whose call runs and tells of a plugin that stopped. Here,
4568
+ // not there, so that the kit's words reach the editor.
4569
+ let callingRun: i32 = 0;
4570
+ const stopListeners: ((plugin: number) => void)[] = [];
4571
+
4572
+ /**
4573
+ * The plugin whose call the module runs now, as a number: what the module
4574
+ * keeps for that plugin - a menu it made, a function it gave - is marked with
4575
+ * it, and dropped when `onPluginStop()` gives the same number. `0` when no
4576
+ * other plugin's call runs: the module's own plugin, its natives for Pawn
4577
+ * plugins, its events and timers.
4578
+ *
4579
+ * ```ts
4580
+ * export function addRule(test: Rule) {
4581
+ * rules.push({ test, from: callingPlugin() });
4582
+ * }
4583
+ * ```
4584
+ */
4585
+ export function callingPlugin(): number {
4586
+ return callingRun;
4587
+ }
4588
+
4589
+ /**
4590
+ * Calls `listener` when a plugin that called the module stops - unloaded,
4591
+ * reloaded, or its load failed - with the number `callingPlugin()` gave
4592
+ * during its calls. The module drops what that plugin gave it: a reloaded
4593
+ * plugin is a new one, which gives everything again, and a function of the
4594
+ * one that stopped answers nothing.
4595
+ *
4596
+ * ```ts
4597
+ * onPluginStop((plugin) => {
4598
+ * rules = rules.filter(rule => rule.from != plugin);
4599
+ * });
4600
+ * ```
4601
+ */
4602
+ export function onPluginStop(listener: (plugin: number) => void): void {
4603
+ stopListeners.push(listener);
4604
+ }
4605
+
4606
+ /** @hidden as/remote.ts: a call of the run `from` starts; the run whose call ran before it, to give back. */
4607
+ export function __callFrom(from: i32): i32 {
4608
+ const outer = callingRun;
4609
+ callingRun = from;
4610
+ return outer;
4611
+ }
4612
+
4613
+ /** @hidden as/remote.ts: the plugin of the run `run` stopped. */
4614
+ export function __pluginStopped(run: i32): void {
4615
+ for (const listener of stopListeners) listener(run);
4616
+ }
4617
+
3837
4618
  // ---------------------------------------------------------------- forwards
3838
4619
 
3839
4620
  // @ts-ignore: decorator
@@ -3881,6 +4662,20 @@ const WINNER_NAMES: RoundWinner[] = ["none", "CT", "TERRORIST", "draw"];
3881
4662
  /** The reason a round ends with, by WinStatus number: rg_round_end's `event`. */
3882
4663
  const ROUND_REASONS: i32[] = [ROUND_NONE, ROUND_CTS_WIN, ROUND_TERRORISTS_WIN, ROUND_END_DRAW];
3883
4664
 
4665
+ // The game's message and radio phrase for a round's end, by WinStatus
4666
+ // number: what rg_round_end's "default" stands for.
4667
+ const ROUND_MESSAGES: string[] = ["", "#CTs_Win", "#Terrorists_Win", "#Round_Draw"];
4668
+ const ROUND_SOUNDS: string[] = ["", "ctwin", "terwin", "rounddraw"];
4669
+
4670
+ /** A radio phrase to everyone, as the game broadcasts the round's end. */
4671
+ function broadcastAudio(sample: string): void {
4672
+ message_begin(MSG_ALL, get_user_msgid("SendAudio"), [0, 0, 0], 0);
4673
+ write_byte(0);
4674
+ write_string(sample);
4675
+ write_short(100);
4676
+ message_end();
4677
+ }
4678
+
3884
4679
  /** A name that crosses as a number - a Team or a RoundWinner - as that number. */
3885
4680
  function nameCell(name: string, crossing: string): i32 {
3886
4681
  if (crossing == CROSS_WINNER) return max(WINNER_NAMES.indexOf(name as RoundWinner), 0);
@@ -4372,6 +5167,8 @@ import { GameAnswerMap, GameEventMap, addGameListener, removeGameListener } from
4372
5167
  // Three numbers with vector math - what entity.origin and the rest return.
4373
5168
  export * from "./vector";
4374
5169
  export * from "./effects";
5170
+ // fetch, URL and useFetch, over the module's network client.
5171
+ export * from "./fetch";
4375
5172
  export { EntityFilter } from "./entities";
4376
5173
 
4377
5174
  // =============================================================================
@@ -4637,3 +5434,226 @@ export function showMenu(id: number, keys: number, text: string, title: string):
4637
5434
  // game's 175 bytes itself.
4638
5435
  show_menu(id, keys, menuColors(text), -1, title);
4639
5436
  }
5437
+
5438
+ // ---------------------------------------------------------------- menus
5439
+
5440
+ // AMX Mod X's own menus (newmenus) as an object: `new Menu(title)`, items
5441
+ // with a title, a test for when each is shown and when it can be chosen, and
5442
+ // what choosing it does; `show(player, data)`. AMX Mod X draws the pages,
5443
+ // Back, More and Exit.
5444
+ //
5445
+ // What a menu shows depends on the player and the data it is shown with, so
5446
+ // each show() builds AMX Mod X's menu afresh - the title and every item's
5447
+ // title, `visible` and `enabled` asked then, for that player - and the menu
5448
+ // is destroyed when the player chooses or leaves it: the handler AMX Mod X
5449
+ // calls once per display destroys it before it runs the item. Every menu of
5450
+ // a plugin answers through one public, which finds the item by the menu's id;
5451
+ // a grey item is one an item callback that always says ITEM_DISABLED draws.
5452
+ // A menu left open by a plugin a reload took away answers the new one's
5453
+ // public, which knows nothing of it and destroys it.
5454
+ import { menu_additem, menu_create, menu_destroy, menu_display, menu_makecallback, NATIVE_menu_setprop } from "./natives";
5455
+ import {
5456
+ ITEM_DISABLED, MEXIT_NEVER, MPROP_BACKNAME, MPROP_EXIT, MPROP_EXITNAME, MPROP_NEXTNAME, MPROP_NUMBER_COLOR, MPROP_PERPAGE
5457
+ } from "./constants";
5458
+
5459
+ /** A colour of a menu's item numbers: a menu's colour tag, `"!y"`, `"!r"`, `"!d"` or `"!w"`. */
5460
+ export type MenuColor = "!y" | "!r" | "!d" | "!w";
5461
+
5462
+ /** The options of a `Menu`: its pages and the texts of its own items. */
5463
+ export interface MenuOptions {
5464
+ /**
5465
+ * Items on a page, `7` at most: Back, More and Exit go below them. `0`
5466
+ * puts every item on one page, without Back and More - `10` at most.
5467
+ *
5468
+ * Pawn: `MPROP_PERPAGE`
5469
+ */
5470
+ perPage?: number;
5471
+ /**
5472
+ * Whether the menu has an Exit item; `true` by default.
5473
+ *
5474
+ * Pawn: `MPROP_EXIT`
5475
+ */
5476
+ exit?: boolean;
5477
+ /**
5478
+ * The Back item's text; AMX Mod X's `"Back"`, in the player's language, by default.
5479
+ *
5480
+ * Pawn: `MPROP_BACKNAME`
5481
+ */
5482
+ backText?: string;
5483
+ /**
5484
+ * The More item's text; AMX Mod X's `"More"` by default.
5485
+ *
5486
+ * Pawn: `MPROP_NEXTNAME`
5487
+ */
5488
+ nextText?: string;
5489
+ /**
5490
+ * The Exit item's text; AMX Mod X's `"Exit"` by default.
5491
+ *
5492
+ * Pawn: `MPROP_EXITNAME`
5493
+ */
5494
+ exitText?: string;
5495
+ /**
5496
+ * The colour of the item numbers; `"!r"`, red, by default.
5497
+ *
5498
+ * Pawn: `MPROP_NUMBER_COLOR`
5499
+ */
5500
+ numberColor?: MenuColor;
5501
+ }
5502
+
5503
+ /** The context a menu's functions get: the player it is shown to, the menu and the data it was shown with. */
5504
+ export interface MenuContext<Data extends object = object> {
5505
+ /** The player the menu is shown to. */
5506
+ player: Player;
5507
+ /** The menu itself: `menu.show(player, data)` keeps it open after a choice. */
5508
+ menu: Menu<Data>;
5509
+ /** The data `show` was given. */
5510
+ data: Data;
5511
+ }
5512
+
5513
+ /** An item of a `Menu`: its title, when it is shown and can be chosen, and what choosing it does. */
5514
+ export interface MenuItemOptions<Data extends object = object> {
5515
+ /** The item's text - or a function that gives it for the player it is shown to. */
5516
+ title: string | ((context: MenuContext<Data>) => string);
5517
+ /** Whether the player can choose it; one he cannot is drawn grey and does nothing. `true` by default. */
5518
+ enabled?: boolean | ((context: MenuContext<Data>) => boolean);
5519
+ /** Whether it is shown at all; a hidden item takes no place. `true` by default. */
5520
+ visible?: boolean | ((context: MenuContext<Data>) => boolean);
5521
+ /** The item's action, run when the player chooses it. The menu closes, unless this shows it again. */
5522
+ onSelect: (context: MenuContext<Data>) => void;
5523
+ }
5524
+
5525
+ // The menus on players' screens, by AMX Mod X's id: what choosing item N of
5526
+ // each runs.
5527
+ const shownMenus = new Map<i32, (item: i32) => void>();
5528
+ let menuHandler = "";
5529
+ let greyItem: i32 = -1;
5530
+
5531
+ /** A player chose an item of a menu this plugin showed - or left it, an item below `0`. */
5532
+ function menuChosen(player: number, menu: number, item: number, unused: number): void {
5533
+ const id = <i32>menu;
5534
+ const choose = shownMenus.has(id) ? shownMenus.get(id) : null;
5535
+ shownMenus.delete(id);
5536
+ menu_destroy(id);
5537
+
5538
+ if (choose != null && item >= 0) {
5539
+ const ambient = __co_ambient_player;
5540
+ __co_ambient_player = <i32>player;
5541
+ choose(<i32>item);
5542
+ __co_ambient_player = ambient;
5543
+ }
5544
+ handled();
5545
+ }
5546
+
5547
+ /** An item AMX Mod X draws grey: the item callback of the items that cannot be chosen. */
5548
+ function itemDisabled(player: number, menu: number, item: number, unused: number): void {
5549
+ ret(ITEM_DISABLED);
5550
+ }
5551
+
5552
+ /** Sets a menu property that is text, when it is given, its colour tags made the game's codes. */
5553
+ function setMenuText(menu: i32, prop: i32, text: string | undefined): void {
5554
+ if (text !== undefined) new Call(NATIVE_menu_setprop).num(menu).num(prop).str(menuColors(text)).run();
5555
+ }
5556
+
5557
+ /** A text given as `string | ((context) => string)`: the compiler holds it as the function, a string as one that returns it. */
5558
+ function menuText<C>(text: string | ((context: C) => string), context: C): string {
5559
+ // @ts-ignore: a function here - the editor sees the union
5560
+ return menuColors(text(context));
5561
+ }
5562
+
5563
+ /** A test given as `boolean | ((context) => boolean)`, held as the function as a text is; left out, it says yes. */
5564
+ function menuTest<C>(test: boolean | ((context: C) => boolean) | undefined, context: C): boolean {
5565
+ // @ts-ignore: a function or null here - the editor sees the union
5566
+ return test == null || test(context);
5567
+ }
5568
+
5569
+ /**
5570
+ * A menu of AMX Mod X's own: items a player picks with the number keys, on
5571
+ * pages with Back and More, and Exit. `Data` is what it is shown with, which
5572
+ * its functions get beside the player.
5573
+ *
5574
+ * ```ts
5575
+ * interface ShopData {
5576
+ * category: string;
5577
+ * }
5578
+ *
5579
+ * const shop = new Menu<ShopData>("!yShop");
5580
+ * shop.addItem({
5581
+ * title: "Armor - $1000",
5582
+ * enabled: ({ player }) => player.armor < 100,
5583
+ * onSelect: ({ player }) => {
5584
+ * player.armor = 100;
5585
+ * },
5586
+ * });
5587
+ * shop.show(player, { category: "armor" });
5588
+ * ```
5589
+ *
5590
+ * The title and each item's title, `visible` and `enabled` are asked at
5591
+ * every `show`, for that player. Colour tags as in `showMenu`: `!y` yellow,
5592
+ * `!r` red, `!d` grey, `!w` white, `!R` to the right edge.
5593
+ *
5594
+ * Pawn: `menu_create`, `menu_setprop`
5595
+ */
5596
+ export class Menu<Data extends object = object> {
5597
+ private readonly items: MenuItemOptions<Data>[] = [];
5598
+
5599
+ constructor(
5600
+ private readonly title: string | ((context: MenuContext<Data>) => string),
5601
+ private readonly options: MenuOptions = {}
5602
+ ) {
5603
+ // Asked for now, not at the first show: after a reload, a menu the last
5604
+ // start left open answers here, and is destroyed.
5605
+ if (menuHandler.length == 0) menuHandler = callbackName(menuChosen, "menu");
5606
+ }
5607
+
5608
+ /**
5609
+ * Adds an item: its title, when it is shown and can be chosen, and what
5610
+ * choosing it does.
5611
+ *
5612
+ * ```ts
5613
+ * shop.addItem({
5614
+ * title: ({ player }) => `Heal (${player.health} HP)`,
5615
+ * visible: ({ player }) => player.isAlive,
5616
+ * enabled: ({ player }) => player.health < 100,
5617
+ * onSelect: ({ player }) => {
5618
+ * player.health = 100;
5619
+ * },
5620
+ * });
5621
+ * ```
5622
+ *
5623
+ * Pawn: `menu_additem`
5624
+ */
5625
+ addItem(item: MenuItemOptions<Data>): void {
5626
+ this.items.push(item);
5627
+ }
5628
+
5629
+ /**
5630
+ * Shows the menu to a player, with the data its functions get; it closes
5631
+ * when he chooses an item or leaves it.
5632
+ *
5633
+ * Pawn: `menu_display`
5634
+ */
5635
+ show(player: Player, data: Data | null = null): void {
5636
+ const context: MenuContext<Data> = { player, menu: this, data: changetype<Data>(data) };
5637
+ const id = menu_create(menuText(this.title, context), menuHandler);
5638
+
5639
+ const options = this.options;
5640
+ const perPage = options.perPage;
5641
+ if (perPage !== undefined) new Call(NATIVE_menu_setprop).num(id).num(MPROP_PERPAGE).ref(perPage).run();
5642
+ if (options.exit == false) new Call(NATIVE_menu_setprop).num(id).num(MPROP_EXIT).ref(MEXIT_NEVER).run();
5643
+ setMenuText(id, MPROP_BACKNAME, options.backText);
5644
+ setMenuText(id, MPROP_NEXTNAME, options.nextText);
5645
+ setMenuText(id, MPROP_EXITNAME, options.exitText);
5646
+ setMenuText(id, MPROP_NUMBER_COLOR, options.numberColor);
5647
+
5648
+ const shown = this.items.filter((item: MenuItemOptions<Data>) => menuTest(item.visible, context));
5649
+ for (let i = 0; i < shown.length; i++) {
5650
+ const item = shown[i];
5651
+ const enabled = menuTest(item.enabled, context);
5652
+ if (!enabled && greyItem < 0) greyItem = menu_makecallback(callbackName(itemDisabled, "menu-grey"));
5653
+ menu_additem(id, menuText(item.title, context), "", 0, enabled ? -1 : greyItem);
5654
+ }
5655
+
5656
+ shownMenus.set(id, (item: i32) => shown[item].onSelect(context));
5657
+ menu_display(player.id, id);
5658
+ }
5659
+ }