@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/src/testing/server.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { compile } from './compile';
|
|
|
16
16
|
import { Coroutines, nextTaskId } from './coroutines';
|
|
17
17
|
import { installKitFor } from './kits';
|
|
18
18
|
import { bitsFloat, floatBits, Memory, utf8Fit } from './memory';
|
|
19
|
-
import { NATIVES, WEAPON_NAMES } from './natives';
|
|
19
|
+
import { fieldCell, NATIVES, setFieldCell, WEAPON_NAMES } from './natives';
|
|
20
|
+
import { FakeNetwork } from './network';
|
|
20
21
|
import { constant, tables } from './tables';
|
|
21
22
|
|
|
22
23
|
declare const WebAssembly: any;
|
|
@@ -64,6 +65,8 @@ interface Handler {
|
|
|
64
65
|
shape: number;
|
|
65
66
|
/** A closure's number: fn is then the plugin's dispatcher, called with it first (Handler.tag in module.cpp). */
|
|
66
67
|
tag?: number;
|
|
68
|
+
/** on_cell's filter: the handler hears the forward only when this argument is `value`. */
|
|
69
|
+
where?: { arg: number; value: number };
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/** A host public standing in for a handler: `__amxts_cb<index>`. */
|
|
@@ -72,6 +75,12 @@ interface Slot extends Handler {
|
|
|
72
75
|
/** What the call answers when the handler says nothing. */
|
|
73
76
|
fallback: number;
|
|
74
77
|
key: string;
|
|
78
|
+
/**
|
|
79
|
+
* Switched off while what it delivers has no listener - by the plugin
|
|
80
|
+
* (slot_on), or a hook by DisableHookChain and DisableHamForward: a call
|
|
81
|
+
* answers the fallback without reaching the plugin.
|
|
82
|
+
*/
|
|
83
|
+
off?: boolean;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
interface Task {
|
|
@@ -94,7 +103,7 @@ interface Cvar {
|
|
|
94
103
|
export interface Chain {
|
|
95
104
|
shape: HookShape;
|
|
96
105
|
answer: Value;
|
|
97
|
-
args:
|
|
106
|
+
args: HookArg[];
|
|
98
107
|
/** SetHookChainReturn ran: reapi lets a chain that answers be stopped only then. */
|
|
99
108
|
answered?: boolean;
|
|
100
109
|
}
|
|
@@ -106,9 +115,12 @@ export interface HookResult {
|
|
|
106
115
|
/** The chain's answer after every listener: a number, a boolean or text, by the chain. */
|
|
107
116
|
result: Value;
|
|
108
117
|
/** The arguments as the listeners left them (`event.damage = 10`). */
|
|
109
|
-
args:
|
|
118
|
+
args: HookArg[];
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
/** A hookchain's argument: a number, a boolean, text, or a vector as `[x, y, z]`. */
|
|
122
|
+
export type HookArg = Value | number[];
|
|
123
|
+
|
|
112
124
|
/** A user message a plugin sent: ScreenFade, StatusIcon, ... */
|
|
113
125
|
export interface UserMessage {
|
|
114
126
|
name: string;
|
|
@@ -129,6 +141,36 @@ export interface SentForward {
|
|
|
129
141
|
args: (Value | number[])[];
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
/** A menu of AMX Mod X's own, made by menu_create, as the fake keeps it. */
|
|
145
|
+
export interface FakeMenu {
|
|
146
|
+
id: number;
|
|
147
|
+
title: string;
|
|
148
|
+
/** The public menu_create was given: called with (player, menu, item). */
|
|
149
|
+
handler: Slot;
|
|
150
|
+
/** Each item's name and its callback's number from menu_makecallback, or -1. */
|
|
151
|
+
items: { name: string; callback: number }[];
|
|
152
|
+
/** MPROP_PERPAGE: `0` is one page, without Back and More. */
|
|
153
|
+
perPage: number;
|
|
154
|
+
/** MPROP_EXIT is not MEXIT_NEVER. */
|
|
155
|
+
exit: boolean;
|
|
156
|
+
back: string;
|
|
157
|
+
next: string;
|
|
158
|
+
exitName: string;
|
|
159
|
+
numberColor: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A menu of AMX Mod X's own on a player's screen, as menu_display drew it. */
|
|
163
|
+
export interface MenuScreen {
|
|
164
|
+
menu: number;
|
|
165
|
+
page: number;
|
|
166
|
+
/** What he sees, with the game's colour codes: `\r1.\w Armor`. */
|
|
167
|
+
text: string;
|
|
168
|
+
/** The keys that answer: 1-9 and 0. */
|
|
169
|
+
keys: number[];
|
|
170
|
+
/** The keys of the items drawn grey, which do nothing. */
|
|
171
|
+
disabled: number[];
|
|
172
|
+
}
|
|
173
|
+
|
|
132
174
|
/** One line a player was shown. */
|
|
133
175
|
export interface Message {
|
|
134
176
|
variant: 'chat' | 'center' | 'console' | 'notify' | 'hud';
|
|
@@ -147,12 +189,14 @@ export interface ServerOptions {
|
|
|
147
189
|
/** Files in the game folder before the plugin loads, by path: `{ "addons/amxmodx/configs/x.ini": "..." }`. */
|
|
148
190
|
files?: Record<string, string>;
|
|
149
191
|
/**
|
|
150
|
-
* The system the server runs on, as
|
|
192
|
+
* The system the server runs on, as `@amxts/core/os` finds it: "win32" puts the amxts
|
|
151
193
|
* module's amxts_amxx.dll in the modules folder. "linux" unless said.
|
|
152
194
|
*/
|
|
153
195
|
platform?: 'win32' | 'linux';
|
|
154
196
|
/** The server's time zone, what Date's local getters read: "Asia/Yerevan". This machine's unless said. */
|
|
155
197
|
timeZone?: string;
|
|
198
|
+
/** Whether reapi finds Reunion, so a player's `authType`, `protocol` and `authKey` are his own. Not unless said. */
|
|
199
|
+
reunion?: boolean;
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
export type TeamName = 'UNASSIGNED' | 'TERRORIST' | 'CT' | 'SPECTATOR';
|
|
@@ -165,8 +209,15 @@ export interface JoinOptions {
|
|
|
165
209
|
armor?: number;
|
|
166
210
|
alive?: boolean;
|
|
167
211
|
bot?: boolean;
|
|
168
|
-
|
|
212
|
+
/** His SteamID: "STEAM_0:0:<id>" unless said, "BOT" for a bot. */
|
|
213
|
+
steamId?: string;
|
|
169
214
|
ip?: string;
|
|
215
|
+
/** How Reunion says his game proved who he is, by its name in the player's API: "steam" unless said. */
|
|
216
|
+
authType?: string;
|
|
217
|
+
/** His game's protocol, as Reunion says: 48 unless said. */
|
|
218
|
+
protocol?: number;
|
|
219
|
+
/** The key Reunion read from his game: "" unless said. */
|
|
220
|
+
authKey?: string;
|
|
170
221
|
/** Admin flags as users.ini writes them: "abcdefghijklmnopqrstu". "z" - a plain user - unless said. */
|
|
171
222
|
flags?: string;
|
|
172
223
|
origin?: number[];
|
|
@@ -176,11 +227,16 @@ export interface JoinOptions {
|
|
|
176
227
|
|
|
177
228
|
const SHAPE_NARROW = 0;
|
|
178
229
|
const SHAPE_WIDE = 1;
|
|
230
|
+
/** A slot taken back after a reload: SLOT_REUSED in module.cpp. */
|
|
231
|
+
const SLOT_REUSED = 0x10000;
|
|
232
|
+
/** A request that tells an owner a plugin which called it stopped: RPC_GONE in module.cpp, KIND_GONE in as/remote.ts. */
|
|
233
|
+
const RPC_GONE = 2;
|
|
179
234
|
const PLUGIN_HANDLED = 1;
|
|
180
235
|
const HC_SUPERCEDE = 1;
|
|
181
236
|
const HC_BREAK = 2;
|
|
182
|
-
// A Ham Sandwich hook's answer that blocks the game's function.
|
|
237
|
+
// A Ham Sandwich hook's answer that blocks the game's function, and fakemeta's.
|
|
183
238
|
const HAM_SUPERCEDE = 4;
|
|
239
|
+
const FMRES_SUPERCEDE = 4;
|
|
184
240
|
// A message argument's types, as get_msg_argtype answers.
|
|
185
241
|
const ARG_BYTE = 1;
|
|
186
242
|
const ARG_COORD = 6;
|
|
@@ -266,8 +322,11 @@ export class FakePlayer extends FakeEntity {
|
|
|
266
322
|
connected = true;
|
|
267
323
|
alive: boolean;
|
|
268
324
|
readonly bot: boolean;
|
|
269
|
-
readonly
|
|
325
|
+
readonly steamId: string;
|
|
270
326
|
readonly ip: string;
|
|
327
|
+
readonly authType: string;
|
|
328
|
+
readonly protocol: number;
|
|
329
|
+
readonly authKey: string;
|
|
271
330
|
/** Admin flags as bits: ADMIN_* . */
|
|
272
331
|
flags: number;
|
|
273
332
|
/** SPEAK_* flags: `muted` is SPEAK_MUTED. */
|
|
@@ -280,14 +339,22 @@ export class FakePlayer extends FakeEntity {
|
|
|
280
339
|
readonly commands: string[] = [];
|
|
281
340
|
/** His userinfo keys - `lang` says his language. */
|
|
282
341
|
readonly info = new Map<string, string>();
|
|
342
|
+
/** The menu of AMX Mod X's own on his screen (menu_display), or null; `menuselect <key>` answers it. */
|
|
343
|
+
menu: MenuScreen | null = null;
|
|
344
|
+
/** His userid, the number `#12` names him by in a command: the server counts them up as players come. */
|
|
345
|
+
readonly userid: number;
|
|
283
346
|
|
|
284
347
|
constructor(server: FakeServer, id: number, public name: string, options: JoinOptions) {
|
|
285
348
|
super(server, id, 'player');
|
|
286
349
|
this.alive = options.alive ?? true;
|
|
287
350
|
this.bot = options.bot ?? false;
|
|
288
|
-
this.
|
|
351
|
+
this.steamId = options.steamId ?? (this.bot ? 'BOT' : `STEAM_0:0:${id}`);
|
|
289
352
|
this.ip = options.ip ?? `127.0.0.${id}:27005`;
|
|
353
|
+
this.authType = options.authType ?? 'steam';
|
|
354
|
+
this.protocol = options.protocol ?? 48;
|
|
355
|
+
this.authKey = options.authKey ?? '';
|
|
290
356
|
this.flags = flagBits(options.flags ?? 'z');
|
|
357
|
+
this.userid = server.nextUserid();
|
|
291
358
|
this.team = options.team ?? 'CT';
|
|
292
359
|
this.health = options.health ?? 100;
|
|
293
360
|
this.armor = options.armor ?? 0;
|
|
@@ -408,9 +475,14 @@ export class FakePlayer extends FakeEntity {
|
|
|
408
475
|
return false;
|
|
409
476
|
}
|
|
410
477
|
|
|
411
|
-
/**
|
|
478
|
+
/**
|
|
479
|
+
* A console command, split the way the engine splits it: `amx_slap "Some One" 5`.
|
|
480
|
+
* `menuselect <key>` goes to his menu first, as AMX Mod X takes it. True if handled.
|
|
481
|
+
*/
|
|
412
482
|
command(line: string): boolean {
|
|
413
|
-
|
|
483
|
+
const argv = splitCommand(line);
|
|
484
|
+
if (argv[0] === 'menuselect' && this.menu && this.server.selectMenu(this, Number(argv[1]))) return true;
|
|
485
|
+
return this.server.clientCommand(this, argv);
|
|
414
486
|
}
|
|
415
487
|
|
|
416
488
|
/** Leaves the server: the disconnect events, then the slot is free. */
|
|
@@ -452,12 +524,34 @@ function splitCommand(line: string): string[] {
|
|
|
452
524
|
return [...line.matchAll(/"([^"]*)"|(\S+)/g)].map(m => m[1] ?? m[2]);
|
|
453
525
|
}
|
|
454
526
|
|
|
527
|
+
/**
|
|
528
|
+
* A log line split as AMX Mod X splits it for read_logargv: a quoted part
|
|
529
|
+
* and a part in brackets each one argument, the text between them another.
|
|
530
|
+
*/
|
|
531
|
+
export function splitLog(line: string): string[] {
|
|
532
|
+
// The spaces between two parts are none.
|
|
533
|
+
return [...line.matchAll(/"([^"]*)"|\(([^)]*)\)|([^"(]+)/g)]
|
|
534
|
+
.filter(m => m[3] === undefined || m[3].trim() !== '')
|
|
535
|
+
.map(m => m[1] ?? m[2] ?? m[3].trim());
|
|
536
|
+
}
|
|
537
|
+
|
|
455
538
|
/** A loaded plugin: its instance and its memory. */
|
|
539
|
+
/** The runs handed out so far: each plugin loaded is a new one. */
|
|
540
|
+
let runs = 0;
|
|
541
|
+
|
|
456
542
|
export class PluginInstance {
|
|
457
543
|
instance: any;
|
|
458
544
|
memory!: Memory;
|
|
459
545
|
/** What plugin() said about it. */
|
|
460
546
|
info = { name: '', version: '', author: '', description: '' };
|
|
547
|
+
/** Taken off the server (FakeServer.unload): its slots answer nothing until the same file takes them back. */
|
|
548
|
+
unloaded = false;
|
|
549
|
+
/** This run of the plugin, as module.cpp's Plugin.run: what the other side of a shared module's call knows it by. */
|
|
550
|
+
readonly run = ++runs;
|
|
551
|
+
/** The shared modules it calls (Plugin.uses): their owners hear when it is unloaded. */
|
|
552
|
+
readonly uses: string[] = [];
|
|
553
|
+
/** It called back a function of a plugin unloaded since, and was told so: once. */
|
|
554
|
+
toldGone = false;
|
|
461
555
|
|
|
462
556
|
/** The coroutine scheduler, for a plugin that awaits; null for one that does not. */
|
|
463
557
|
readonly coroutines: Coroutines | null;
|
|
@@ -492,7 +586,7 @@ export class PluginInstance {
|
|
|
492
586
|
|
|
493
587
|
if (this.server.hasNative(name)) return (...args: number[]) => this.server.callNativeImpl(this, name, args);
|
|
494
588
|
|
|
495
|
-
// Another plugin's `export function`, called through
|
|
589
|
+
// Another plugin's `export function`, called through @amxts/core/natives as AMX
|
|
496
590
|
// Mod X would route it; whether one exports it is known only at the call.
|
|
497
591
|
return (...cells: number[]) => {
|
|
498
592
|
if (this.server.exported.has(name)) return this.server.callFromPlugin(this, name, cells);
|
|
@@ -515,6 +609,8 @@ export class FakeServer {
|
|
|
515
609
|
readonly map: string;
|
|
516
610
|
readonly maxPlayers: number;
|
|
517
611
|
readonly modules: Set<string>;
|
|
612
|
+
/** Whether reapi finds Reunion (has_reunion). */
|
|
613
|
+
readonly reunion: boolean;
|
|
518
614
|
/** Milliseconds since the map started; advance() moves it. */
|
|
519
615
|
time = 0;
|
|
520
616
|
/** Date.now() when the map started. */
|
|
@@ -575,6 +671,8 @@ export class FakeServer {
|
|
|
575
671
|
|
|
576
672
|
/** The game rules' members a plugin set (set_member_game), by member. */
|
|
577
673
|
readonly rules = new Map<number, number>();
|
|
674
|
+
/** The members plugins asked member_slot for, by slot: each one's reapi constant. */
|
|
675
|
+
private readonly memberSlots: number[] = [];
|
|
578
676
|
/** Rounds a plugin ended with rg_round_end, in order. */
|
|
579
677
|
readonly roundEnds: { status: number; event: number; delay: number; message: string; sound: string; trigger: boolean }[] = [];
|
|
580
678
|
/** Sounds played: from an entity (emit_sound, rh_emit_sound2), or to a player alone (SendAudio, rg_send_audio). */
|
|
@@ -610,8 +708,37 @@ export class FakeServer {
|
|
|
610
708
|
hookedMessage: { args: (number | string)[]; types: number[] } | null = null;
|
|
611
709
|
/** register_touch's callbacks. */
|
|
612
710
|
readonly touches: { touched: string; toucher: string; slot: Slot }[] = [];
|
|
711
|
+
/** register_logevent's callbacks: the number of arguments a line has, and the filters it passes. @internal */
|
|
712
|
+
readonly logEvents: { argc: number; filters: string[]; slot: Slot }[] = [];
|
|
713
|
+
/** The log line logevent callbacks are reading, as AMX Mod X splits it. @internal */
|
|
714
|
+
logArgs: string[] = [];
|
|
715
|
+
/** That line whole, as read_logdata reads it. @internal */
|
|
716
|
+
logLine = '';
|
|
717
|
+
/** register_event's callbacks, by the message's name, with their conditions ("1=0"). @internal */
|
|
718
|
+
readonly messageEvents: { name: string; conditions: string[]; slot: Slot }[] = [];
|
|
719
|
+
/** register_forward's callbacks, by `<FM_* number>:pre|post`. */
|
|
720
|
+
readonly fakemetaForwards = new Map<string, Slot[]>();
|
|
721
|
+
/** What a fakemeta callback answered with forward_return, and what get_orig_retval reads. @internal */
|
|
722
|
+
forwardAnswer: number | string | null = null;
|
|
723
|
+
origRetval = 0;
|
|
724
|
+
/** The engine's EngFunc_SetClientListening calls plugins made: listener, sender, whether he hears. */
|
|
725
|
+
readonly listening: number[][] = [];
|
|
726
|
+
/** The engine's and the game's functions plugins called through engfunc and dllfunc, each as a line: `TraceLine 1,2,3 4,5,6 1 7 0`. */
|
|
727
|
+
readonly engineCalls: string[] = [];
|
|
613
728
|
/** query_client_cvar's questions, waiting for answerCvar(). */
|
|
614
729
|
readonly cvarQueries: { player: number; cvar: string; slot: Slot }[] = [];
|
|
730
|
+
/** The menus menu_create made and menu_destroy has not taken away, by id. */
|
|
731
|
+
readonly menus = new Map<number, FakeMenu>();
|
|
732
|
+
/** menu_makecallback's publics, by the number it gave. @internal */
|
|
733
|
+
readonly menuCallbacks: Slot[] = [];
|
|
734
|
+
private menuIds = 0;
|
|
735
|
+
private userids = 1;
|
|
736
|
+
|
|
737
|
+
/** A userid for a player who comes: one more than the last. @internal */
|
|
738
|
+
nextUserid(): number {
|
|
739
|
+
return this.userids++;
|
|
740
|
+
}
|
|
741
|
+
|
|
615
742
|
/** What precache_model and precache_sound were asked for, in order: the index is the place + 1. */
|
|
616
743
|
readonly precached: string[] = [];
|
|
617
744
|
/**
|
|
@@ -623,7 +750,7 @@ export class FakeServer {
|
|
|
623
750
|
/** Which of those texts are lists of player ids ("3,5"), by `slot:key`: a player who leaves goes from them. */
|
|
624
751
|
readonly playerLists = new Set<string>();
|
|
625
752
|
/**
|
|
626
|
-
* The
|
|
753
|
+
* The playerChange listeners, as the module keeps them: per plugin, the
|
|
627
754
|
* fields it hears ("" for every one) and the trampoline that hands a change
|
|
628
755
|
* to its listeners.
|
|
629
756
|
*/
|
|
@@ -640,13 +767,15 @@ export class FakeServer {
|
|
|
640
767
|
private outcome = 0;
|
|
641
768
|
private outcomeSaid = false;
|
|
642
769
|
private taskOrder = 0;
|
|
643
|
-
|
|
770
|
+
/** The slots of RegisterHookChain's and RegisterHam's handles, which DisableHookChain and the rest take. */
|
|
771
|
+
private readonly hookSlots = new Map<number, Slot>();
|
|
644
772
|
private entityIds: number;
|
|
645
773
|
|
|
646
774
|
constructor(options: ServerOptions = {}) {
|
|
647
775
|
this.map = options.map ?? 'de_dust2';
|
|
648
776
|
this.maxPlayers = options.maxPlayers ?? 32;
|
|
649
777
|
this.modules = new Set(options.modules ?? ['reapi', 'cstrike', 'fun', 'hamsandwich', 'engine', 'fakemeta', 'nvault', 'resemiclip']);
|
|
778
|
+
this.reunion = options.reunion ?? false;
|
|
650
779
|
this.entityIds = this.maxPlayers + 1;
|
|
651
780
|
this.timeZone = options.timeZone;
|
|
652
781
|
// AMX Mod X's own, which a test may set: the languages.
|
|
@@ -870,11 +999,29 @@ export class FakeServer {
|
|
|
870
999
|
this.entities.set(id, player);
|
|
871
1000
|
|
|
872
1001
|
this.fire('client_connect', id);
|
|
873
|
-
this.fire('client_authorized', id, player.
|
|
1002
|
+
this.fire('client_authorized', id, player.steamId);
|
|
874
1003
|
this.fire('client_putinserver', id);
|
|
875
1004
|
return player;
|
|
876
1005
|
}
|
|
877
1006
|
|
|
1007
|
+
/** The module's network client: the requests plugins sent with fetch. @internal */
|
|
1008
|
+
readonly network = new FakeNetwork(this);
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Waits for the responses to the requests the plugins sent with `fetch`,
|
|
1012
|
+
* and hands each to its plugin as the server's next frame would; a
|
|
1013
|
+
* request sent on the way is waited for too. A timer between two tries
|
|
1014
|
+
* waits for `advance()`.
|
|
1015
|
+
*
|
|
1016
|
+
* ```ts
|
|
1017
|
+
* player.say("/weather");
|
|
1018
|
+
* await server.responses();
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
async responses(): Promise<void> {
|
|
1022
|
+
await this.network.settle();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
878
1025
|
/**
|
|
879
1026
|
* Moves the clock forward and runs every timer that comes due on the way,
|
|
880
1027
|
* in order - a repeating one as many times as it fits.
|
|
@@ -913,6 +1060,7 @@ export class FakeServer {
|
|
|
913
1060
|
|
|
914
1061
|
let result = 0;
|
|
915
1062
|
for (const handler of [...(this.events.get(name) ?? [])]) {
|
|
1063
|
+
if (handler.where && Number(args[handler.where.arg]) !== handler.where.value) continue;
|
|
916
1064
|
const one = this.call(handler, [first], 0);
|
|
917
1065
|
if (one > result) result = one;
|
|
918
1066
|
}
|
|
@@ -932,24 +1080,26 @@ export class FakeServer {
|
|
|
932
1080
|
*
|
|
933
1081
|
* ```ts
|
|
934
1082
|
* server.fireHook("takeDamage", [victim.id, 0, attacker.id, 30.0, 2]);
|
|
935
|
-
* server.fireHook("
|
|
1083
|
+
* server.fireHook("fallDamage", [player.id], { result: 40 });
|
|
936
1084
|
* ```
|
|
937
1085
|
*
|
|
938
1086
|
* `event` is the name game.addEventListener takes, or reapi's short one
|
|
939
1087
|
* ("take_damage"). `args` are the chain's arguments in order; a float is
|
|
940
|
-
* written as a number and goes as one
|
|
1088
|
+
* written as a number and goes as one, a vector as `[x, y, z]`. `result` is what the game's own
|
|
941
1089
|
* function answers when it runs - what a post listener reads as
|
|
942
1090
|
* event.result.
|
|
943
1091
|
*/
|
|
944
|
-
fireHook(event: string, args:
|
|
1092
|
+
fireHook(event: string, args: HookArg[] = [], options: { result?: Value } = {}): HookResult {
|
|
945
1093
|
const shape = tables().hooks.get(event);
|
|
946
1094
|
if (!shape) throw new Error(`no hookchain named "${event}" in as/hooks.ts`);
|
|
947
1095
|
|
|
948
|
-
const cells = args.map((a, i) =>
|
|
949
|
-
? a
|
|
950
|
-
: typeof a === '
|
|
951
|
-
?
|
|
952
|
-
:
|
|
1096
|
+
const cells: ArgValue[] = args.map((a, i) => Array.isArray(a)
|
|
1097
|
+
? a.map(floatBits)
|
|
1098
|
+
: typeof a === 'string'
|
|
1099
|
+
? a
|
|
1100
|
+
: typeof a === 'boolean'
|
|
1101
|
+
? (a ? 1 : 0)
|
|
1102
|
+
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
953
1103
|
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
954
1104
|
|
|
955
1105
|
const nothing: Value = shape.answer === constant('ATYPE_BOOL') ? false : shape.answer === constant('ATYPE_STRING') ? '' : 0;
|
|
@@ -980,6 +1130,12 @@ export class FakeServer {
|
|
|
980
1130
|
if (!prevented) chain.answer = options.result ?? nothing;
|
|
981
1131
|
|
|
982
1132
|
if (!broken) {
|
|
1133
|
+
// A vector is an array reapi copies back into the game's: what
|
|
1134
|
+
// a pre listener wrote in it is what the game and the post
|
|
1135
|
+
// listeners get.
|
|
1136
|
+
cells.forEach((cell, i) => {
|
|
1137
|
+
if (Array.isArray(cell)) chain.args[i] = cell.map(bitsFloat);
|
|
1138
|
+
});
|
|
983
1139
|
for (const slot of [...listeners.post]) {
|
|
984
1140
|
this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
985
1141
|
}
|
|
@@ -1001,8 +1157,11 @@ export class FakeServer {
|
|
|
1001
1157
|
* const sent = server.sendMessage("RoundTime", [120]); // sent.args: [90] with a listener that writes 90
|
|
1002
1158
|
* ```
|
|
1003
1159
|
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1160
|
+
* `name` is the game's name of the message, `"TextMsg"` for the
|
|
1161
|
+
* `"text"` that server.addMessageListener takes, and `args` are its
|
|
1162
|
+
* arguments in the game's order. A text argument is a string; a whole
|
|
1163
|
+
* number is written as a byte, a fraction as a coordinate, unless `types`
|
|
1164
|
+
* gives each argument's ARG_*.
|
|
1006
1165
|
* Returns whether a callback stopped it, and the arguments as they left.
|
|
1007
1166
|
*/
|
|
1008
1167
|
sendMessage(name: string, args: (number | string)[], options: { player?: FakePlayer; types?: number[] } = {}): { prevented: boolean; args: (number | string)[] } {
|
|
@@ -1020,6 +1179,14 @@ export class FakeServer {
|
|
|
1020
1179
|
const handed = [id ?? 0, receiver ? 1 : 0, receiver, 0];
|
|
1021
1180
|
if (this.withCallArgs(handed, () => this.call(slot, handed, 0)) === PLUGIN_HANDLED) prevented = true;
|
|
1022
1181
|
}
|
|
1182
|
+
// register_event's callbacks hear it once it is sent, when its conditions hold.
|
|
1183
|
+
const holds = (condition: string) => {
|
|
1184
|
+
const match = condition.match(/^(\d+)=(.*)$/);
|
|
1185
|
+
return !match || String(message.args[Number(match[1]) - 1]) === match[2];
|
|
1186
|
+
};
|
|
1187
|
+
for (const { slot } of this.messageEvents.filter(one => one.name === name && one.conditions.every(holds))) {
|
|
1188
|
+
if (!prevented) this.withCallArgs([receiver], () => this.call(slot, [receiver, 0, 0, 0], 0));
|
|
1189
|
+
}
|
|
1023
1190
|
return { prevented, args: message.args };
|
|
1024
1191
|
} finally {
|
|
1025
1192
|
this.hookedMessage = previous;
|
|
@@ -1041,7 +1208,7 @@ export class FakeServer {
|
|
|
1041
1208
|
* function's arguments after the entity, a float as a number and a vector
|
|
1042
1209
|
* as an array of three.
|
|
1043
1210
|
*/
|
|
1044
|
-
fireHam(event: string, entity: FakeEntity | number, args:
|
|
1211
|
+
fireHam(event: string, entity: FakeEntity | number, args: HookArg[] = [], options: { result?: Value } = {}): HookResult {
|
|
1045
1212
|
const shape = tables().hooks.get(event);
|
|
1046
1213
|
if (shape?.ham === undefined) throw new Error(`no Ham Sandwich function under the event "${event}" in as/hooks.ts`);
|
|
1047
1214
|
return this.runHam(shape, typeof entity === 'number' ? entity : entity.id, args, options.result);
|
|
@@ -1051,7 +1218,7 @@ export class FakeServer {
|
|
|
1051
1218
|
* fireHam's work, and ExecuteHamB's: `args` without the entity.
|
|
1052
1219
|
* @internal
|
|
1053
1220
|
*/
|
|
1054
|
-
runHam(shape: HookShape, id: number, args:
|
|
1221
|
+
runHam(shape: HookShape, id: number, args: HookArg[], result?: Value): HookResult {
|
|
1055
1222
|
const classname = this.entities.get(id)?.classname ?? (id >= 1 && id <= this.maxPlayers ? 'player' : '');
|
|
1056
1223
|
const all = [id, ...args];
|
|
1057
1224
|
const cells: ArgValue[] = all.map((a, i) => Array.isArray(a)
|
|
@@ -1063,7 +1230,7 @@ export class FakeServer {
|
|
|
1063
1230
|
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
1064
1231
|
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
1065
1232
|
const nothing: Value = shape.answer === constant('ATYPE_STRING') ? '' : 0;
|
|
1066
|
-
const chain: Chain = { shape, answer: nothing, args: all
|
|
1233
|
+
const chain: Chain = { shape, answer: nothing, args: all };
|
|
1067
1234
|
const listeners = (post: boolean) => this.hams.get(`${shape.ham}:${classname}:${post ? 'post' : 'pre'}`) ?? [];
|
|
1068
1235
|
|
|
1069
1236
|
const previous = this.chain;
|
|
@@ -1281,7 +1448,7 @@ export class FakeServer {
|
|
|
1281
1448
|
|
|
1282
1449
|
/**
|
|
1283
1450
|
* A plugin calling another plugin's `export function` native
|
|
1284
|
-
* through its
|
|
1451
|
+
* through its @amxts/core/natives wrapper: the cells it pushed - text and buffers as
|
|
1285
1452
|
* addresses in its memory - turned into the frame the native reads, and
|
|
1286
1453
|
* what the native wrote into a buffer copied back, as AMX Mod X does.
|
|
1287
1454
|
* @internal
|
|
@@ -1344,6 +1511,9 @@ export class FakeServer {
|
|
|
1344
1511
|
|
|
1345
1512
|
/** Fire in module.cpp: one handler, its outcome, and the state put back. @internal */
|
|
1346
1513
|
call(handler: Handler, args: number[], fallback: number): number {
|
|
1514
|
+
// An orphan answers PLUGIN_CONTINUE, as in n_callback: a command passes on to the plugin loaded since.
|
|
1515
|
+
if (handler.plugin.unloaded) return 0;
|
|
1516
|
+
if ((handler as Slot).off) return fallback;
|
|
1347
1517
|
const fn = handler.plugin.table.get(handler.fn);
|
|
1348
1518
|
const cells = handler.shape === SHAPE_WIDE ? [0, 1, 2, 3].map(i => args[i] ?? 0) : [args[0] ?? 0];
|
|
1349
1519
|
if (handler.tag) cells.unshift(handler.tag);
|
|
@@ -1431,6 +1601,208 @@ export class FakeServer {
|
|
|
1431
1601
|
}
|
|
1432
1602
|
}
|
|
1433
1603
|
|
|
1604
|
+
/**
|
|
1605
|
+
* Takes a plugin off the server, as `amxts_reload` does before it loads
|
|
1606
|
+
* the plugins again: its timers are removed, and a call to one of its
|
|
1607
|
+
* other slots answers nothing, until a plugin of the same file asks for
|
|
1608
|
+
* the same key and takes the slot back. `load` the file again for the
|
|
1609
|
+
* reload's second half.
|
|
1610
|
+
*/
|
|
1611
|
+
unload(plugin: PluginInstance): void {
|
|
1612
|
+
plugin.unloaded = true;
|
|
1613
|
+
this.plugins.splice(this.plugins.indexOf(plugin), 1);
|
|
1614
|
+
for (let i = this.tasks.length - 1; i >= 0; i--) {
|
|
1615
|
+
if (this.tasks[i].slot.plugin === plugin) this.tasks.splice(i, 1);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// The modules it served answer nothing; the owners of those it called drop what it gave them (TellOwners).
|
|
1619
|
+
for (const service of this.services.filter(each => each.plugin === plugin)) service.plugin = null;
|
|
1620
|
+
const owners = this.services.filter(each => plugin.uses.includes(each.name) && each.plugin && each.plugin !== plugin);
|
|
1621
|
+
const gone = new Uint8Array(new Int32Array([RPC_GONE, 0]).buffer);
|
|
1622
|
+
for (const owner of owners) this.request(owner.plugin!, plugin.run, gone);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** A menu by its id, or the error AMX Mod X gives for another number. @internal */
|
|
1626
|
+
menu(id: number): FakeMenu {
|
|
1627
|
+
const menu = this.menus.get(id);
|
|
1628
|
+
if (!menu) throw new Error(`Invalid menu id ${id}`);
|
|
1629
|
+
return menu;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** menu_create: a menu of seven items a page, with Back, More and Exit. @internal */
|
|
1633
|
+
createMenu(title: string, handler: Slot): number {
|
|
1634
|
+
const id = this.menuIds++;
|
|
1635
|
+
this.menus.set(id, { id, title, handler, items: [], perPage: 7, exit: true, back: 'Back', next: 'More', exitName: 'Exit', numberColor: '\\r' });
|
|
1636
|
+
return id;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/** menu_destroy: whoever has it open sees it close, and its handler hears MENU_EXIT. @internal */
|
|
1640
|
+
destroyMenu(id: number): void {
|
|
1641
|
+
const menu = this.menu(id);
|
|
1642
|
+
this.menus.delete(id);
|
|
1643
|
+
for (const player of this.players.filter(one => one.menu?.menu === id)) {
|
|
1644
|
+
player.menu = null;
|
|
1645
|
+
this.menuAnswer(menu, player, constant('MENU_EXIT'));
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* menu_display: a page drawn for a player. An item with a callback is
|
|
1651
|
+
* asked as AMX Mod X asks it, as it is drawn - the callback may name it
|
|
1652
|
+
* for him, and its answer greys it out. Another menu he had open is
|
|
1653
|
+
* closed first: its handler hears MENU_EXIT. @internal
|
|
1654
|
+
*/
|
|
1655
|
+
displayMenu(player: FakePlayer, menu: FakeMenu, page: number): void {
|
|
1656
|
+
const before = player.menu;
|
|
1657
|
+
player.menu = null;
|
|
1658
|
+
const replaced = before && before.menu !== menu.id ? this.menus.get(before.menu) : undefined;
|
|
1659
|
+
if (replaced) this.menuAnswer(replaced, player, constant('MENU_EXIT'));
|
|
1660
|
+
|
|
1661
|
+
const perPage = menu.perPage === 0 ? Math.min(menu.items.length, 10) : menu.perPage;
|
|
1662
|
+
const pages = Math.max(1, Math.ceil(menu.items.length / Math.max(perPage, 1)));
|
|
1663
|
+
const at = Math.min(Math.max(page, 0), pages - 1);
|
|
1664
|
+
const lines = [`${menu.title}${pages > 1 ? ` ${at + 1}/${pages}` : ''}`, ''];
|
|
1665
|
+
const keys: number[] = [];
|
|
1666
|
+
const disabled: number[] = [];
|
|
1667
|
+
const line = (key: number, text: string) => lines.push(`${menu.numberColor}${key}.\\w ${text}`);
|
|
1668
|
+
|
|
1669
|
+
for (let i = 0; i < perPage && at * perPage + i < menu.items.length; i++) {
|
|
1670
|
+
const item = at * perPage + i;
|
|
1671
|
+
const callback = this.menuCallbacks[menu.items[item].callback];
|
|
1672
|
+
const args = [player.id, menu.id, item];
|
|
1673
|
+
const answer = callback ? this.withCallArgs(args, () => this.call(callback, args, constant('ITEM_IGNORE'))) : constant('ITEM_IGNORE');
|
|
1674
|
+
const key = (i + 1) % 10;
|
|
1675
|
+
keys.push(key);
|
|
1676
|
+
if (answer !== constant('ITEM_DISABLED')) {
|
|
1677
|
+
line(key, menu.items[item].name);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
disabled.push(key);
|
|
1681
|
+
lines.push(`\\d${key}. ${menu.items[item].name}`);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const paged = menu.perPage > 0;
|
|
1685
|
+
if (paged) lines.push('');
|
|
1686
|
+
if (paged && at > 0) {
|
|
1687
|
+
keys.push(8);
|
|
1688
|
+
line(8, menu.back);
|
|
1689
|
+
}
|
|
1690
|
+
if (paged && at < pages - 1) {
|
|
1691
|
+
keys.push(9);
|
|
1692
|
+
line(9, menu.next);
|
|
1693
|
+
}
|
|
1694
|
+
if (paged && menu.exit) {
|
|
1695
|
+
keys.push(0);
|
|
1696
|
+
line(0, menu.exitName);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
player.menu = { menu: menu.id, page: at, text: lines.join('\n'), keys, disabled };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/** A key on a menu of AMX Mod X's own: an item to the handler, Back and More a page, Exit MENU_EXIT. Whether the menu took it. @internal */
|
|
1703
|
+
selectMenu(player: FakePlayer, key: number): boolean {
|
|
1704
|
+
const screen = player.menu;
|
|
1705
|
+
const menu = screen ? this.menus.get(screen.menu) : undefined;
|
|
1706
|
+
if (!screen || !menu || !screen.keys.includes(key)) return false;
|
|
1707
|
+
|
|
1708
|
+
const paged = menu.perPage > 0;
|
|
1709
|
+
const turn = paged && key === 8 ? -1 : paged && key === 9 ? 1 : 0;
|
|
1710
|
+
if (turn !== 0 || screen.disabled.includes(key)) {
|
|
1711
|
+
this.displayMenu(player, menu, screen.page + turn);
|
|
1712
|
+
return true;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
player.menu = null;
|
|
1716
|
+
const perPage = paged ? menu.perPage : 10;
|
|
1717
|
+
this.menuAnswer(menu, player, paged && key === 0 ? constant('MENU_EXIT') : screen.page * perPage + (key === 0 ? 10 : key) - 1);
|
|
1718
|
+
return true;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private menuAnswer(menu: FakeMenu, player: FakePlayer, item: number): void {
|
|
1722
|
+
const args = [player.id, menu.id, item];
|
|
1723
|
+
this.withCallArgs(args, () => this.call(menu.handler, args, 0));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* A line of the game's log, as the engine hands it to AMX Mod X: split
|
|
1728
|
+
* into its arguments as AMX Mod X splits it, and every logevent callback
|
|
1729
|
+
* whose filters it passes is called.
|
|
1730
|
+
*
|
|
1731
|
+
* ```ts
|
|
1732
|
+
* server.gameLog('World triggered "Round_End"');
|
|
1733
|
+
* server.gameLog(`"${alice.name}<${alice.userid}><STEAM_0:0:1><TERRORIST>" triggered "Planted_The_Bomb"`);
|
|
1734
|
+
* ```
|
|
1735
|
+
*/
|
|
1736
|
+
gameLog(line: string): void {
|
|
1737
|
+
// AMX Mod X keeps one parsed line: one logged while a callback runs
|
|
1738
|
+
// replaces it, and the walk of the outer line's callbacks goes on with it.
|
|
1739
|
+
this.logLine = line;
|
|
1740
|
+
this.logArgs = splitLog(line);
|
|
1741
|
+
const argc = this.logArgs.length;
|
|
1742
|
+
const passes = (filter: string) => {
|
|
1743
|
+
const match = filter.match(/^(\d+)([=&!])(.*)$/);
|
|
1744
|
+
if (!match) return true;
|
|
1745
|
+
const value = this.logArgs[Number(match[1])] ?? '';
|
|
1746
|
+
return match[2] === '=' ? value === match[3] : match[2] === '&' ? value.includes(match[3]) : value !== match[3];
|
|
1747
|
+
};
|
|
1748
|
+
for (const event of [...this.logEvents]) {
|
|
1749
|
+
if (event.argc === argc && event.filters.every(passes)) this.withCallArgs([], () => this.call(event.slot, [0, 0, 0, 0], 0));
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* A fakemeta function the game calls, through the register_forward
|
|
1755
|
+
* callbacks of it: the pre ones, then, unless one of them superseded it,
|
|
1756
|
+
* the post ones with `result` as get_orig_retval reads it. Returns whether
|
|
1757
|
+
* it was superseded and what a callback answered with forward_return.
|
|
1758
|
+
*
|
|
1759
|
+
* ```ts
|
|
1760
|
+
* server.fireForward('FM_EmitSound', [alice.id, 2, 'player/die1.wav', 1.0, 0.8, 0, 100]);
|
|
1761
|
+
* ```
|
|
1762
|
+
*/
|
|
1763
|
+
fireForward(name: string, args: ArgValue[], result = 0): { superseded: boolean; answer: number | string | null } {
|
|
1764
|
+
const fn = constant(name);
|
|
1765
|
+
const handed = args.slice(0, 4).map(a => typeof a === 'number' ? a : 0);
|
|
1766
|
+
const cells = args.map(a => typeof a === 'number' && !Number.isInteger(a) ? floatBits(a) : a);
|
|
1767
|
+
this.forwardAnswer = null;
|
|
1768
|
+
let superseded = false;
|
|
1769
|
+
for (const slot of [...(this.fakemetaForwards.get(`${fn}:pre`) ?? [])]) {
|
|
1770
|
+
if (this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback)) >= FMRES_SUPERCEDE) superseded = true;
|
|
1771
|
+
}
|
|
1772
|
+
if (!superseded) {
|
|
1773
|
+
this.origRetval = result;
|
|
1774
|
+
for (const slot of [...(this.fakemetaForwards.get(`${fn}:post`) ?? [])]) this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
1775
|
+
}
|
|
1776
|
+
return { superseded, answer: this.forwardAnswer };
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/** Switches a hook off or on by the handle RegisterHookChain or RegisterHam gave. @internal */
|
|
1780
|
+
switchHook(handle: number, on: boolean): number {
|
|
1781
|
+
const slot = this.hookSlots.get(handle);
|
|
1782
|
+
if (!slot) throw new Error(`the fake server knows no hook handle ${handle}`);
|
|
1783
|
+
slot.off = !on;
|
|
1784
|
+
return 1;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Whether a game event's hook calls the plugin - reapi's chain, or Ham
|
|
1789
|
+
* Sandwich's function on a class: false while the plugin has switched it
|
|
1790
|
+
* off, for having no listener; undefined when it was never registered.
|
|
1791
|
+
*
|
|
1792
|
+
* ```ts
|
|
1793
|
+
* server.hooked('resetMaxSpeed'); // the chain, before the game
|
|
1794
|
+
* server.hooked('spawn', { classname: 'player', post: true });
|
|
1795
|
+
* ```
|
|
1796
|
+
*/
|
|
1797
|
+
hooked(event: string, options: { post?: boolean; classname?: string } = {}): boolean | undefined {
|
|
1798
|
+
const shape = tables().hooks.get(event);
|
|
1799
|
+
if (!shape) throw new Error(`no hookchain named "${event}" in as/hooks.ts`);
|
|
1800
|
+
const slots = options.classname !== undefined
|
|
1801
|
+
? this.hams.get(`${shape.ham}:${options.classname}:${options.post ? 'post' : 'pre'}`)
|
|
1802
|
+
: this.hookchains.get(shape.kind)?.[options.post ? 'post' : 'pre'];
|
|
1803
|
+
return slots?.length ? slots.some(slot => !slot.off) : undefined;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1434
1806
|
/** A slot by the public name a native was given: "__amxts_cb3". @internal */
|
|
1435
1807
|
slotByPublic(name: string): Slot {
|
|
1436
1808
|
const match = name.match(/^__amxts_cb(\d+)$/);
|
|
@@ -1507,11 +1879,21 @@ export class FakeServer {
|
|
|
1507
1879
|
}
|
|
1508
1880
|
|
|
1509
1881
|
/** Modules a plugin runs for the others (module.cpp's g_services), and the bytes of the call in flight. @internal */
|
|
1510
|
-
private services: { name: string; hash: number; plugin:
|
|
1882
|
+
private services: { name: string; hash: number; plugin: PluginInstance | null }[] = [];
|
|
1511
1883
|
private rpcRequest = new Uint8Array(0);
|
|
1512
1884
|
private rpcReply = new Uint8Array(0);
|
|
1513
1885
|
private rpcResult = new Uint8Array(0);
|
|
1514
1886
|
|
|
1887
|
+
/** Runs a request in `callee`'s __amxts_rpc, from the run `from` (module.cpp's RunRequest): its answer, or null. */
|
|
1888
|
+
private request(callee: PluginInstance, from: number, request: Uint8Array<ArrayBuffer>): Uint8Array | null {
|
|
1889
|
+
const rpc = callee.instance.exports.__amxts_rpc;
|
|
1890
|
+
if (!rpc) return null;
|
|
1891
|
+
this.rpcRequest = request;
|
|
1892
|
+
this.rpcReply = new Uint8Array(0);
|
|
1893
|
+
this.within(callee, () => rpc(this.rpcRequest.length, from));
|
|
1894
|
+
return this.rpcReply;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1515
1897
|
/**
|
|
1516
1898
|
* The externals the facade declares, as module.cpp registers
|
|
1517
1899
|
* them in g_wasmNatives. Each is called with the plugin that called it.
|
|
@@ -1521,30 +1903,32 @@ export class FakeServer {
|
|
|
1521
1903
|
// Shared modules: module.cpp's w_serve, w_owner, w_rpc and the three that move the bytes.
|
|
1522
1904
|
amxts_serve(this: FakeServer, plugin: PluginInstance, name: number, hash: number) {
|
|
1523
1905
|
const wanted = plugin.memory.string(name);
|
|
1524
|
-
const index = this.plugins.indexOf(plugin);
|
|
1525
1906
|
const known = this.services.find(s => s.name === wanted);
|
|
1526
|
-
if (known) Object.assign(known, { hash, plugin
|
|
1527
|
-
else this.services.push({ name: wanted, hash, plugin
|
|
1907
|
+
if (known) Object.assign(known, { hash, plugin });
|
|
1908
|
+
else this.services.push({ name: wanted, hash, plugin });
|
|
1528
1909
|
},
|
|
1529
1910
|
|
|
1530
1911
|
amxts_owner(this: FakeServer, plugin: PluginInstance, name: number, hash: number) {
|
|
1531
1912
|
const wanted = plugin.memory.string(name);
|
|
1913
|
+
if (!plugin.uses.includes(wanted)) plugin.uses.push(wanted);
|
|
1532
1914
|
const index = this.services.findIndex(s => s.name === wanted);
|
|
1533
|
-
if (index < 0 || this.services[index].plugin
|
|
1915
|
+
if (index < 0 || !this.services[index].plugin) return -1;
|
|
1534
1916
|
return this.services[index].hash === hash ? index : -2;
|
|
1535
1917
|
},
|
|
1536
1918
|
|
|
1537
1919
|
amxts_rpc(this: FakeServer, plugin: PluginInstance, service: number, target: number, data: number, length: number) {
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
const
|
|
1546
|
-
|
|
1547
|
-
answer
|
|
1920
|
+
const callee = service >= 0 ? this.services[service]?.plugin : this.plugins.find(one => one.run === target);
|
|
1921
|
+
|
|
1922
|
+
if (service < 0 && !callee && !plugin.toldGone) {
|
|
1923
|
+
plugin.toldGone = true;
|
|
1924
|
+
this.logLines.push(`[amxts] ${plugin.source} called back a function of a plugin that was unloaded or reloaded since - the call answers nothing`);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
const reply = callee ? this.request(callee, plugin.run, new Uint8Array(plugin.instance.exports.memory.buffer, data, length).slice()) : null;
|
|
1928
|
+
if (!callee || !reply) return -1;
|
|
1929
|
+
const answer = new Uint8Array(4 + reply.length);
|
|
1930
|
+
new DataView(answer.buffer).setInt32(0, callee.run, true);
|
|
1931
|
+
answer.set(reply, 4);
|
|
1548
1932
|
this.rpcResult = answer;
|
|
1549
1933
|
return answer.length;
|
|
1550
1934
|
},
|
|
@@ -1563,11 +1947,58 @@ export class FakeServer {
|
|
|
1563
1947
|
this.rpcResult = new Uint8Array(0);
|
|
1564
1948
|
},
|
|
1565
1949
|
|
|
1950
|
+
// The network client: module.cpp's w_net_*, over Bun's fetch and FTP and SFTP clients (network.ts).
|
|
1951
|
+
net_open(this: FakeServer, plugin: PluginInstance, url: number) {
|
|
1952
|
+
return this.network.open(plugin, plugin.memory.string(url));
|
|
1953
|
+
},
|
|
1954
|
+
net_option(this: FakeServer, plugin: PluginInstance, id: number, name: number, value: number) {
|
|
1955
|
+
return this.network.option(plugin, id, plugin.memory.string(name), plugin.memory.string(value));
|
|
1956
|
+
},
|
|
1957
|
+
net_body(this: FakeServer, plugin: PluginInstance, id: number, data: number, length: number) {
|
|
1958
|
+
this.network.body(plugin, id, new Uint8Array(plugin.instance.exports.memory.buffer, data, length).slice());
|
|
1959
|
+
},
|
|
1960
|
+
net_send(this: FakeServer, plugin: PluginInstance, id: number, fn: number) {
|
|
1961
|
+
return this.network.send(plugin, id, fn);
|
|
1962
|
+
},
|
|
1963
|
+
net_cancel(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1964
|
+
this.network.cancel(plugin, id);
|
|
1965
|
+
},
|
|
1966
|
+
net_close(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1967
|
+
this.network.cancel(plugin, id);
|
|
1968
|
+
},
|
|
1969
|
+
net_status(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1970
|
+
return this.network.status(plugin, id);
|
|
1971
|
+
},
|
|
1972
|
+
net_redirects(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1973
|
+
return this.network.redirects(plugin, id);
|
|
1974
|
+
},
|
|
1975
|
+
net_text(this: FakeServer, plugin: PluginInstance, id: number, what: number, out: number, max: number) {
|
|
1976
|
+
return this.network.text(plugin, id, what, out, max);
|
|
1977
|
+
},
|
|
1978
|
+
net_size(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1979
|
+
return this.network.size(plugin, id);
|
|
1980
|
+
},
|
|
1981
|
+
net_read(this: FakeServer, plugin: PluginInstance, id: number, out: number, max: number) {
|
|
1982
|
+
return this.network.read(plugin, id, out, max);
|
|
1983
|
+
},
|
|
1984
|
+
net_reply(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1985
|
+
return this.network.reply(plugin, id);
|
|
1986
|
+
},
|
|
1987
|
+
|
|
1566
1988
|
abort(this: FakeServer, plugin: PluginInstance, message: number, file: number, line: number, column: number) {
|
|
1567
1989
|
const text = message ? plugin.memory.string(message) : 'assertion failed';
|
|
1568
1990
|
throw new Error(`abort: ${text} (${file ? plugin.memory.string(file) : '?'}:${line}:${column})`);
|
|
1569
1991
|
},
|
|
1570
1992
|
|
|
1993
|
+
// An error's stack: the server reads frames WAMR keeps, which a
|
|
1994
|
+
// plugin run here has none of - `stack` is the error's first line.
|
|
1995
|
+
stack_frames() {
|
|
1996
|
+
return 0;
|
|
1997
|
+
},
|
|
1998
|
+
stack_text() {
|
|
1999
|
+
return 0;
|
|
2000
|
+
},
|
|
2001
|
+
|
|
1571
2002
|
seed() {
|
|
1572
2003
|
return Date.now();
|
|
1573
2004
|
},
|
|
@@ -1636,6 +2067,22 @@ export class FakeServer {
|
|
|
1636
2067
|
list.push({ plugin, fn, shape });
|
|
1637
2068
|
},
|
|
1638
2069
|
|
|
2070
|
+
// The plugin's handler of a forward taken off, once its event has no listener left.
|
|
2071
|
+
off(this: FakeServer, plugin: PluginInstance, name: number, fn: number) {
|
|
2072
|
+
const list = this.events.get(plugin.memory.string(name)) ?? [];
|
|
2073
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
2074
|
+
if (list[i].plugin === plugin && list[i].fn === fn) list.splice(i, 1);
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
|
|
2078
|
+
// on, for the calls whose argument `arg` is `value` alone - compared before the plugin, as module.cpp does.
|
|
2079
|
+
on_cell(this: FakeServer, plugin: PluginInstance, name: number, fn: number, shape: number, arg: number, value: number) {
|
|
2080
|
+
const event = plugin.memory.string(name);
|
|
2081
|
+
let list = this.events.get(event);
|
|
2082
|
+
if (!list) this.events.set(event, list = []);
|
|
2083
|
+
list.push({ plugin, fn, shape, where: { arg, value } });
|
|
2084
|
+
},
|
|
2085
|
+
|
|
1639
2086
|
subscribe(this: FakeServer, plugin: PluginInstance, name: number, fn: number, tag: number) {
|
|
1640
2087
|
const forward = plugin.memory.string(name);
|
|
1641
2088
|
let list = this.subscriptions.get(forward);
|
|
@@ -1661,8 +2108,19 @@ export class FakeServer {
|
|
|
1661
2108
|
this.withCallArgs(args, () => this.deliver(forward));
|
|
1662
2109
|
},
|
|
1663
2110
|
|
|
2111
|
+
// A slot a reload left takes the same file's plugin back, as SLOT_REUSED says in module.cpp.
|
|
1664
2112
|
slot(this: FakeServer, plugin: PluginInstance, fn: number, shape: number, key: number, fallback: number) {
|
|
1665
|
-
|
|
2113
|
+
const name = plugin.memory.string(key);
|
|
2114
|
+
const left = name ? this.slots.find(one => one.key === name && one.plugin.unloaded && one.plugin.source === plugin.source) : undefined;
|
|
2115
|
+
if (!left) return this.takeSlot(plugin, fn, shape, name, fallback).index;
|
|
2116
|
+
Object.assign(left, { plugin, fn, shape, fallback, tag: this.takeTag() });
|
|
2117
|
+
return left.index | SLOT_REUSED;
|
|
2118
|
+
},
|
|
2119
|
+
|
|
2120
|
+
// One of the plugin's slots switched off while what it delivers has no listener, or back on.
|
|
2121
|
+
slot_on(this: FakeServer, plugin: PluginInstance, index: number, on: number) {
|
|
2122
|
+
const slot = this.slots[index];
|
|
2123
|
+
if (slot?.plugin === plugin) slot.off = on === 0;
|
|
1666
2124
|
},
|
|
1667
2125
|
|
|
1668
2126
|
clcmd(this: FakeServer, plugin: PluginInstance, pattern: number, fn: number, flags: number, info: number, shape: number) {
|
|
@@ -1698,7 +2156,8 @@ export class FakeServer {
|
|
|
1698
2156
|
let chain = this.hookchains.get(kind);
|
|
1699
2157
|
if (!chain) this.hookchains.set(kind, chain = { pre: [], post: [] });
|
|
1700
2158
|
(post ? chain.post : chain.pre).push(slot);
|
|
1701
|
-
|
|
2159
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2160
|
+
return this.hookSlots.size;
|
|
1702
2161
|
},
|
|
1703
2162
|
|
|
1704
2163
|
// RegisterHam: kept by the function's id and the class, for hamCall().
|
|
@@ -1708,7 +2167,8 @@ export class FakeServer {
|
|
|
1708
2167
|
const list = this.hams.get(key) ?? [];
|
|
1709
2168
|
list.push(slot);
|
|
1710
2169
|
this.hams.set(key, list);
|
|
1711
|
-
|
|
2170
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2171
|
+
return this.hookSlots.size;
|
|
1712
2172
|
},
|
|
1713
2173
|
|
|
1714
2174
|
tag(this: FakeServer, _plugin: PluginInstance, tag: number) {
|
|
@@ -1788,6 +2248,64 @@ export class FakeServer {
|
|
|
1788
2248
|
return length;
|
|
1789
2249
|
},
|
|
1790
2250
|
|
|
2251
|
+
// Entity fields and members, as the module reads them where the game
|
|
2252
|
+
// keeps them (runtime/src/fields.h): an entvar by its offset in
|
|
2253
|
+
// entvars_t, a member by the slot member_slot gave its gamedata name,
|
|
2254
|
+
// the game rules' by the id -1.
|
|
2255
|
+
ent_get(this: FakeServer, plugin: PluginInstance, id: number, offset: number) {
|
|
2256
|
+
const at = tables().entvarAt.get(offset);
|
|
2257
|
+
return at ? fieldCell(this.entities.get(id), at.field, at.component) : 0;
|
|
2258
|
+
},
|
|
2259
|
+
|
|
2260
|
+
ent_set(this: FakeServer, plugin: PluginInstance, id: number, offset: number, cell: number) {
|
|
2261
|
+
const at = tables().entvarAt.get(offset);
|
|
2262
|
+
if (at) setFieldCell(this.entities.get(id), at.field, cell, at.component);
|
|
2263
|
+
},
|
|
2264
|
+
|
|
2265
|
+
ent_entity(this: FakeServer, plugin: PluginInstance, id: number, offset: number) {
|
|
2266
|
+
const at = tables().entvarAt.get(offset);
|
|
2267
|
+
return at ? fieldCell(this.entities.get(id), at.field) : 0;
|
|
2268
|
+
},
|
|
2269
|
+
|
|
2270
|
+
ent_set_entity(this: FakeServer, plugin: PluginInstance, id: number, offset: number, index: number) {
|
|
2271
|
+
const at = tables().entvarAt.get(offset);
|
|
2272
|
+
if (at) setFieldCell(this.entities.get(id), at.field, Math.max(index, 0));
|
|
2273
|
+
},
|
|
2274
|
+
|
|
2275
|
+
member_slot(this: FakeServer, plugin: PluginInstance, className: number, name: number) {
|
|
2276
|
+
const key = `${plugin.memory.string(className)}::${plugin.memory.string(name)}`;
|
|
2277
|
+
const field = tables().memberNamed.get(key);
|
|
2278
|
+
if (field === undefined) throw new Error(`no reapi member is ${key} in the gamedata`);
|
|
2279
|
+
const slot = this.memberSlots.indexOf(field);
|
|
2280
|
+
return slot >= 0 ? slot : this.memberSlots.push(field) - 1;
|
|
2281
|
+
},
|
|
2282
|
+
|
|
2283
|
+
member_get(this: FakeServer, plugin: PluginInstance, id: number, slot: number, element: number) {
|
|
2284
|
+
const field = this.memberSlots[slot];
|
|
2285
|
+
if (id === -1) return this.rules.get(field) ?? 0;
|
|
2286
|
+
return fieldCell(this.entities.get(id), field, element);
|
|
2287
|
+
},
|
|
2288
|
+
|
|
2289
|
+
member_set(this: FakeServer, plugin: PluginInstance, id: number, slot: number, element: number, cell: number) {
|
|
2290
|
+
const field = this.memberSlots[slot];
|
|
2291
|
+
if (id === -1) this.rules.set(field, cell);
|
|
2292
|
+
else setFieldCell(this.entities.get(id), field, cell, element);
|
|
2293
|
+
},
|
|
2294
|
+
|
|
2295
|
+
member_text(this: FakeServer, plugin: PluginInstance, id: number, slot: number, out: number, max: number) {
|
|
2296
|
+
const value = this.entities.get(id)?.fields.get(FakeEntity.key(this.memberSlots[slot]));
|
|
2297
|
+
return plugin.memory.setUtf8(out, max, typeof value === 'string' ? value : '');
|
|
2298
|
+
},
|
|
2299
|
+
|
|
2300
|
+
member_set_text(this: FakeServer, plugin: PluginInstance, id: number, slot: number, text: number) {
|
|
2301
|
+
this.entities.get(id)?.fields.set(FakeEntity.key(this.memberSlots[slot]), plugin.memory.string(text));
|
|
2302
|
+
},
|
|
2303
|
+
|
|
2304
|
+
// The fake keeps its game rules where the module would find them.
|
|
2305
|
+
game_rules() {
|
|
2306
|
+
return 1;
|
|
2307
|
+
},
|
|
2308
|
+
|
|
1791
2309
|
// The fields plugins add to Player: the imports their generated accessors call.
|
|
1792
2310
|
player_data_get(this: FakeServer, plugin: PluginInstance, id: number, key: number) {
|
|
1793
2311
|
return this.playerNumber(id, plugin.memory.string(key));
|
|
@@ -1811,7 +2329,7 @@ export class FakeServer {
|
|
|
1811
2329
|
this.setPlayerData(id, plugin.memory.string(key), plugin.memory.string(value), true);
|
|
1812
2330
|
},
|
|
1813
2331
|
|
|
1814
|
-
//
|
|
2332
|
+
// playerChange: the module wakes a plugin for the fields it listens for.
|
|
1815
2333
|
player_change_listen(this: FakeServer, plugin: PluginInstance, field: number, fn: number) {
|
|
1816
2334
|
const name = plugin.memory.string(field);
|
|
1817
2335
|
const known = this.fieldListeners.find(one => one.plugin === plugin);
|