@amxts/core 0.1.0 → 0.2.0
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.
- package/README.md +18 -23
- package/README.ru.md +19 -22
- package/as/amxts.d.ts +63 -3
- package/as/constants.ts +684 -116
- package/as/entities.ts +1905 -1364
- package/as/events.ts +1694 -905
- package/as/facade.ts +1240 -220
- package/as/fetch.ts +1519 -0
- package/as/flags.ts +159 -145
- package/as/hlds.ts +1071 -0
- package/as/hooks.ts +5226 -5379
- package/as/kit.ts +15 -5
- package/as/lib/check.ts +1 -1
- package/as/natives.ts +2658 -2499
- package/as/remote.ts +39 -15
- package/as/tsconfig.json +6 -2
- package/as/vector.ts +12 -9
- package/includes/order.txt +22 -29
- package/includes/sources.json +0 -8
- package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
- package/package.json +29 -7
- package/runtime/deps/assemblyscript/dist/asc.js +86 -16
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
- package/runtime/licenses/MPL-2.0.txt +373 -0
- package/runtime/licenses/README.md +5 -1
- package/runtime/licenses/curl-COPYING +22 -0
- package/runtime/licenses/libssh2-COPYING +43 -0
- package/runtime/licenses/mbedTLS-LICENSE +553 -0
- package/runtime/natives.txt +108 -105
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
- package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
- package/scripts/apply-docs.ts +2 -2
- package/scripts/auto-imports.ts +2 -2
- package/scripts/build-identity.ts +115 -0
- package/scripts/build-wasm.ts +101 -50
- package/scripts/check.ts +17 -3
- package/scripts/client-messages.ts +246 -0
- package/scripts/compile-pool.ts +128 -0
- package/scripts/compile-worker.ts +23 -0
- package/scripts/compile.ts +156 -59
- package/scripts/docker-server.ts +13 -3
- package/scripts/docs/as/amxts.ts +86 -0
- package/scripts/docs/as/facade.ts +688 -188
- package/scripts/docs/as/fetch.ts +805 -0
- package/scripts/docs/entities.ts +43 -144
- package/scripts/docs/events.ts +15 -26
- package/scripts/docs/game.ts +68 -22
- package/scripts/docs/messages.ts +330 -16
- package/scripts/editor-api.ts +101 -0
- package/scripts/hlds-events.ts +370 -0
- package/scripts/includes.ts +12 -10
- package/scripts/player-fields.ts +16 -15
- package/scripts/prebuilt.ts +65 -23
- package/scripts/prepare.ts +39 -28
- package/scripts/project.ts +120 -20
- package/scripts/reapi-events.ts +98 -0
- package/scripts/shared-modules.ts +85 -23
- package/scripts/source-map.ts +242 -0
- package/scripts/system.ts +1 -1
- package/scripts/typed-commands.ts +263 -0
- package/scripts/typed-configs.ts +23 -12
- package/scripts/ui.ts +3 -1
- package/scripts/upgrade-menus.ts +274 -0
- package/scripts/upgrade-names.ts +384 -0
- package/scripts/upgrade.ts +830 -0
- package/src/cli-api.mjs +37 -4
- package/src/system.mjs +74 -3
- package/src/testing/compile.ts +8 -5
- package/src/testing/natives.ts +199 -5
- package/src/testing/network.ts +531 -0
- package/src/testing/server.ts +565 -47
- package/src/testing/tables.ts +25 -0
- package/as/modules/http.ts +0 -184
- 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
|
|
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 `"
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1143
|
-
|
|
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 `"
|
|
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("
|
|
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
|
|
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("
|
|
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`; `
|
|
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
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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.
|
|
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 (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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("
|
|
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 (
|
|
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 (
|
|
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`. `
|
|
1736
|
-
*
|
|
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
|
-
|
|
1761
|
-
|
|
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
|
|
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
|
|
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
|
|
1776
|
-
const
|
|
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
|
-
* [`"
|
|
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 =
|
|
1800
|
-
if (access
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
2178
|
+
function runCommand(at: i32, id: i32, words: __CommandWords): void {
|
|
1832
2179
|
const ambient = __co_ambient_player;
|
|
1833
2180
|
__co_ambient_player = id;
|
|
1834
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
for (let i = 0; i < 4; i++)
|
|
2088
|
-
|
|
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
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
2114
|
-
|
|
2466
|
+
ewrite_byte(status);
|
|
2467
|
+
ewrite_string(sprite);
|
|
2115
2468
|
if (status != 0) {
|
|
2116
|
-
for (let i = 0; i < 3; i++)
|
|
2469
|
+
for (let i = 0; i < 3; i++) ewrite_byte(i < color.length ? <i32>color[i] : 0);
|
|
2117
2470
|
}
|
|
2118
|
-
|
|
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
|
-
|
|
2131
|
-
|
|
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
|
-
|
|
2143
|
-
|
|
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
|
-
|
|
2154
|
-
|
|
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
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
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.
|
|
2270
|
-
* console.log(`${event.args.
|
|
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.
|
|
2724
|
+
* server.addMessageListener("text", (event) => {
|
|
2319
2725
|
* if (event.text == "#Round_Draw") event.preventDefault();
|
|
2320
2726
|
* });
|
|
2321
2727
|
* ```
|
|
2322
2728
|
*
|
|
2323
|
-
* A message
|
|
2324
|
-
*
|
|
2325
|
-
*
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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("
|
|
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 `"
|
|
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
|
|
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("
|
|
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
|
|
2758
|
-
*
|
|
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
|
|
2763
|
-
if (isDefined(changetype<ServerEventMap[K]>(0).
|
|
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).
|
|
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
|
-
*
|
|
2835
|
-
*
|
|
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("/
|
|
2839
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
2854
|
-
|
|
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
|
-
|
|
2870
|
-
|
|
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
|
-
|
|
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("
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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,
|
|
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 = "
|
|
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 = "
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|