@amxts/core 0.2.4 → 0.3.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 +5 -5
- package/README.ru.md +5 -5
- package/as/amxts.d.ts +13 -3
- package/as/entities.ts +571 -501
- package/as/events.ts +3323 -557
- package/as/facade.ts +3079 -718
- package/as/fetch.ts +2 -2
- package/as/fs.ts +352 -59
- package/as/hlds.ts +121 -120
- package/as/hooks.ts +10585 -5177
- package/as/lib/check.ts +3 -2
- package/as/natives.ts +856 -1272
- package/as/promise.ts +37 -27
- package/as/vector.ts +94 -14
- package/includes/order.txt +17 -19
- package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
- package/package.json +9 -6
- package/runtime/deps/assemblyscript/dist/asc.js +136 -42
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
- package/runtime/licenses/README.md +9 -7
- package/runtime/licenses/ReGameDLL-LICENSE +21 -0
- package/runtime/licenses/ReHLDS-LICENSE +21 -0
- package/runtime/licenses/zlib-LICENSE +22 -0
- package/runtime/natives.txt +618 -523
- package/runtime/patches/README.md +87 -1
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
- package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
- package/scripts/auto-imports.ts +1 -1
- package/scripts/build-identity.ts +102 -65
- package/scripts/build-wasm.ts +12 -12
- package/scripts/check.ts +4 -1
- package/scripts/client-messages.ts +73 -1
- package/scripts/compile-cache.ts +11 -8
- package/scripts/compile-pool.ts +6 -3
- package/scripts/compile-worker.ts +23 -23
- package/scripts/compile.ts +142 -19
- package/scripts/docs/as/amxts.ts +10 -4
- package/scripts/docs/as/facade.ts +1779 -275
- package/scripts/docs/as/fetch.ts +2 -2
- package/scripts/docs/as/fs.ts +190 -6
- package/scripts/docs/as/vector.ts +81 -0
- package/scripts/docs/entities.ts +12 -0
- package/scripts/docs/events.ts +49 -13
- package/scripts/docs/game.ts +6 -0
- package/scripts/docs/messages.ts +10 -1
- package/scripts/hlds-events.ts +11 -11
- package/scripts/includes.ts +3 -4
- package/scripts/leaf-natives.ts +84 -0
- package/scripts/player-fields.ts +1 -1
- package/scripts/plugin-natives.ts +1 -1
- package/scripts/prebuilt.ts +41 -10
- package/scripts/project.ts +2 -3
- package/scripts/reapi-events.ts +9 -9
- package/scripts/server-env.ts +145 -0
- package/scripts/shared-modules.ts +23 -7
- package/scripts/system.ts +1 -1
- package/scripts/tracked-fs.ts +11 -2
- package/scripts/typed-commands.ts +153 -15
- package/scripts/upgrade-calls.ts +225 -0
- package/scripts/upgrade-names.ts +1 -4
- package/scripts/upgrade.ts +26 -11
- package/src/cli-api.mjs +3 -2
- package/src/system.mjs +21 -11
- package/src/testing/compile.ts +31 -8
- package/src/testing/coroutines.ts +0 -10
- package/src/testing/memory.ts +89 -2
- package/src/testing/natives.ts +67 -30
- package/src/testing/server.ts +1094 -155
- package/src/testing/tables.ts +23 -6
- package/src/typecheck.mjs +110 -0
- package/runtime/licenses/Bun-LICENSE.md +0 -84
package/src/testing/server.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import type { PluginNative } from '../../scripts/plugin-natives';
|
|
2
2
|
import type { Native, NativeCall } from './natives';
|
|
3
3
|
import type { HookShape } from './tables';
|
|
4
|
-
import { pawnLayout } from '../../scripts/plugin-natives';
|
|
5
4
|
// A game server in TypeScript, standing where runtime/src/module.cpp and AMX
|
|
6
5
|
// Mod X stand on a real one.
|
|
7
6
|
//
|
|
8
7
|
// A plugin's wasm imports two things from "env": the bridge externals the
|
|
9
8
|
// facade declares (slot, clcmd, task, on, hook, call, arg_text, say_text ...)
|
|
10
9
|
// and the natives as/natives.ts declares, which take Pawn cells. The bridge is
|
|
11
|
-
// implemented here the way module.cpp implements it -
|
|
10
|
+
// implemented here the way module.cpp implements it - publicFor names, the
|
|
12
11
|
// events registry, Fire with its outcome - and the natives in natives.ts,
|
|
13
12
|
// against the players and entities below. An import neither covers throws,
|
|
14
13
|
// naming itself, the first time the plugin calls it.
|
|
14
|
+
import { basename } from 'node:path';
|
|
15
|
+
import { inflateRawSync } from 'node:zlib';
|
|
16
|
+
import { pawnLayout } from '../../scripts/plugin-natives';
|
|
17
|
+
import { envProblems, parseDotenv } from '../../scripts/server-env';
|
|
15
18
|
import { compile } from './compile';
|
|
16
|
-
import { Coroutines
|
|
19
|
+
import { Coroutines } from './coroutines';
|
|
17
20
|
import { installKitFor } from './kits';
|
|
18
21
|
import { bitsFloat, floatBits, Memory, utf8Fit } from './memory';
|
|
19
|
-
import { fieldCell, NATIVES, setFieldCell, WEAPON_NAMES } from './natives';
|
|
22
|
+
import { AUTH_TYPES, clientCommand, fieldCell, giveWeapon, NATIVES, setFieldCell, switchTo, WEAPON_NAMES } from './natives';
|
|
20
23
|
import { FakeNetwork } from './network';
|
|
21
24
|
import { constant, tables } from './tables';
|
|
22
25
|
|
|
@@ -48,6 +51,13 @@ export function normalizePath(path: string): string {
|
|
|
48
51
|
return path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/** A path of the game folder, or null when it would leave it - absolute, a drive, a `..` - as the module's GamePath. */
|
|
55
|
+
function gamePath(path: string): string | null {
|
|
56
|
+
const normal = normalizePath(path);
|
|
57
|
+
if (normal === '' || path.startsWith('/') || path.startsWith('\\') || path.includes(':') || normal.split('/').includes('..')) return null;
|
|
58
|
+
return normal;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/** A parameter's include default as the value Pawn passes when the call leaves it out. */
|
|
52
62
|
function pawnDefaultValue(text: string | null): Value {
|
|
53
63
|
if (text === null) return 0;
|
|
@@ -67,6 +77,13 @@ interface Handler {
|
|
|
67
77
|
tag?: number;
|
|
68
78
|
/** on_cell's filter: the handler hears the forward only when this argument is `value`. */
|
|
69
79
|
where?: { arg: number; value: number };
|
|
80
|
+
/**
|
|
81
|
+
* The plugin's one listener, called in place of the handler with the
|
|
82
|
+
* event's object (Handler.via in module.cpp, set by on_direct and
|
|
83
|
+
* hook_direct): its table index, its closure's variables, the object, and
|
|
84
|
+
* whether the first cell is the player an async function runs under.
|
|
85
|
+
*/
|
|
86
|
+
via?: { fn: number; env: number; arg: number; player: boolean };
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
/** A host public standing in for a handler: `__amxts_cb<index>`. */
|
|
@@ -81,11 +98,14 @@ interface Slot extends Handler {
|
|
|
81
98
|
* answers the fallback without reaching the plugin.
|
|
82
99
|
*/
|
|
83
100
|
off?: boolean;
|
|
101
|
+
/** A command's admin flags: a player with none of them does not reach it. */
|
|
102
|
+
access?: number;
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
interface Task {
|
|
87
106
|
slot: Slot;
|
|
88
|
-
|
|
107
|
+
/** The timer's own slot, which task hands the plugin: the lowest free one, as module.cpp reuses them. */
|
|
108
|
+
timer: number;
|
|
89
109
|
interval: number;
|
|
90
110
|
due: number;
|
|
91
111
|
repeat: boolean;
|
|
@@ -99,13 +119,17 @@ interface Cvar {
|
|
|
99
119
|
hooks: Slot[];
|
|
100
120
|
}
|
|
101
121
|
|
|
102
|
-
/** The
|
|
122
|
+
/** The hooked call being run: what chain_set writes and arg(-1) reads. */
|
|
103
123
|
export interface Chain {
|
|
104
124
|
shape: HookShape;
|
|
105
125
|
answer: Value;
|
|
106
126
|
args: HookArg[];
|
|
107
|
-
/**
|
|
127
|
+
/** The arguments as cells, which arg() reads: a float as its bits, a vector as three. */
|
|
128
|
+
cells: ArgValue[];
|
|
129
|
+
/** A listener answered (chain_set(-1)): the game's own answer is not taken. */
|
|
108
130
|
answered?: boolean;
|
|
131
|
+
/** A Ham Sandwich function's call: a boolean answer is a number, as Ham Sandwich gives it. */
|
|
132
|
+
ham?: boolean;
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
/** What firing a hookchain came to. */
|
|
@@ -128,9 +152,9 @@ export interface UserMessage {
|
|
|
128
152
|
player: number;
|
|
129
153
|
/** What was written, in order: bytes, shorts and longs as numbers, strings as text. */
|
|
130
154
|
args: (number | string)[];
|
|
131
|
-
/** For
|
|
155
|
+
/** For the engine's own message (`SVC_TEMPENTITY`, `SVC_NEWMOVEVARS`, ...): who it went to, as `MSG_*` - `0` everyone, `4` those who see `origin`, `8` one player. */
|
|
132
156
|
dest?: number;
|
|
133
|
-
/** For
|
|
157
|
+
/** For the engine's own message: the point it is seen from, with `dest` `4`. */
|
|
134
158
|
origin?: number[];
|
|
135
159
|
}
|
|
136
160
|
|
|
@@ -174,7 +198,10 @@ export interface MenuScreen {
|
|
|
174
198
|
/** One line a player was shown. */
|
|
175
199
|
export interface Message {
|
|
176
200
|
variant: 'chat' | 'center' | 'console' | 'notify' | 'hud';
|
|
201
|
+
/** The text, colour codes taken out. */
|
|
177
202
|
text: string;
|
|
203
|
+
/** The line as the client got it, colour codes `\x01`-`\x04` kept. */
|
|
204
|
+
raw: string;
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
export interface ServerOptions {
|
|
@@ -188,6 +215,8 @@ export interface ServerOptions {
|
|
|
188
215
|
cvars?: Record<string, string>;
|
|
189
216
|
/** Files in the game folder before the plugin loads, by path: `{ "addons/amxmodx/configs/x.ini": "..." }`. */
|
|
190
217
|
files?: Record<string, string>;
|
|
218
|
+
/** The server's environment, which `env()` reads over `addons/amxts/.env`: `{ MYPLUGIN_TOKEN: "abc" }`. None unless said. */
|
|
219
|
+
env?: Record<string, string>;
|
|
191
220
|
/**
|
|
192
221
|
* The system the server runs on, as `@amxts/core/os` finds it: "win32" puts the amxts
|
|
193
222
|
* module's amxts_amxx.dll in the modules folder. "linux" unless said.
|
|
@@ -195,8 +224,14 @@ export interface ServerOptions {
|
|
|
195
224
|
platform?: 'win32' | 'linux';
|
|
196
225
|
/** The server's time zone, what Date's local getters read: "Asia/Yerevan". This machine's unless said. */
|
|
197
226
|
timeZone?: string;
|
|
198
|
-
/** Whether
|
|
227
|
+
/** Whether the server has Reunion, so a player's `authType`, `protocol` and `authKey` are his own: the first two through ReHLDS (`chains`), the key through reapi. Not unless said. */
|
|
199
228
|
reunion?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Whether the server has ReGameDLL and ReHLDS - their hookchains, which the
|
|
231
|
+
* module hooks itself, and ReGameDLL's API, through which it acts. As it has
|
|
232
|
+
* reapi (`modules`) unless said: a server without reapi is plain HLDS's model.
|
|
233
|
+
*/
|
|
234
|
+
chains?: boolean;
|
|
200
235
|
}
|
|
201
236
|
|
|
202
237
|
export type TeamName = 'UNASSIGNED' | 'TERRORIST' | 'CT' | 'SPECTATOR';
|
|
@@ -228,20 +263,30 @@ export interface JoinOptions {
|
|
|
228
263
|
const SHAPE_NARROW = 0;
|
|
229
264
|
const SHAPE_WIDE = 1;
|
|
230
265
|
/** A slot taken back after a reload: SLOT_REUSED in module.cpp. */
|
|
231
|
-
const SLOT_REUSED =
|
|
266
|
+
const SLOT_REUSED = 0x40000000;
|
|
232
267
|
/** A request that tells an owner a plugin which called it stopped: RPC_GONE in module.cpp, KIND_GONE in as/remote.ts. */
|
|
233
268
|
const RPC_GONE = 2;
|
|
234
269
|
const PLUGIN_HANDLED = 1;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
// What a hooked function's listener says: the game's function does not run;
|
|
271
|
+
// and the listeners after it do not either (OUTCOME_* in gamehooks.h).
|
|
272
|
+
const OUTCOME_BREAK = 2;
|
|
273
|
+
// fakemeta's answer that blocks the game's function.
|
|
239
274
|
const FMRES_SUPERCEDE = 4;
|
|
240
275
|
// A message argument's types, as get_msg_argtype answers.
|
|
241
276
|
const ARG_BYTE = 1;
|
|
277
|
+
const ARG_ANGLE = 5;
|
|
242
278
|
const ARG_COORD = 6;
|
|
243
279
|
const ARG_STRING = 7;
|
|
244
280
|
|
|
281
|
+
// A message's destination, as the module hands it to a message's hook.
|
|
282
|
+
const MSG_ONE = 1;
|
|
283
|
+
const MSG_ALL = 2;
|
|
284
|
+
|
|
285
|
+
// The functions stock_hook takes, by number (STOCK_* in as/facade.ts), and
|
|
286
|
+
// the fakemeta function each is.
|
|
287
|
+
const STOCK_FUNCTIONS = ['FM_SetModel', 'FM_EmitSound', 'FM_Voice_SetClientListening', 'FM_PrecacheModel', 'FM_PrecacheSound', 'FM_PrecacheGeneric', 'FM_GetGameDescription'];
|
|
288
|
+
const STOCK_CVAR_ANSWER = 7;
|
|
289
|
+
|
|
245
290
|
/**
|
|
246
291
|
* Date#getTimezoneOffset() in a time zone: minutes from its wall clock at
|
|
247
292
|
* that moment to UTC, UTC+4 being -240. This machine's without one.
|
|
@@ -303,11 +348,52 @@ export class FakeWeapon extends FakeEntity {
|
|
|
303
348
|
constructor(server: FakeServer, id: number, readonly kind: string, owner: number) {
|
|
304
349
|
super(server, id, kind);
|
|
305
350
|
this.set('m_iId', WEAPON_NAMES.indexOf(kind));
|
|
351
|
+
this.set('m_Weapon_iPrimaryAmmoType', ammoTypeOf(kind));
|
|
306
352
|
this.set('var_owner', owner);
|
|
307
353
|
this.set('m_pPlayer', owner);
|
|
308
354
|
}
|
|
309
355
|
}
|
|
310
356
|
|
|
357
|
+
/** A player's backpack ammo by weapon name, read and written in his m_rgAmmo. */
|
|
358
|
+
class AmmoMap extends Map<string, number> {
|
|
359
|
+
constructor(private readonly owner: FakeEntity) {
|
|
360
|
+
super();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
override get(weapon: string): number {
|
|
364
|
+
return ammoTypeOf(weapon) < 0 ? 0 : Number(this.owner.get('m_rgAmmo', ammoTypeOf(weapon)));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
override set(weapon: string, amount: number): this {
|
|
368
|
+
if (ammoTypeOf(weapon) >= 0) this.owner.set('m_rgAmmo', amount, ammoTypeOf(weapon));
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The kind of ammo a weapon takes, its place in m_rgAmmo, as the game numbers them; -1 for none. */
|
|
374
|
+
function ammoTypeOf(kind: string): number {
|
|
375
|
+
const at = AMMO_KINDS.findIndex(weapons => weapons.split(' ').includes(kind.replace('weapon_', '')));
|
|
376
|
+
return at < 0 ? -1 : at + 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The weapons each kind of ammo is for, from 1: .338 Magnum, 7.62 NATO, ... the bomb. */
|
|
380
|
+
const AMMO_KINDS = [
|
|
381
|
+
'awp',
|
|
382
|
+
'ak47 scout g3sg1',
|
|
383
|
+
'm249',
|
|
384
|
+
'aug sg550 galil famas m4a1 sg552',
|
|
385
|
+
'm3 xm1014',
|
|
386
|
+
'usp mac10 ump45',
|
|
387
|
+
'p90 fiveseven',
|
|
388
|
+
'deagle',
|
|
389
|
+
'p228',
|
|
390
|
+
'glock18 mp5navy tmp elite',
|
|
391
|
+
'flashbang',
|
|
392
|
+
'hegrenade',
|
|
393
|
+
'smokegrenade',
|
|
394
|
+
'c4',
|
|
395
|
+
];
|
|
396
|
+
|
|
311
397
|
/** Which of m_rgpPlayerItems a weapon goes in: 1 primary, 2 pistol, 3 knife, 4 grenades, 5 C4. */
|
|
312
398
|
function slotOf(kind: string): number {
|
|
313
399
|
if (kind === 'weapon_knife') return 3;
|
|
@@ -331,12 +417,16 @@ export class FakePlayer extends FakeEntity {
|
|
|
331
417
|
flags: number;
|
|
332
418
|
/** SPEAK_* flags: `muted` is SPEAK_MUTED. */
|
|
333
419
|
speak = 0;
|
|
334
|
-
/** Backpack ammo by weapon name. */
|
|
335
|
-
readonly ammo
|
|
420
|
+
/** Backpack ammo by weapon name: his m_rgAmmo at the weapon's kind of ammo, as the game keeps it. */
|
|
421
|
+
readonly ammo: Map<string, number> = new AmmoMap(this);
|
|
336
422
|
/** Every line he was shown, in order. */
|
|
337
423
|
readonly messages: Message[] = [];
|
|
338
424
|
/** What was run in his console: client_cmd, engclient_cmd. */
|
|
339
425
|
readonly commands: string[] = [];
|
|
426
|
+
/** Whether his steps make no sound (player_silent). */
|
|
427
|
+
silentSteps = false;
|
|
428
|
+
/** When he connected, by the server's clock (`server.time`, milliseconds). */
|
|
429
|
+
readonly connectedAt: number;
|
|
340
430
|
/** His userinfo keys - `lang` says his language. */
|
|
341
431
|
readonly info = new Map<string, string>();
|
|
342
432
|
/** The menu of AMX Mod X's own on his screen (menu_display), or null; `menuselect <key>` answers it. */
|
|
@@ -344,8 +434,22 @@ export class FakePlayer extends FakeEntity {
|
|
|
344
434
|
/** His userid, the number `#12` names him by in a command: the server counts them up as players come. */
|
|
345
435
|
readonly userid: number;
|
|
346
436
|
|
|
347
|
-
|
|
437
|
+
/** His name. A new one reaches the plugins as AMX Mod X's does: their facades read it again. */
|
|
438
|
+
get name(): string {
|
|
439
|
+
return this.named;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
set name(value: string) {
|
|
443
|
+
this.named = value;
|
|
444
|
+
this.set('var_netname', value);
|
|
445
|
+
this.server.countNameChange(this.id);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private named: string;
|
|
449
|
+
|
|
450
|
+
constructor(server: FakeServer, id: number, name: string, options: JoinOptions) {
|
|
348
451
|
super(server, id, 'player');
|
|
452
|
+
this.named = name;
|
|
349
453
|
this.alive = options.alive ?? true;
|
|
350
454
|
this.bot = options.bot ?? false;
|
|
351
455
|
this.steamId = options.steamId ?? (this.bot ? 'BOT' : `STEAM_0:0:${id}`);
|
|
@@ -355,6 +459,7 @@ export class FakePlayer extends FakeEntity {
|
|
|
355
459
|
this.authKey = options.authKey ?? '';
|
|
356
460
|
this.flags = flagBits(options.flags ?? 'z');
|
|
357
461
|
this.userid = server.nextUserid();
|
|
462
|
+
this.connectedAt = server.time;
|
|
358
463
|
this.team = options.team ?? 'CT';
|
|
359
464
|
this.health = options.health ?? 100;
|
|
360
465
|
this.armor = options.armor ?? 0;
|
|
@@ -387,6 +492,11 @@ export class FakePlayer extends FakeEntity {
|
|
|
387
492
|
|
|
388
493
|
/** The chat lines he was shown, one a line, colour bytes taken out. */
|
|
389
494
|
get chat(): string { return this.lines('chat'); }
|
|
495
|
+
/**
|
|
496
|
+
* The chat lines as the client got them, colour codes `\x01`-`\x04` kept:
|
|
497
|
+
* the client draws no colour in a line that does not start with one.
|
|
498
|
+
*/
|
|
499
|
+
get chatRaw(): string { return this.messages.filter(m => m.variant === 'chat').map(m => m.raw).join('\n'); }
|
|
390
500
|
/** What was printed in the middle of his screen. */
|
|
391
501
|
get center(): string { return this.lines('center'); }
|
|
392
502
|
/** What was printed in his console. */
|
|
@@ -402,7 +512,7 @@ export class FakePlayer extends FakeEntity {
|
|
|
402
512
|
show(variant: Message['variant'], text: string): void {
|
|
403
513
|
// The colour codes ^1-^4 a chat line carries are not text.
|
|
404
514
|
// oxlint-disable-next-line no-control-regex
|
|
405
|
-
this.messages.push({ variant, text: text.replace(/[\x01-\x04]/g, '') });
|
|
515
|
+
this.messages.push({ variant, text: text.replace(/[\x01-\x04]/g, ''), raw: text });
|
|
406
516
|
}
|
|
407
517
|
|
|
408
518
|
/** Forgets what he was shown, for a test that checks one step at a time. */
|
|
@@ -448,6 +558,21 @@ export class FakePlayer extends FakeEntity {
|
|
|
448
558
|
return weapon;
|
|
449
559
|
}
|
|
450
560
|
|
|
561
|
+
/** Takes a weapon out of his slots, as the game's RemovePlayerItem does: it is no longer his. */
|
|
562
|
+
takeWeapon(weapon: FakeEntity): void {
|
|
563
|
+
const slot = slotOf(weapon.classname);
|
|
564
|
+
const first = Number(this.get('m_rgpPlayerItems', slot));
|
|
565
|
+
if (first === weapon.id) {
|
|
566
|
+
this.set('m_rgpPlayerItems', Number(weapon.get('m_pNext')), slot);
|
|
567
|
+
} else {
|
|
568
|
+
for (let at = this.server.entities.get(first); at; at = this.server.entities.get(Number(at.get('m_pNext')))) {
|
|
569
|
+
if (Number(at.get('m_pNext')) === weapon.id) at.set('m_pNext', Number(weapon.get('m_pNext')));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
weapon.set('m_pNext', 0);
|
|
573
|
+
if (Number(this.get('m_pActiveItem')) === weapon.id) this.set('m_pActiveItem', 0);
|
|
574
|
+
}
|
|
575
|
+
|
|
451
576
|
/** Takes every weapon away. */
|
|
452
577
|
stripWeapons(): void {
|
|
453
578
|
for (const weapon of this.items) this.server.entities.delete(weapon.id);
|
|
@@ -477,12 +602,17 @@ export class FakePlayer extends FakeEntity {
|
|
|
477
602
|
|
|
478
603
|
/**
|
|
479
604
|
* A console command, split the way the engine splits it: `amx_slap "Some One" 5`.
|
|
480
|
-
* `menuselect <key>` goes to his menu
|
|
605
|
+
* `menuselect <key>` goes to his menu of AMX Mod X's own first, as AMX Mod
|
|
606
|
+
* X takes it, and last to the menu a plugin's Menu shows him, as the
|
|
607
|
+
* module takes it; the key `0` or `10`. True if handled.
|
|
481
608
|
*/
|
|
482
609
|
command(line: string): boolean {
|
|
483
610
|
const argv = splitCommand(line);
|
|
484
|
-
|
|
485
|
-
|
|
611
|
+
const select = argv[0] === 'menuselect';
|
|
612
|
+
const key = Number(argv[1]) % 10;
|
|
613
|
+
const pressed = select ? this.server.takeMenuKey(this, key) : null;
|
|
614
|
+
if (select && this.menu && this.server.selectMenu(this, key)) return true;
|
|
615
|
+
return this.server.clientCommand(this, argv) || (pressed !== null && this.server.pressMenu(this, pressed));
|
|
486
616
|
}
|
|
487
617
|
|
|
488
618
|
/** Leaves the server: the disconnect events, then the slot is free. */
|
|
@@ -491,6 +621,7 @@ export class FakePlayer extends FakeEntity {
|
|
|
491
621
|
const reason = options.reason ?? 'Client sent \'drop\'';
|
|
492
622
|
this.server.fire('client_disconnected', this.id, dropped, reason, 192);
|
|
493
623
|
this.server.clearPlayerData(this.id);
|
|
624
|
+
this.server.shownMenus.delete(this.id);
|
|
494
625
|
this.server.fire('client_disconnect', this.id);
|
|
495
626
|
this.connected = false;
|
|
496
627
|
this.alive = false;
|
|
@@ -520,6 +651,10 @@ function unsimulated(name: string): Error {
|
|
|
520
651
|
}
|
|
521
652
|
|
|
522
653
|
/** A command line as the engine splits it: on spaces, a quoted part kept whole. */
|
|
654
|
+
function addTo(commands: Map<string, Slot[]>, name: string, slot: Slot): void {
|
|
655
|
+
commands.set(name, [...(commands.get(name) ?? []), slot]);
|
|
656
|
+
}
|
|
657
|
+
|
|
523
658
|
function splitCommand(line: string): string[] {
|
|
524
659
|
return [...line.matchAll(/"([^"]*)"|(\S+)/g)].map(m => m[1] ?? m[2]);
|
|
525
660
|
}
|
|
@@ -539,6 +674,12 @@ export function splitLog(line: string): string[] {
|
|
|
539
674
|
/** The runs handed out so far: each plugin loaded is a new one. */
|
|
540
675
|
let runs = 0;
|
|
541
676
|
|
|
677
|
+
/** A trace's start and end, six numbers (f64) where the plugin's facade put them. */
|
|
678
|
+
function tracePoints(plugin: PluginInstance, at: number): number[][] {
|
|
679
|
+
const numbers = Array.from({ length: 6 }, (_, i) => plugin.memory.number(at + i * 8));
|
|
680
|
+
return [numbers.slice(0, 3), numbers.slice(3)];
|
|
681
|
+
}
|
|
682
|
+
|
|
542
683
|
export class PluginInstance {
|
|
543
684
|
instance: any;
|
|
544
685
|
memory!: Memory;
|
|
@@ -552,6 +693,10 @@ export class PluginInstance {
|
|
|
552
693
|
readonly uses: string[] = [];
|
|
553
694
|
/** It called back a function of a plugin unloaded since, and was told so: once. */
|
|
554
695
|
toldGone = false;
|
|
696
|
+
/** Where its facade's table of the slots a new player took is (player_slots); 0 until it gives one. */
|
|
697
|
+
playerSlots = 0;
|
|
698
|
+
/** Where its facade's count of each slot's name changes is (player_names); 0 until it gives one. */
|
|
699
|
+
playerNames = 0;
|
|
555
700
|
|
|
556
701
|
/** The coroutine scheduler, for a plugin that awaits; null for one that does not. */
|
|
557
702
|
readonly coroutines: Coroutines | null;
|
|
@@ -584,12 +729,13 @@ export class PluginInstance {
|
|
|
584
729
|
return (...args: number[]) => bridge.call(this.server, this, ...args);
|
|
585
730
|
}
|
|
586
731
|
|
|
587
|
-
|
|
732
|
+
const marks = tables().crossings.get(name) ?? [];
|
|
733
|
+
if (this.server.hasNative(name)) return (...args: number[]) => this.memory.across(marks, args, cells => this.server.callNativeImpl(this, name, cells));
|
|
588
734
|
|
|
589
735
|
// Another plugin's `export function`, called through @amxts/core/natives as AMX
|
|
590
736
|
// Mod X would route it; whether one exports it is known only at the call.
|
|
591
|
-
return (...
|
|
592
|
-
if (this.server.exported.has(name)) return this.server.callFromPlugin(this, name, cells);
|
|
737
|
+
return (...args: number[]) => {
|
|
738
|
+
if (this.server.exported.has(name)) return this.memory.across(marks, args, cells => this.server.callFromPlugin(this, name, cells));
|
|
593
739
|
throw unsimulated(name);
|
|
594
740
|
};
|
|
595
741
|
}
|
|
@@ -609,8 +755,30 @@ export class FakeServer {
|
|
|
609
755
|
readonly map: string;
|
|
610
756
|
readonly maxPlayers: number;
|
|
611
757
|
readonly modules: Set<string>;
|
|
758
|
+
/** The GeoIP database, by address: `server.countries.set("1.2.3.4", { code: "DE", name: "Germany" })`. */
|
|
759
|
+
readonly countries = new Map<string, { code: string; name: string }>();
|
|
760
|
+
/** The server's environment, which `env()` reads over `addons/amxts/.env` (a file in `files`). */
|
|
761
|
+
env: Record<string, string>;
|
|
762
|
+
/** The model a plugin put on each player (player.model), by his id. */
|
|
763
|
+
readonly models = new Map<number, string>();
|
|
764
|
+
/** The entity each player sees through (player.view), by his id; 0 or none for his own eyes. */
|
|
765
|
+
readonly views = new Map<number, number>();
|
|
766
|
+
/** The maps the fake server has: map_valid and change_level know these. */
|
|
767
|
+
readonly maps: string[] = ['de_dust2', 'de_inferno', 'cs_office'];
|
|
768
|
+
/**
|
|
769
|
+
* Walls of the fake world a trace stops at: a plane at `x`, with the
|
|
770
|
+
* entity a trace that meets it hits (0, the world, when left out) and the
|
|
771
|
+
* hit group.
|
|
772
|
+
*/
|
|
773
|
+
readonly walls: { x: number; entity?: number; hitGroup?: number }[] = [];
|
|
774
|
+
/** What plugins asked of plugins (server.plugins), in order: "stop shop.aot", "reload shop.aot". */
|
|
775
|
+
readonly pluginActions: string[] = [];
|
|
776
|
+
/** What the game rules were asked to run, in order: "restartRound", "checkWinConditions". */
|
|
777
|
+
readonly rulesRuns: string[] = [];
|
|
612
778
|
/** Whether reapi finds Reunion (has_reunion). */
|
|
613
779
|
readonly reunion: boolean;
|
|
780
|
+
/** Whether the module finds ReGameDLL's and ReHLDS's hookchains (game_api). */
|
|
781
|
+
readonly chains: boolean;
|
|
614
782
|
/** Milliseconds since the map started; advance() moves it. */
|
|
615
783
|
time = 0;
|
|
616
784
|
/** Date.now() when the map started. */
|
|
@@ -666,13 +834,28 @@ export class FakeServer {
|
|
|
666
834
|
readonly exported = new Map<string, Handler>();
|
|
667
835
|
readonly cvars = new Map<string, Cvar>();
|
|
668
836
|
readonly vaultHandles: string[] = [];
|
|
837
|
+
/** The Storages plugins opened, by handle: their names. */
|
|
838
|
+
readonly storeNames: string[] = [];
|
|
839
|
+
/** When each Storage key was last set, by "name\0key": the fake's clock, in seconds. */
|
|
840
|
+
readonly storeTimes = new Map<string, number>();
|
|
669
841
|
readonly forwardHandles: { name: string; types: number[] }[] = [];
|
|
670
|
-
|
|
842
|
+
/** How the last HUD message was sent: its colour, place, hold, channel (-1 automatic), line (0 none) and size. */
|
|
843
|
+
hud = { color: [200, 100, 0], x: -1, y: 0.35, hold: 12, channel: -1, line: 0, large: false };
|
|
844
|
+
/** The HUD lines plugins made. */
|
|
845
|
+
hudLines = 0;
|
|
671
846
|
|
|
672
847
|
/** The game rules' members a plugin set (set_member_game), by member. */
|
|
673
848
|
readonly rules = new Map<number, number>();
|
|
674
849
|
/** The members plugins asked member_slot for, by slot: each one's reapi constant. */
|
|
675
850
|
private readonly memberSlots: number[] = [];
|
|
851
|
+
/**
|
|
852
|
+
* What playbackEvent's listeners asked of the last event `fire` raised:
|
|
853
|
+
* stopped (`preventDefault()`), or played only to these player ids
|
|
854
|
+
* (`recipients`), `null` for all it reaches.
|
|
855
|
+
*/
|
|
856
|
+
playback: { blocked: boolean; recipients: number[] | null } = { blocked: false, recipients: null };
|
|
857
|
+
/** The map's light, as `server.lightStyle` last set it: `"m"` the map's own. */
|
|
858
|
+
lightStyle = 'm';
|
|
676
859
|
/** Rounds a plugin ended with rg_round_end, in order. */
|
|
677
860
|
readonly roundEnds: { status: number; event: number; delay: number; message: string; sound: string; trigger: boolean }[] = [];
|
|
678
861
|
/** Sounds played: from an entity (emit_sound, rh_emit_sound2), or to a player alone (SendAudio, rg_send_audio). */
|
|
@@ -682,6 +865,12 @@ export class FakeServer {
|
|
|
682
865
|
* went to (0 for everyone), and what was written, write_* by write_*.
|
|
683
866
|
*/
|
|
684
867
|
readonly userMessages: UserMessage[] = [];
|
|
868
|
+
/**
|
|
869
|
+
* The player's movement a movement event (`airMove`, `jumpMovement`, ...)
|
|
870
|
+
* reads: `event.touchCount` and `event.frameTime`. Set it before
|
|
871
|
+
* fireHook.
|
|
872
|
+
*/
|
|
873
|
+
readonly playerMove = { touchCount: 0, frameTime: 0 };
|
|
685
874
|
/** The ids get_user_msgid handed out, by name - ShowMenu's is the game's 96. @internal */
|
|
686
875
|
readonly messageIds = new Map<string, number>([['ShowMenu', 96]]);
|
|
687
876
|
/** The message between message_begin and message_end. @internal */
|
|
@@ -704,10 +893,16 @@ export class FakeServer {
|
|
|
704
893
|
readonly hamCalls: { fn: number; entity: number; args: number[]; hooks: boolean }[] = [];
|
|
705
894
|
/** register_message's callbacks, by the message's id. */
|
|
706
895
|
readonly messageHooks = new Map<number, Slot[]>();
|
|
896
|
+
/** The module's hooks of a message (msg_hook), by its id: they hear it after register_message's. @internal */
|
|
897
|
+
readonly moduleMessageHooks = new Map<number, Slot[]>();
|
|
707
898
|
/** The message register_message's callbacks are reading and writing: its arguments and their ARG_* types. @internal */
|
|
708
899
|
hookedMessage: { args: (number | string)[]; types: number[] } | null = null;
|
|
709
900
|
/** register_touch's callbacks. */
|
|
710
901
|
readonly touches: { touched: string; toucher: string; slot: Slot }[] = [];
|
|
902
|
+
/** The entityState listeners' hooks: the class each is about, "" any. */
|
|
903
|
+
readonly stateHooks: { classname: string; slot: Slot }[] = [];
|
|
904
|
+
/** The state being sent while its listeners run (sendState), by the module's field numbers. */
|
|
905
|
+
sending: number[] | null = null;
|
|
711
906
|
/** register_logevent's callbacks: the number of arguments a line has, and the filters it passes. @internal */
|
|
712
907
|
readonly logEvents: { argc: number; filters: string[]; slot: Slot }[] = [];
|
|
713
908
|
/** The log line logevent callbacks are reading, as AMX Mod X splits it. @internal */
|
|
@@ -718,6 +913,8 @@ export class FakeServer {
|
|
|
718
913
|
readonly messageEvents: { name: string; conditions: string[]; slot: Slot }[] = [];
|
|
719
914
|
/** register_forward's callbacks, by `<FM_* number>:pre|post`. */
|
|
720
915
|
readonly fakemetaForwards = new Map<string, Slot[]>();
|
|
916
|
+
/** The module's hooks of the same functions (stock_hook), by `<STOCK_* number>:pre|post`. @internal */
|
|
917
|
+
readonly stockHooks = new Map<string, Slot[]>();
|
|
721
918
|
/** What a fakemeta callback answered with forward_return, and what get_orig_retval reads. @internal */
|
|
722
919
|
forwardAnswer: number | string | null = null;
|
|
723
920
|
origRetval = 0;
|
|
@@ -725,12 +922,15 @@ export class FakeServer {
|
|
|
725
922
|
readonly listening: number[][] = [];
|
|
726
923
|
/** 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
924
|
readonly engineCalls: string[] = [];
|
|
728
|
-
/** query_client_cvar's questions, waiting for answerCvar(). */
|
|
729
|
-
readonly cvarQueries: { player: number; cvar: string; slot
|
|
925
|
+
/** query_client_cvar's questions, and the module's (query_cvar) by their ids, waiting for answerCvar(). */
|
|
926
|
+
readonly cvarQueries: { player: number; cvar: string; slot?: Slot; request?: number }[] = [];
|
|
927
|
+
private cvarRequests = 0;
|
|
730
928
|
/** The menus menu_create made and menu_destroy has not taken away, by id. */
|
|
731
929
|
readonly menus = new Map<number, FakeMenu>();
|
|
732
930
|
/** menu_makecallback's publics, by the number it gave. @internal */
|
|
733
931
|
readonly menuCallbacks: Slot[] = [];
|
|
932
|
+
/** The menu a plugin's Menu shows each player (menu_open), by his id: its handler of the keys, and the keys it takes as show_menu's bits. @internal */
|
|
933
|
+
readonly shownMenus = new Map<number, Handler & { keys: number }>();
|
|
734
934
|
private menuIds = 0;
|
|
735
935
|
private userids = 1;
|
|
736
936
|
|
|
@@ -776,10 +976,15 @@ export class FakeServer {
|
|
|
776
976
|
this.maxPlayers = options.maxPlayers ?? 32;
|
|
777
977
|
this.modules = new Set(options.modules ?? ['reapi', 'cstrike', 'fun', 'hamsandwich', 'engine', 'fakemeta', 'nvault', 'resemiclip']);
|
|
778
978
|
this.reunion = options.reunion ?? false;
|
|
979
|
+
this.chains = options.chains ?? this.modules.has('reapi');
|
|
980
|
+
// The voice manager counts the server's slots, as the game sets it up.
|
|
981
|
+
this.rules.set(constant('m_nMaxPlayers'), this.maxPlayers);
|
|
779
982
|
this.entityIds = this.maxPlayers + 1;
|
|
780
983
|
this.timeZone = options.timeZone;
|
|
781
|
-
|
|
782
|
-
|
|
984
|
+
this.env = { ...options.env };
|
|
985
|
+
// AMX Mod X's own, which a test may set: the languages; and the game's
|
|
986
|
+
// a plugin often sets, at the game's defaults.
|
|
987
|
+
const cvars = { amx_language: 'en', amx_client_languages: '1', mp_freezetime: '6', mp_round_infinite: '0', ...options.cvars };
|
|
783
988
|
for (const [name, value] of Object.entries(cvars)) this.createCvar(name, value);
|
|
784
989
|
for (const [path, text] of Object.entries(options.files ?? {})) this.writeFile(path, text);
|
|
785
990
|
if (options.platform === 'win32') this.writeFile('addons/amxmodx/modules/amxts_amxx.dll', '');
|
|
@@ -816,7 +1021,10 @@ export class FakeServer {
|
|
|
816
1021
|
*/
|
|
817
1022
|
async load(source: string): Promise<PluginInstance> {
|
|
818
1023
|
await installKitFor(this, source);
|
|
819
|
-
const { module, natives, binary, async } = await compile(source);
|
|
1024
|
+
const { module, natives, binary, async, env } = await compile(source);
|
|
1025
|
+
// A variable it requires that neither the environment nor .env has, or one of another kind, keeps it from starting, as the module does.
|
|
1026
|
+
const missing = envProblems(env, this.variables());
|
|
1027
|
+
if (missing.length) throw new Error(`[amxts] ${basename(source, '.ts')} needs ${missing.join(', ')} in addons/amxts/.env`);
|
|
820
1028
|
const plugin = new PluginInstance(this, source, module, natives, async ? binary : undefined);
|
|
821
1029
|
this.plugins.push(plugin);
|
|
822
1030
|
for (const native of natives) {
|
|
@@ -861,6 +1069,13 @@ export class FakeServer {
|
|
|
861
1069
|
return this.cvars.get(name.toLowerCase())?.value;
|
|
862
1070
|
}
|
|
863
1071
|
|
|
1072
|
+
/** What `env()` reads: `addons/amxts/.env`, and the environment over it. @internal */
|
|
1073
|
+
variables(): Map<string, string> {
|
|
1074
|
+
const variables = parseDotenv(this.file('addons/amxts/.env') ?? '');
|
|
1075
|
+
for (const [name, value] of Object.entries(this.env)) variables.set(name, value);
|
|
1076
|
+
return variables;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
864
1079
|
/** Sets a cvar as the console would: `mp_freezetime 5`, and its change listeners hear it. */
|
|
865
1080
|
setCvar(name: string, value: string | number): void {
|
|
866
1081
|
const cvar = this.cvars.get(name.toLowerCase()) ?? this.createCvar(name, '');
|
|
@@ -880,8 +1095,12 @@ export class FakeServer {
|
|
|
880
1095
|
|
|
881
1096
|
// ------------------------------------------------------------ dictionaries
|
|
882
1097
|
|
|
883
|
-
/**
|
|
884
|
-
|
|
1098
|
+
/**
|
|
1099
|
+
* The dictionaries' lines by language, then key: what register_dictionary
|
|
1100
|
+
* loaded and translate() added. It starts with the languages AMX Mod X's
|
|
1101
|
+
* own dictionaries bring, as a server's does.
|
|
1102
|
+
*/
|
|
1103
|
+
readonly dictionary = new Map<string, Map<string, string>>(['en', 'de', 'sr', 'tr', 'fr', 'sv', 'da', 'pl', 'nl', 'es', 'bp', 'cz', 'fi', 'ru'].map(code => [code, new Map()]));
|
|
885
1104
|
/** Every key in the order it came, as GetLangTransKey numbers them. */
|
|
886
1105
|
private readonly langKeys: string[] = [];
|
|
887
1106
|
|
|
@@ -986,11 +1205,36 @@ export class FakeServer {
|
|
|
986
1205
|
return array.items.map(item => array.cellSize === 1 ? item[0] : [...item]);
|
|
987
1206
|
}
|
|
988
1207
|
|
|
989
|
-
/** A
|
|
990
|
-
|
|
991
|
-
let
|
|
992
|
-
|
|
993
|
-
|
|
1208
|
+
/** A trace from `start` to `end` through the fake world, written as the module writes it: stopped at the first wall across it. @internal */
|
|
1209
|
+
writeTrace(plugin: PluginInstance, out: number, start: number[], end: number[]): void {
|
|
1210
|
+
let fraction = 1;
|
|
1211
|
+
let wall: { x: number; entity?: number; hitGroup?: number } | null = null;
|
|
1212
|
+
for (const each of this.walls) {
|
|
1213
|
+
const at = end[0] === start[0] ? -1 : (each.x - start[0]) / (end[0] - start[0]);
|
|
1214
|
+
if (at >= 0 && at < fraction) {
|
|
1215
|
+
fraction = at;
|
|
1216
|
+
wall = each;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const stop = start.map((v, i) => v + (end[i] - v) * fraction);
|
|
1220
|
+
const normal = wall ? [Math.sign(start[0] - end[0]), 0, 0] : [0, 0, 0];
|
|
1221
|
+
const values = [fraction, ...stop, ...normal, wall ? (wall.entity ?? 0) : -1, 4, wall?.hitGroup ?? 0];
|
|
1222
|
+
values.forEach((value, i) => plugin.memory.setNumber(out + i * 8, value));
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/** A Storage's values by key, as a Map a test can fill before the plugin reads it - a Pawn plugin's nVault of that name too. */
|
|
1226
|
+
storage(name: string): Map<string, string> {
|
|
1227
|
+
let storage = this.vaults.get(name);
|
|
1228
|
+
if (!storage) this.vaults.set(name, storage = new Map());
|
|
1229
|
+
return storage;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/** A bot's move, through engfunc or the module's own call: written to engineCalls, and the bot carried along its yaw. @internal */
|
|
1233
|
+
runPlayerMove(id: number, angles: number[], forward: number, side: number, up: number, buttons: number, impulse: number, msec: number): void {
|
|
1234
|
+
this.engineCalls.push(`RunPlayerMove ${id} ${angles.join(',')} ${forward} ${side} ${up} ${buttons} ${impulse} ${msec}`);
|
|
1235
|
+
const bot = this.players.find(each => each.id === id);
|
|
1236
|
+
const turn = angles[1] * Math.PI / 180;
|
|
1237
|
+
if (bot) bot.origin = [bot.origin[0] + Math.cos(turn) * forward * msec / 1000, bot.origin[1] + Math.sin(turn) * forward * msec / 1000, bot.origin[2]];
|
|
994
1238
|
}
|
|
995
1239
|
|
|
996
1240
|
// ------------------------------------------------------------ what a test does
|
|
@@ -1045,14 +1289,14 @@ export class FakeServer {
|
|
|
1045
1289
|
if (due.repeat) due.due += due.interval;
|
|
1046
1290
|
else this.tasks.splice(this.tasks.indexOf(due), 1);
|
|
1047
1291
|
|
|
1048
|
-
this.withCallArgs([due.
|
|
1292
|
+
this.withCallArgs([due.timer], () => this.call(due.slot, [due.timer], 0));
|
|
1049
1293
|
}
|
|
1050
1294
|
|
|
1051
1295
|
this.time = until;
|
|
1052
1296
|
}
|
|
1053
1297
|
|
|
1054
1298
|
/**
|
|
1055
|
-
* Raises a forward the
|
|
1299
|
+
* Raises a forward the module raises - "client_putinserver",
|
|
1056
1300
|
* "plugin_cfg" - or one a Pawn plugin sends: every handler and every
|
|
1057
1301
|
* Forward.subscribe() of that name hears it. Text arrives as text, a
|
|
1058
1302
|
* boolean as 1 or 0, an array as its cells (a Float's as its bits). As in
|
|
@@ -1061,6 +1305,8 @@ export class FakeServer {
|
|
|
1061
1305
|
*/
|
|
1062
1306
|
fire(name: string, ...args: ArgValue[]): number {
|
|
1063
1307
|
const first = typeof args[0] === 'number' || typeof args[0] === 'boolean' ? Number(args[0]) : 0;
|
|
1308
|
+
if (name === 'client_connect') this.newPlayer(first);
|
|
1309
|
+
if (name === 'pfn_playbackevent') this.playback = { blocked: false, recipients: null };
|
|
1064
1310
|
return this.withCallArgs(args, () => {
|
|
1065
1311
|
this.deliver(name);
|
|
1066
1312
|
|
|
@@ -1074,6 +1320,22 @@ export class FakeServer {
|
|
|
1074
1320
|
});
|
|
1075
1321
|
}
|
|
1076
1322
|
|
|
1323
|
+
/** A player connects to slot `id`: each plugin's facade makes him a Player of his own, as the module's NewPlayer has it. */
|
|
1324
|
+
private newPlayer(id: number): void {
|
|
1325
|
+
for (const plugin of this.plugins) {
|
|
1326
|
+
if (plugin.playerSlots && id >= 0 && id <= 32) plugin.memory.setCell(plugin.playerSlots + id * 4, 1);
|
|
1327
|
+
}
|
|
1328
|
+
this.countNameChange(id);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Player `id`'s name changed: each plugin's facade reads it again, as the module's NameChanges has it. */
|
|
1332
|
+
countNameChange(id: number): void {
|
|
1333
|
+
for (const plugin of this.plugins) {
|
|
1334
|
+
const at = plugin.playerNames + id * 4;
|
|
1335
|
+
if (plugin.playerNames && id >= 0 && id <= 32) plugin.memory.setCell(at, plugin.memory.cell(at) + 2);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1077
1339
|
/** Every Forward.subscribe() of a forward, called with its tag; the arguments are the context's. */
|
|
1078
1340
|
private deliver(forward: string): void {
|
|
1079
1341
|
for (const { handler, tag } of [...(this.subscriptions.get(forward) ?? [])]) this.call(handler, [tag], 0);
|
|
@@ -1098,7 +1360,18 @@ export class FakeServer {
|
|
|
1098
1360
|
fireHook(event: string, args: HookArg[] = [], options: { result?: Value } = {}): HookResult {
|
|
1099
1361
|
const shape = tables().hooks.get(event);
|
|
1100
1362
|
if (!shape) throw new Error(`no hookchain named "${event}" in as/hooks.ts`);
|
|
1363
|
+
const listeners = this.hookchains.get(shape.kind) ?? { pre: [], post: [] };
|
|
1364
|
+
return this.runChain(shape, listeners.pre, listeners.post, args, options.result, false);
|
|
1365
|
+
}
|
|
1101
1366
|
|
|
1367
|
+
/**
|
|
1368
|
+
* A hooked call, as the module's hook runs it (gamehooks.h): the pre
|
|
1369
|
+
* listeners - one that answers or blocks keeps the game's function from
|
|
1370
|
+
* running, one that breaks stops every listener after it - then the
|
|
1371
|
+
* game's function, then the post listeners. The listeners read the
|
|
1372
|
+
* arguments as cells and write them (chain_set); -1 is the answer.
|
|
1373
|
+
*/
|
|
1374
|
+
private runChain(shape: HookShape, pre: Slot[], post: Slot[], args: HookArg[], result: Value | undefined, ham: boolean): HookResult {
|
|
1102
1375
|
const cells: ArgValue[] = args.map((a, i) => Array.isArray(a)
|
|
1103
1376
|
? a.map(floatBits)
|
|
1104
1377
|
: typeof a === 'string'
|
|
@@ -1107,10 +1380,8 @@ export class FakeServer {
|
|
|
1107
1380
|
? (a ? 1 : 0)
|
|
1108
1381
|
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
1109
1382
|
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
const chain: Chain = { shape, answer: nothing, args: [...args] };
|
|
1113
|
-
const listeners = this.hookchains.get(shape.kind) ?? { pre: [], post: [] };
|
|
1383
|
+
const nothing: Value = shape.answer === 'text' ? '' : shape.answer === 'bool' && !ham ? false : 0;
|
|
1384
|
+
const chain: Chain = { shape, answer: nothing, args: [...args], cells, ham };
|
|
1114
1385
|
|
|
1115
1386
|
const previous = this.chain;
|
|
1116
1387
|
this.chain = chain;
|
|
@@ -1118,41 +1389,54 @@ export class FakeServer {
|
|
|
1118
1389
|
try {
|
|
1119
1390
|
let prevented = false;
|
|
1120
1391
|
let broken = false;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1392
|
+
const walk = (slots: Slot[], before: boolean) => {
|
|
1393
|
+
for (const slot of [...slots]) {
|
|
1394
|
+
if (slot.off) continue;
|
|
1395
|
+
const said = this.withCallArgs(cells, () => this.call(slot, handed, 0));
|
|
1396
|
+
if (said === OUTCOME_BREAK) {
|
|
1397
|
+
broken = true;
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (said !== 0 && before) prevented = true;
|
|
1128
1401
|
}
|
|
1129
|
-
|
|
1130
|
-
if (said === HC_BREAK) {
|
|
1131
|
-
prevented = broken = true;
|
|
1132
|
-
break;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
if (!prevented) chain.answer = options.result ?? nothing;
|
|
1137
|
-
|
|
1138
|
-
if (!broken) {
|
|
1139
|
-
// A vector is an array reapi copies back into the game's: what
|
|
1140
|
-
// a pre listener wrote in it is what the game and the post
|
|
1141
|
-
// listeners get.
|
|
1142
|
-
cells.forEach((cell, i) => {
|
|
1143
|
-
if (Array.isArray(cell)) chain.args[i] = cell.map(bitsFloat);
|
|
1144
|
-
});
|
|
1145
|
-
for (const slot of [...listeners.post]) {
|
|
1146
|
-
this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1402
|
+
};
|
|
1149
1403
|
|
|
1150
|
-
|
|
1404
|
+
walk(pre, true);
|
|
1405
|
+
if (!prevented && !broken && !chain.answered) chain.answer = result ?? nothing;
|
|
1406
|
+
// A vector is written where the game keeps it: what a pre listener
|
|
1407
|
+
// wrote in it is what the game and the post listeners get.
|
|
1408
|
+
cells.forEach((cell, i) => {
|
|
1409
|
+
if (Array.isArray(cell)) chain.args[i] = cell.map(bitsFloat);
|
|
1410
|
+
});
|
|
1411
|
+
if (!broken) walk(post, false);
|
|
1412
|
+
return { prevented: prevented || broken, result: chain.answer, args: chain.args };
|
|
1151
1413
|
} finally {
|
|
1152
1414
|
this.chain = previous;
|
|
1153
1415
|
}
|
|
1154
1416
|
}
|
|
1155
1417
|
|
|
1418
|
+
/** What a listener wrote through chain_set: argument `index`, or the answer at -1. @internal */
|
|
1419
|
+
chainSet(index: number, value: ArgValue): void {
|
|
1420
|
+
const chain = this.chain;
|
|
1421
|
+
if (!chain) throw new Error('chain_set outside a hooked call');
|
|
1422
|
+
if (index < 0) {
|
|
1423
|
+
const answer = chain.shape.answer;
|
|
1424
|
+
chain.answer = typeof value !== 'number' ? value as Value : answer === 'float' ? bitsFloat(value) : answer === 'bool' && !chain.ham ? value !== 0 : value;
|
|
1425
|
+
chain.answered = true;
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
chain.cells[index] = value;
|
|
1429
|
+
chain.args[index] = typeof value !== 'number' ? value as HookArg : chain.shape.floats.has(index) ? bitsFloat(value) : value;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** The answer of the hooked call as a cell, as arg(-1) reads it. @internal */
|
|
1433
|
+
chainAnswer(): ArgValue {
|
|
1434
|
+
const answer = this.chain?.answer ?? 0;
|
|
1435
|
+
if (typeof answer === 'boolean') return answer ? 1 : 0;
|
|
1436
|
+
if (typeof answer === 'number') return this.chain?.shape.answer === 'float' ? floatBits(answer) : answer | 0;
|
|
1437
|
+
return answer;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1156
1440
|
/**
|
|
1157
1441
|
* Sends a message to a client the way the game would, through the
|
|
1158
1442
|
* register_message callbacks of its name: they read and change its
|
|
@@ -1185,6 +1469,11 @@ export class FakeServer {
|
|
|
1185
1469
|
const handed = [id ?? 0, receiver ? 1 : 0, receiver, 0];
|
|
1186
1470
|
if (this.withCallArgs(handed, () => this.call(slot, handed, 0)) === PLUGIN_HANDLED) prevented = true;
|
|
1187
1471
|
}
|
|
1472
|
+
// The module's hooks: the receiver, the id, the destination; any answer blocks it.
|
|
1473
|
+
for (const slot of [...(id !== undefined ? this.moduleMessageHooks.get(id) ?? [] : [])]) {
|
|
1474
|
+
const handed = [receiver, id ?? 0, receiver ? MSG_ONE : MSG_ALL, 0];
|
|
1475
|
+
if (this.withCallArgs(handed, () => this.call(slot, handed, 0)) !== 0) prevented = true;
|
|
1476
|
+
}
|
|
1188
1477
|
// register_event's callbacks hear it once it is sent, when its conditions hold.
|
|
1189
1478
|
const holds = (condition: string) => {
|
|
1190
1479
|
const match = condition.match(/^(\d+)=(.*)$/);
|
|
@@ -1226,33 +1515,8 @@ export class FakeServer {
|
|
|
1226
1515
|
*/
|
|
1227
1516
|
runHam(shape: HookShape, id: number, args: HookArg[], result?: Value): HookResult {
|
|
1228
1517
|
const classname = this.entities.get(id)?.classname ?? (id >= 1 && id <= this.maxPlayers ? 'player' : '');
|
|
1229
|
-
const all = [id, ...args];
|
|
1230
|
-
const cells: ArgValue[] = all.map((a, i) => Array.isArray(a)
|
|
1231
|
-
? a.map(floatBits)
|
|
1232
|
-
: typeof a === 'string'
|
|
1233
|
-
? a
|
|
1234
|
-
: typeof a === 'boolean'
|
|
1235
|
-
? (a ? 1 : 0)
|
|
1236
|
-
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
1237
|
-
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
1238
|
-
const nothing: Value = shape.answer === constant('ATYPE_STRING') ? '' : 0;
|
|
1239
|
-
const chain: Chain = { shape, answer: nothing, args: all };
|
|
1240
1518
|
const listeners = (post: boolean) => this.hams.get(`${shape.ham}:${classname}:${post ? 'post' : 'pre'}`) ?? [];
|
|
1241
|
-
|
|
1242
|
-
const previous = this.chain;
|
|
1243
|
-
this.chain = chain;
|
|
1244
|
-
|
|
1245
|
-
try {
|
|
1246
|
-
let prevented = false;
|
|
1247
|
-
for (const slot of [...listeners(false)]) {
|
|
1248
|
-
if (this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback)) >= HAM_SUPERCEDE) prevented = true;
|
|
1249
|
-
}
|
|
1250
|
-
if (!prevented) chain.answer = result ?? nothing;
|
|
1251
|
-
for (const slot of [...listeners(true)]) this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
1252
|
-
return { prevented, result: chain.answer, args: chain.args };
|
|
1253
|
-
} finally {
|
|
1254
|
-
this.chain = previous;
|
|
1255
|
-
}
|
|
1519
|
+
return this.runChain(shape, listeners(false), listeners(true), [id, ...args], result, true);
|
|
1256
1520
|
}
|
|
1257
1521
|
|
|
1258
1522
|
/**
|
|
@@ -1529,7 +1793,8 @@ export class FakeServer {
|
|
|
1529
1793
|
this.outcomeSaid = false;
|
|
1530
1794
|
|
|
1531
1795
|
try {
|
|
1532
|
-
|
|
1796
|
+
const via = handler.via;
|
|
1797
|
+
this.within(handler.plugin, () => (via ? this.callVia(handler.plugin, via, args[0] ?? 0) : fn(...cells)));
|
|
1533
1798
|
return this.outcomeSaid ? this.outcome : fallback;
|
|
1534
1799
|
} finally {
|
|
1535
1800
|
this.outcomeSaid = said;
|
|
@@ -1537,6 +1802,26 @@ export class FakeServer {
|
|
|
1537
1802
|
}
|
|
1538
1803
|
}
|
|
1539
1804
|
|
|
1805
|
+
/** CallVia in module.cpp: the one listener, with what the plugin's own call of it sets first. */
|
|
1806
|
+
private callVia(plugin: PluginInstance, via: NonNullable<Handler['via']>, first: number): void {
|
|
1807
|
+
const globals = plugin.instance.exports;
|
|
1808
|
+
if (globals.__env) globals.__env.value = via.env;
|
|
1809
|
+
if (globals.__argumentsLength) globals.__argumentsLength.value = 1;
|
|
1810
|
+
const ambient = via.player ? globals.__co_ambient_player : undefined;
|
|
1811
|
+
const before = ambient?.value;
|
|
1812
|
+
if (ambient) ambient.value = first;
|
|
1813
|
+
try {
|
|
1814
|
+
plugin.table.get(via.fn)(via.arg);
|
|
1815
|
+
} finally {
|
|
1816
|
+
if (ambient) ambient.value = before;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/** Has `handler` call its plugin's one listener `fn` itself (Handler.via); 0 the handler again. */
|
|
1821
|
+
private direct(handler: Handler, fn: number, env: number, arg: number, player: boolean): void {
|
|
1822
|
+
handler.via = fn ? { fn, env, arg, player } : undefined;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1540
1825
|
/** Runs `body` as `plugin`: what its natives are called on behalf of. @internal */
|
|
1541
1826
|
within<T>(plugin: PluginInstance, body: () => T): T {
|
|
1542
1827
|
const previous = this.current;
|
|
@@ -1590,8 +1875,11 @@ export class FakeServer {
|
|
|
1590
1875
|
|
|
1591
1876
|
/** The handlers of the command in argv, in order, until one takes it. */
|
|
1592
1877
|
private handleCommand(handlers: Map<string, Slot[]>, id: number): boolean {
|
|
1878
|
+
const flags = this.players.find(each => each.id === id)?.flags ?? 0;
|
|
1593
1879
|
return [...(handlers.get(this.argv[0].toLowerCase()) ?? [])].some((slot) => {
|
|
1594
|
-
|
|
1880
|
+
if (slot.access && !(flags & slot.access)) return false;
|
|
1881
|
+
// The module's own handlers read the count of the command's words, its name counted, last.
|
|
1882
|
+
const args = [id, 0, slot.index, this.argv.length];
|
|
1595
1883
|
return this.withCallArgs(args, () => this.call(slot, args, slot.fallback)) >= PLUGIN_HANDLED;
|
|
1596
1884
|
});
|
|
1597
1885
|
}
|
|
@@ -1628,6 +1916,29 @@ export class FakeServer {
|
|
|
1628
1916
|
for (const owner of owners) this.request(owner.plugin!, plugin.run, gone);
|
|
1629
1917
|
}
|
|
1630
1918
|
|
|
1919
|
+
/**
|
|
1920
|
+
* `amxts_reload <plugin>`: the plugin hears `"end"` and goes, its file is
|
|
1921
|
+
* loaded again, and the new one hears the map's start - `"init"`,
|
|
1922
|
+
* `"pluginsLoaded"`, `"configsQueued"`, `"configsExecuted"` - as a
|
|
1923
|
+
* reload on a running map does.
|
|
1924
|
+
*/
|
|
1925
|
+
async reload(plugin: PluginInstance): Promise<PluginInstance> {
|
|
1926
|
+
this.fireTo(plugin, 'plugin_end');
|
|
1927
|
+
this.unload(plugin);
|
|
1928
|
+
const fresh = await this.load(plugin.source);
|
|
1929
|
+
for (const name of ['plugin_init', 'plugin_cfg', 'OnAutoConfigsBuffered', 'OnConfigsExecuted']) this.fireTo(fresh, name);
|
|
1930
|
+
return fresh;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/** A forward with no arguments to one plugin's handlers, as the module's RaiseTo. */
|
|
1934
|
+
private fireTo(plugin: PluginInstance, name: string): void {
|
|
1935
|
+
this.withCallArgs([], () => {
|
|
1936
|
+
for (const handler of [...(this.events.get(name) ?? [])]) {
|
|
1937
|
+
if (handler.plugin === plugin) this.call(handler, [0], 0);
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1631
1942
|
/** A menu by its id, or the error AMX Mod X gives for another number. @internal */
|
|
1632
1943
|
menu(id: number): FakeMenu {
|
|
1633
1944
|
const menu = this.menus.get(id);
|
|
@@ -1705,11 +2016,54 @@ export class FakeServer {
|
|
|
1705
2016
|
player.menu = { menu: menu.id, page: at, text: lines.join('\n'), keys, disabled };
|
|
1706
2017
|
}
|
|
1707
2018
|
|
|
2019
|
+
/**
|
|
2020
|
+
* show_menu: the text on the player's screen, or every player's for 0,
|
|
2021
|
+
* with the keys it takes; a menu of AMX Mod X's own he had open is closed
|
|
2022
|
+
* first, its handler hearing MENU_EXIT. @internal
|
|
2023
|
+
*/
|
|
2024
|
+
showMenu(id: number, keys: number, text: string): void {
|
|
2025
|
+
for (const player of this.players.filter(one => one.connected && (id === 0 || one.id === id))) {
|
|
2026
|
+
// The module's show_menu: a menu over a plugin's Menu takes its keys.
|
|
2027
|
+
this.shownMenus.delete(player.id);
|
|
2028
|
+
const before = player.menu;
|
|
2029
|
+
player.menu = null;
|
|
2030
|
+
const replaced = before ? this.menus.get(before.menu) : undefined;
|
|
2031
|
+
if (replaced) this.menuAnswer(replaced, player, constant('MENU_EXIT'));
|
|
2032
|
+
const pressable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0].filter(key => keys & (1 << (key === 0 ? 9 : key - 1)));
|
|
2033
|
+
player.menu = { menu: -1, page: 0, text, keys: pressable, disabled: [] };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* A key of the menu a plugin's Menu shows the player, taken off as the
|
|
2039
|
+
* module takes it: its handler and the key, 0 for 1 and 9 for 0 - or null
|
|
2040
|
+
* when he has none from a running plugin, or it does not take the key. @internal
|
|
2041
|
+
*/
|
|
2042
|
+
takeMenuKey(player: FakePlayer, key: number): { handler: Handler; index: number } | null {
|
|
2043
|
+
const shown = this.shownMenus.get(player.id);
|
|
2044
|
+
const index = key === 0 ? 9 : key - 1;
|
|
2045
|
+
if (!shown || shown.plugin.unloaded || index < 0 || index > 9 || !(shown.keys & (1 << index))) return null;
|
|
2046
|
+
this.shownMenus.delete(player.id);
|
|
2047
|
+
return { handler: shown, index };
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/** A key of a Menu's page to its plugin, as the module hands it on. @internal */
|
|
2051
|
+
pressMenu(player: FakePlayer, pressed: { handler: Handler; index: number }): boolean {
|
|
2052
|
+
const args = [player.id, pressed.index, 0, 0];
|
|
2053
|
+
this.withCallArgs(args, () => this.call(pressed.handler, args, 0));
|
|
2054
|
+
return true;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1708
2057
|
/** 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 */
|
|
1709
2058
|
selectMenu(player: FakePlayer, key: number): boolean {
|
|
1710
2059
|
const screen = player.menu;
|
|
1711
|
-
|
|
1712
|
-
|
|
2060
|
+
if (!screen || !screen.keys.includes(key)) return false;
|
|
2061
|
+
const menu = this.menus.get(screen.menu);
|
|
2062
|
+
// show_menu's: the screen goes, and the key passes on.
|
|
2063
|
+
if (!menu) {
|
|
2064
|
+
player.menu = null;
|
|
2065
|
+
return false;
|
|
2066
|
+
}
|
|
1713
2067
|
|
|
1714
2068
|
const paged = menu.perPage > 0;
|
|
1715
2069
|
const turn = paged && key === 8 ? -1 : paged && key === 9 ? 1 : 0;
|
|
@@ -1730,9 +2084,10 @@ export class FakeServer {
|
|
|
1730
2084
|
}
|
|
1731
2085
|
|
|
1732
2086
|
/**
|
|
1733
|
-
* A line of the game's log, as the engine hands it to
|
|
1734
|
-
* into its arguments as AMX Mod X splits
|
|
1735
|
-
*
|
|
2087
|
+
* A line of the game's log, as the engine hands it to the module: split
|
|
2088
|
+
* into its arguments as AMX Mod X splits them, and every log hook whose
|
|
2089
|
+
* filters it passes is called. A line logged while one runs is heard by
|
|
2090
|
+
* its own, and read_logargv reads the outer line again after it.
|
|
1736
2091
|
*
|
|
1737
2092
|
* ```ts
|
|
1738
2093
|
* server.gameLog('World triggered "Round_End"');
|
|
@@ -1740,19 +2095,25 @@ export class FakeServer {
|
|
|
1740
2095
|
* ```
|
|
1741
2096
|
*/
|
|
1742
2097
|
gameLog(line: string): void {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
this.logLine = line;
|
|
1746
|
-
this.logArgs = splitLog(line);
|
|
1747
|
-
const argc = this.logArgs.length;
|
|
2098
|
+
const [outerLine, outerArgs] = [this.logLine, this.logArgs];
|
|
2099
|
+
const args = splitLog(line);
|
|
1748
2100
|
const passes = (filter: string) => {
|
|
1749
2101
|
const match = filter.match(/^(\d+)([=&!])(.*)$/);
|
|
1750
2102
|
if (!match) return true;
|
|
1751
|
-
const value =
|
|
2103
|
+
const value = args[Number(match[1])] ?? '';
|
|
1752
2104
|
return match[2] === '=' ? value === match[3] : match[2] === '&' ? value.includes(match[3]) : value !== match[3];
|
|
1753
2105
|
};
|
|
1754
|
-
|
|
1755
|
-
|
|
2106
|
+
try {
|
|
2107
|
+
// The log event hears every line, as the module raises it.
|
|
2108
|
+
[this.logLine, this.logArgs] = [line, args];
|
|
2109
|
+
this.fire('plugin_log');
|
|
2110
|
+
for (const event of [...this.logEvents]) {
|
|
2111
|
+
if (event.argc !== args.length || !event.filters.every(passes)) continue;
|
|
2112
|
+
[this.logLine, this.logArgs] = [line, args];
|
|
2113
|
+
this.withCallArgs([], () => this.call(event.slot, [0, 0, 0, 0], 0));
|
|
2114
|
+
}
|
|
2115
|
+
} finally {
|
|
2116
|
+
[this.logLine, this.logArgs] = [outerLine, outerArgs];
|
|
1756
2117
|
}
|
|
1757
2118
|
}
|
|
1758
2119
|
|
|
@@ -1775,14 +2136,38 @@ export class FakeServer {
|
|
|
1775
2136
|
for (const slot of [...(this.fakemetaForwards.get(`${fn}:pre`) ?? [])]) {
|
|
1776
2137
|
if (this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback)) >= FMRES_SUPERCEDE) superseded = true;
|
|
1777
2138
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
2139
|
+
|
|
2140
|
+
// The module's own hooks of the function: any answer blocks it, and
|
|
2141
|
+
// chain_set writes an argument or (at -1) its answer.
|
|
2142
|
+
const stock = STOCK_FUNCTIONS.indexOf(name);
|
|
2143
|
+
const chain: Chain = { shape: { kind: name, floats: new Set(), texts: new Set(), answer: typeof result === 'string' ? 'text' : 'int' }, answer: result, args: [...args] as HookArg[], cells };
|
|
2144
|
+
const previous = this.chain;
|
|
2145
|
+
this.chain = chain;
|
|
2146
|
+
try {
|
|
2147
|
+
for (const slot of [...(this.stockHooks.get(`${stock}:pre`) ?? [])]) {
|
|
2148
|
+
if (this.withCallArgs(cells, () => this.call(slot, handed, 0)) !== 0) superseded = true;
|
|
2149
|
+
}
|
|
2150
|
+
if (chain.answered) this.forwardAnswer = chain.answer as number | string;
|
|
2151
|
+
if (!superseded) {
|
|
2152
|
+
this.origRetval = result;
|
|
2153
|
+
for (const slot of [...(this.fakemetaForwards.get(`${fn}:post`) ?? [])]) this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
2154
|
+
for (const slot of [...(this.stockHooks.get(`${stock}:post`) ?? [])]) this.withCallArgs(cells, () => this.call(slot, handed, 0));
|
|
2155
|
+
}
|
|
2156
|
+
} finally {
|
|
2157
|
+
this.chain = previous;
|
|
1781
2158
|
}
|
|
1782
2159
|
return { superseded, answer: this.forwardAnswer };
|
|
1783
2160
|
}
|
|
1784
2161
|
|
|
1785
|
-
/**
|
|
2162
|
+
/** A handler of one of the module's hooks, under `key` in `hooks`; its handle for hook_on. @internal */
|
|
2163
|
+
addModuleHook<K>(plugin: PluginInstance, fn: number, hooks: Map<K, Slot[]>, key: K): number {
|
|
2164
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, '', 0);
|
|
2165
|
+
hooks.set(key, [...(hooks.get(key) ?? []), slot]);
|
|
2166
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2167
|
+
return this.hookSlots.size;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/** Switches a hook off or on by its handle: the module's (hook_on), or a raw RegisterHookChain's or RegisterHam's. @internal */
|
|
1786
2171
|
switchHook(handle: number, on: boolean): number {
|
|
1787
2172
|
const slot = this.hookSlots.get(handle);
|
|
1788
2173
|
if (!slot) throw new Error(`the fake server knows no hook handle ${handle}`);
|
|
@@ -1817,6 +2202,26 @@ export class FakeServer {
|
|
|
1817
2202
|
return slot;
|
|
1818
2203
|
}
|
|
1819
2204
|
|
|
2205
|
+
/**
|
|
2206
|
+
* What the server sends `host` of `entity` (AddToFullPack): its listeners
|
|
2207
|
+
* run on the state - sixteen numbers, by the module's field numbers:
|
|
2208
|
+
* origin, angles, render mode, amount, colour, fx, effects, model index,
|
|
2209
|
+
* body, skin - and say what was sent, or that it was hidden.
|
|
2210
|
+
*/
|
|
2211
|
+
sendState(host: FakePlayer, entity: FakeEntity, state: number[] = Array.from({ length: 16 }).fill(0) as number[]): { hidden: boolean; state: number[] } {
|
|
2212
|
+
const sent = [...state];
|
|
2213
|
+
let hidden = false;
|
|
2214
|
+
this.sending = sent;
|
|
2215
|
+
try {
|
|
2216
|
+
for (const { slot } of this.stateHooks.filter(h => h.classname === '' || h.classname === entity.classname)) {
|
|
2217
|
+
if (this.withCallArgs([host.id, entity.id], () => this.call(slot, [host.id, entity.id], 0)) > 0) hidden = true;
|
|
2218
|
+
}
|
|
2219
|
+
} finally {
|
|
2220
|
+
this.sending = null;
|
|
2221
|
+
}
|
|
2222
|
+
return { hidden, state: sent };
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1820
2225
|
/**
|
|
1821
2226
|
* One entity moving into another: the register_touch callbacks whose
|
|
1822
2227
|
* classes match ("*" or "" is any), called as the engine module calls
|
|
@@ -1831,10 +2236,21 @@ export class FakeServer {
|
|
|
1831
2236
|
return answer;
|
|
1832
2237
|
}
|
|
1833
2238
|
|
|
1834
|
-
/**
|
|
2239
|
+
/**
|
|
2240
|
+
* A client answering query_client_cvar and the module's questions: every
|
|
2241
|
+
* question about that cvar it had. The module's go to its hooks of the
|
|
2242
|
+
* answer, (player, id, cvar, value), once.
|
|
2243
|
+
*/
|
|
1835
2244
|
answerCvar(player: FakePlayer, cvar: string, value: string): void {
|
|
1836
2245
|
for (const query of this.cvarQueries.filter(q => q.player === player.id && q.cvar === cvar)) {
|
|
1837
|
-
|
|
2246
|
+
if (query.slot) {
|
|
2247
|
+
const slot = query.slot;
|
|
2248
|
+
this.withCallArgs([player.id, cvar, value], () => this.call(slot, [player.id, 0, 0, 0], 0));
|
|
2249
|
+
continue;
|
|
2250
|
+
}
|
|
2251
|
+
this.cvarQueries.splice(this.cvarQueries.indexOf(query), 1);
|
|
2252
|
+
const args = [player.id, query.request ?? 0, cvar, value];
|
|
2253
|
+
for (const slot of [...(this.stockHooks.get(`${STOCK_CVAR_ANSWER}:pre`) ?? [])]) this.withCallArgs(args, () => this.call(slot, [player.id, query.request ?? 0, 0, 0], 0));
|
|
1838
2254
|
}
|
|
1839
2255
|
}
|
|
1840
2256
|
|
|
@@ -1987,6 +2403,36 @@ export class FakeServer {
|
|
|
1987
2403
|
net_read(this: FakeServer, plugin: PluginInstance, id: number, out: number, max: number) {
|
|
1988
2404
|
return this.network.read(plugin, id, out, max);
|
|
1989
2405
|
},
|
|
2406
|
+
|
|
2407
|
+
// A file's bytes, in files: as the module's fs_read and fs_write, a path that leaves the game folder is refused.
|
|
2408
|
+
fs_read(this: FakeServer, plugin: PluginInstance, name: number, out: number, max: number) {
|
|
2409
|
+
const path = gamePath(plugin.memory.string(name));
|
|
2410
|
+
const bytes = path === null ? undefined : this.files.get(path);
|
|
2411
|
+
if (!bytes) return -1;
|
|
2412
|
+
if (out && max > 0) plugin.memory.setRaw(out, bytes.subarray(0, max));
|
|
2413
|
+
return bytes.length;
|
|
2414
|
+
},
|
|
2415
|
+
fs_write(this: FakeServer, plugin: PluginInstance, name: number, data: number, length: number, append: number) {
|
|
2416
|
+
const path = gamePath(plugin.memory.string(name));
|
|
2417
|
+
if (path === null || !this.folderExists(path.includes('/') ? path.slice(0, path.lastIndexOf('/')) : '')) return 0;
|
|
2418
|
+
const bytes = new Uint8Array(plugin.instance.exports.memory.buffer, data, length);
|
|
2419
|
+
const before = append ? this.files.get(path) ?? new Uint8Array(0) : new Uint8Array(0);
|
|
2420
|
+
const file = new Uint8Array(before.length + length);
|
|
2421
|
+
file.set(before);
|
|
2422
|
+
file.set(bytes, before.length);
|
|
2423
|
+
this.files.set(path, file);
|
|
2424
|
+
return 1;
|
|
2425
|
+
},
|
|
2426
|
+
zip_inflate(this: FakeServer, plugin: PluginInstance, data: number, length: number, out: number, max: number) {
|
|
2427
|
+
try {
|
|
2428
|
+
const inflated = inflateRawSync(new Uint8Array(plugin.instance.exports.memory.buffer, data, length));
|
|
2429
|
+
if (inflated.length > max) return -1;
|
|
2430
|
+
plugin.memory.setRaw(out, inflated);
|
|
2431
|
+
return inflated.length;
|
|
2432
|
+
} catch {
|
|
2433
|
+
return -1;
|
|
2434
|
+
}
|
|
2435
|
+
},
|
|
1990
2436
|
net_reply(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1991
2437
|
return this.network.reply(plugin, id);
|
|
1992
2438
|
},
|
|
@@ -2073,6 +2519,13 @@ export class FakeServer {
|
|
|
2073
2519
|
list.push({ plugin, fn, shape });
|
|
2074
2520
|
},
|
|
2075
2521
|
|
|
2522
|
+
// The plugin's handler of a forward calls its one listener itself, or the handler again.
|
|
2523
|
+
on_direct(this: FakeServer, plugin: PluginInstance, name: number, fn: number, target: number, env: number, arg: number, player: number) {
|
|
2524
|
+
for (const handler of this.events.get(plugin.memory.string(name)) ?? []) {
|
|
2525
|
+
if (handler.plugin === plugin && handler.fn === fn) this.direct(handler, target, env, arg, player !== 0);
|
|
2526
|
+
}
|
|
2527
|
+
},
|
|
2528
|
+
|
|
2076
2529
|
// The plugin's handler of a forward taken off, once its event has no listener left.
|
|
2077
2530
|
off(this: FakeServer, plugin: PluginInstance, name: number, fn: number) {
|
|
2078
2531
|
const list = this.events.get(plugin.memory.string(name)) ?? [];
|
|
@@ -2096,8 +2549,7 @@ export class FakeServer {
|
|
|
2096
2549
|
list.push({ handler: { plugin, fn, shape: SHAPE_NARROW }, tag });
|
|
2097
2550
|
},
|
|
2098
2551
|
|
|
2099
|
-
//
|
|
2100
|
-
// TypeScript plugin emits reaches its subscribers this way: `s` a
|
|
2552
|
+
// A forward a TypeScript plugin emits reaches its subscribers this way: `s` a
|
|
2101
2553
|
// string, `a` an array as its count and then its cells, the rest cells.
|
|
2102
2554
|
emit_local(this: FakeServer, plugin: PluginInstance, name: number, mask: number, cells: number, argc: number) {
|
|
2103
2555
|
const forward = plugin.memory.string(name);
|
|
@@ -2123,35 +2575,318 @@ export class FakeServer {
|
|
|
2123
2575
|
return left.index | SLOT_REUSED;
|
|
2124
2576
|
},
|
|
2125
2577
|
|
|
2126
|
-
//
|
|
2127
|
-
|
|
2128
|
-
const slot = this.
|
|
2129
|
-
|
|
2578
|
+
// The module's own table of commands, as ClientCommand and the engine's server commands walk it.
|
|
2579
|
+
clcmd(this: FakeServer, plugin: PluginInstance, name: number, fn: number, flags: number, shape: number) {
|
|
2580
|
+
const slot = this.takeSlot(plugin, fn, shape, '', PLUGIN_HANDLED);
|
|
2581
|
+
slot.access = flags;
|
|
2582
|
+
addTo(this.clientCommands, plugin.memory.string(name).toLowerCase(), slot);
|
|
2130
2583
|
},
|
|
2131
2584
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2585
|
+
srvcmd(this: FakeServer, plugin: PluginInstance, name: number, fn: number, shape: number) {
|
|
2586
|
+
addTo(this.serverCommands, plugin.memory.string(name).toLowerCase(), this.takeSlot(plugin, fn, shape, '', PLUGIN_HANDLED));
|
|
2587
|
+
},
|
|
2588
|
+
|
|
2589
|
+
// The menu a plugin's Menu shows a player, said after show_menu; no keys for none, 0 for every player.
|
|
2590
|
+
menu_open(this: FakeServer, plugin: PluginInstance, id: number, keys: number, fn: number) {
|
|
2591
|
+
const tag = this.takeTag();
|
|
2592
|
+
if (id === 0) this.shownMenus.clear();
|
|
2593
|
+
else this.shownMenus.delete(id);
|
|
2594
|
+
if (id > 0 && keys & 0x3FF) this.shownMenus.set(id, { plugin, fn, shape: SHAPE_WIDE, tag, keys: keys & 0x3FF });
|
|
2595
|
+
},
|
|
2596
|
+
|
|
2597
|
+
// A client's command, as one he sent: a bot's line, the team menu's choice, a weapon's.
|
|
2598
|
+
bot_cmd(this: FakeServer, plugin: PluginInstance, id: number, line: number) {
|
|
2599
|
+
const target = this.players.find(each => each.id === id);
|
|
2600
|
+
const text = plugin.memory.string(line);
|
|
2601
|
+
if (target?.bot) target.command(text);
|
|
2602
|
+
else clientCommand(target, text);
|
|
2603
|
+
},
|
|
2604
|
+
|
|
2605
|
+
// What a player is given and does, as the module does it on ReGameDLL.
|
|
2606
|
+
// -1 for a name the game has no class of: the fake's game has its weapons and every item_ and ammo_ class.
|
|
2607
|
+
player_give(this: FakeServer, plugin: PluginInstance, id: number, name: number) {
|
|
2608
|
+
const item = plugin.memory.string(name);
|
|
2609
|
+
return WEAPON_NAMES.includes(item) || /^(?:item|ammo)_\w+$/.test(item) ? giveWeapon(this.player(id), item) : -1;
|
|
2610
|
+
},
|
|
2611
|
+
player_strip(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
2612
|
+
this.player(id)?.stripWeapons();
|
|
2613
|
+
},
|
|
2614
|
+
player_respawn(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
2615
|
+
NATIVES.rg_round_respawn({ server: this, plugin, memory: plugin.memory }, [id]);
|
|
2616
|
+
},
|
|
2617
|
+
player_speed() {},
|
|
2618
|
+
player_switch(this: FakeServer, plugin: PluginInstance, id: number, name: number) {
|
|
2619
|
+
return switchTo(this.player(id), plugin.memory.string(name));
|
|
2620
|
+
},
|
|
2621
|
+
player_join(this: FakeServer, plugin: PluginInstance, id: number, team: number) {
|
|
2622
|
+
if (!this.chains) return -1;
|
|
2623
|
+
return NATIVES.rg_join_team({ server: this, plugin, memory: plugin.memory }, [id, team]);
|
|
2624
|
+
},
|
|
2625
|
+
player_observe(this: FakeServer, plugin: PluginInstance, id: number, mode: number) {
|
|
2626
|
+
if (!this.chains) return -1;
|
|
2627
|
+
return NATIVES.rg_set_observer_mode({ server: this, plugin, memory: plugin.memory }, [id, mode]);
|
|
2628
|
+
},
|
|
2629
|
+
player_team(this: FakeServer, plugin: PluginInstance, id: number, team: number) {
|
|
2630
|
+
NATIVES.rg_set_user_team({ server: this, plugin, memory: plugin.memory }, [id, team]);
|
|
2631
|
+
},
|
|
2632
|
+
// The game rules' RestartRound (1) and CheckWinConditions (2), counted.
|
|
2633
|
+
game_rules_run(this: FakeServer, plugin: PluginInstance, action: number) {
|
|
2634
|
+
this.rulesRuns.push(action === 1 ? 'restartRound' : 'checkWinConditions');
|
|
2635
|
+
return 1;
|
|
2636
|
+
},
|
|
2637
|
+
// A slot's items taken with their ammo, as ReGameDLL's RemovePlayerItemEx takes each.
|
|
2638
|
+
player_remove_slot(this: FakeServer, plugin: PluginInstance, id: number, slot: number) {
|
|
2639
|
+
const target = this.player(id);
|
|
2640
|
+
if (!target) return 0;
|
|
2641
|
+
for (const weapon of target.items.filter(w => slotOf(w.kind) === slot)) {
|
|
2642
|
+
target.ammo.set(weapon.kind, 0);
|
|
2643
|
+
target.takeWeapon(weapon);
|
|
2644
|
+
this.entities.delete(weapon.id);
|
|
2645
|
+
}
|
|
2646
|
+
return 1;
|
|
2647
|
+
},
|
|
2648
|
+
// A weapon dropped: no longer his, left in the world.
|
|
2649
|
+
player_drop(this: FakeServer, plugin: PluginInstance, id: number, name: number) {
|
|
2650
|
+
const target = this.player(id);
|
|
2651
|
+
const weapon = target?.items.find(w => w.kind === plugin.memory.string(name));
|
|
2652
|
+
if (!target || !weapon || weapon.kind === 'weapon_knife') return;
|
|
2653
|
+
target.takeWeapon(weapon);
|
|
2654
|
+
weapon.set('var_owner', 0);
|
|
2655
|
+
weapon.set('m_pPlayer', 0);
|
|
2656
|
+
},
|
|
2657
|
+
// The user id (1), the ping (2), the seconds since he came (4).
|
|
2658
|
+
player_stat(this: FakeServer, plugin: PluginInstance, id: number, what: number) {
|
|
2659
|
+
const target = this.player(id);
|
|
2660
|
+
if (!target) return 0;
|
|
2661
|
+
return what === 1 ? target.userid : what === 4 ? Math.floor((this.time - target.connectedAt) / 1000) : 0;
|
|
2662
|
+
},
|
|
2663
|
+
// env(): a variable's UTF-8 length, as much of it as fits into `max`; -1 for none - the module's env_get.
|
|
2664
|
+
env_get(this: FakeServer, plugin: PluginInstance, name: number, out: number, max: number) {
|
|
2665
|
+
const value = this.variables().get(plugin.memory.string(name));
|
|
2666
|
+
return value === undefined ? -1 : plugin.memory.setUtf8(out, max, value);
|
|
2667
|
+
},
|
|
2668
|
+
info_get(this: FakeServer, plugin: PluginInstance, id: number, key: number, out: number, max: number) {
|
|
2669
|
+
return plugin.memory.setUtf8(out, max, this.player(id)?.info.get(plugin.memory.string(key)) ?? '');
|
|
2670
|
+
},
|
|
2671
|
+
info_set(this: FakeServer, plugin: PluginInstance, id: number, key: number, value: number) {
|
|
2672
|
+
this.player(id)?.info.set(plugin.memory.string(key), plugin.memory.string(value));
|
|
2673
|
+
},
|
|
2674
|
+
player_silent(this: FakeServer, plugin: PluginInstance, id: number, on: number) {
|
|
2675
|
+
const target = this.player(id);
|
|
2676
|
+
if (!target) return 0;
|
|
2677
|
+
if (on < 0) return target.silentSteps ? 1 : 0;
|
|
2678
|
+
target.silentSteps = on !== 0;
|
|
2679
|
+
return 1;
|
|
2680
|
+
},
|
|
2681
|
+
// An address's country (code 1, name 2) from `server.countries`; -1 for one it has not.
|
|
2682
|
+
geo_country(this: FakeServer, plugin: PluginInstance, ip: number, what: number, out: number, max: number) {
|
|
2683
|
+
const country = this.countries.get(plugin.memory.string(ip));
|
|
2684
|
+
return country ? plugin.memory.setUtf8(out, max, what === 1 ? country.code : country.name) : -1;
|
|
2685
|
+
},
|
|
2686
|
+
// The player to the other side, as the game swaps sides; a spectator stays.
|
|
2687
|
+
player_switch_team(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
2688
|
+
const target = this.player(id);
|
|
2689
|
+
if (target?.team === 'TERRORIST') target.team = 'CT';
|
|
2690
|
+
else if (target?.team === 'CT') target.team = 'TERRORIST';
|
|
2691
|
+
},
|
|
2692
|
+
// The plugins: the TypeScript ones loaded here, by their source's file; no Pawn plugin.
|
|
2693
|
+
plugins_count(this: FakeServer) {
|
|
2694
|
+
return this.plugins.length;
|
|
2695
|
+
},
|
|
2696
|
+
plugin_text(this: FakeServer, plugin: PluginInstance, index: number, what: number, out: number, max: number) {
|
|
2697
|
+
const target = this.plugins[index];
|
|
2698
|
+
const text = !target ? '' : what === 1 ? target.source.split(/[\\/]/).pop()! : what === 2 ? target.info.name : what === 3 ? target.info.version : what === 4 ? target.info.author : '';
|
|
2699
|
+
return plugin.memory.setUtf8(out, max, text);
|
|
2700
|
+
},
|
|
2701
|
+
plugin_state(this: FakeServer, plugin: PluginInstance, index: number) {
|
|
2702
|
+
const target = this.plugins[index];
|
|
2703
|
+
return !target ? -1 : target.unloaded ? 1 : 0;
|
|
2704
|
+
},
|
|
2705
|
+
// What was asked of a plugin, done at the next frame on the server: recorded here.
|
|
2706
|
+
plugin_action(this: FakeServer, plugin: PluginInstance, name: number, action: number) {
|
|
2707
|
+
this.pluginActions.push(`${['', 'stop', 'start', 'reload'][action]} ${plugin.memory.string(name)}`);
|
|
2708
|
+
},
|
|
2709
|
+
pawn_plugins_count() {
|
|
2710
|
+
return 0;
|
|
2711
|
+
},
|
|
2712
|
+
pawn_plugin_text(this: FakeServer, plugin: PluginInstance, index: number, what: number, out: number, max: number) {
|
|
2713
|
+
return plugin.memory.setUtf8(out, max, '');
|
|
2714
|
+
},
|
|
2715
|
+
pawn_plugin_pause() {
|
|
2716
|
+
return 0;
|
|
2717
|
+
},
|
|
2718
|
+
// A Storage: the map storage(name) gives, by a handle a name.
|
|
2719
|
+
store_open(this: FakeServer, plugin: PluginInstance, name: number) {
|
|
2720
|
+
const wanted = plugin.memory.string(name);
|
|
2721
|
+
if (!wanted || /[/\\:*?"<>|]/.test(wanted)) return -1;
|
|
2722
|
+
this.storage(wanted);
|
|
2723
|
+
const at = this.storeNames.indexOf(wanted);
|
|
2724
|
+
return at >= 0 ? at : this.storeNames.push(wanted) - 1;
|
|
2725
|
+
},
|
|
2726
|
+
store_get(this: FakeServer, plugin: PluginInstance, handle: number, key: number, out: number, max: number) {
|
|
2727
|
+
const value = this.storage(this.storeNames[handle]).get(plugin.memory.string(key));
|
|
2728
|
+
if (value === undefined) return -1;
|
|
2729
|
+
const length = Buffer.byteLength(value);
|
|
2730
|
+
return length > max ? length : plugin.memory.setUtf8(out, max, value);
|
|
2731
|
+
},
|
|
2732
|
+
store_set(this: FakeServer, plugin: PluginInstance, handle: number, key: number, value: number) {
|
|
2733
|
+
const name = this.storeNames[handle];
|
|
2734
|
+
this.storage(name).set(plugin.memory.string(key), plugin.memory.string(value));
|
|
2735
|
+
this.storeTimes.set(`${name}\0${plugin.memory.string(key)}`, (this.startedAt + this.time) / 1000);
|
|
2736
|
+
},
|
|
2737
|
+
store_delete(this: FakeServer, plugin: PluginInstance, handle: number, key: number) {
|
|
2738
|
+
return +this.storage(this.storeNames[handle]).delete(plugin.memory.string(key));
|
|
2739
|
+
},
|
|
2740
|
+
store_count(this: FakeServer, plugin: PluginInstance, handle: number) {
|
|
2741
|
+
return this.storage(this.storeNames[handle]).size;
|
|
2742
|
+
},
|
|
2743
|
+
store_keys(this: FakeServer, plugin: PluginInstance, handle: number, out: number, max: number) {
|
|
2744
|
+
const keys = [...this.storage(this.storeNames[handle]).keys()].sort().map(key => `${key}\0`).join('');
|
|
2745
|
+
const length = Buffer.byteLength(keys);
|
|
2746
|
+
if (length > max || length === 0) return length;
|
|
2747
|
+
plugin.memory.setUtf8(out, max, keys);
|
|
2748
|
+
return length;
|
|
2749
|
+
},
|
|
2750
|
+
// What was set before `before` (seconds since 1970, on the fake clock) goes; a value a test put in has no time and stays.
|
|
2751
|
+
store_prune(this: FakeServer, plugin: PluginInstance, handle: number, before: number) {
|
|
2752
|
+
const name = this.storeNames[handle];
|
|
2753
|
+
let removed = 0;
|
|
2754
|
+
for (const key of [...this.storage(name).keys()]) {
|
|
2755
|
+
const time = this.storeTimes.get(`${name}\0${key}`);
|
|
2756
|
+
if (time !== undefined && time < before) {
|
|
2757
|
+
this.storage(name).delete(key);
|
|
2758
|
+
removed++;
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return removed;
|
|
2762
|
+
},
|
|
2763
|
+
// The server's game and versions: ReHLDS's and ReGameDLL's APIs where the hookchains are.
|
|
2764
|
+
server_text(this: FakeServer, plugin: PluginInstance, what: number, out: number, max: number) {
|
|
2765
|
+
const text = ['', 'cstrike', '0.3.0', this.chains ? '3.14' : '', this.chains ? '5.28' : ''][what] ?? '';
|
|
2766
|
+
return plugin.memory.setUtf8(out, max, text);
|
|
2767
|
+
},
|
|
2768
|
+
map_valid(this: FakeServer, plugin: PluginInstance, name: number) {
|
|
2769
|
+
return +this.maps.includes(plugin.memory.string(name));
|
|
2770
|
+
},
|
|
2771
|
+
change_level(this: FakeServer, plugin: PluginInstance, name: number) {
|
|
2772
|
+
const map = plugin.memory.string(name);
|
|
2773
|
+
if (!this.maps.includes(map)) return 0;
|
|
2774
|
+
this.engineCalls.push(`ChangeLevel ${map}`);
|
|
2775
|
+
return 1;
|
|
2776
|
+
},
|
|
2777
|
+
playback_block(this: FakeServer) {
|
|
2778
|
+
this.playback.blocked = true;
|
|
2779
|
+
},
|
|
2780
|
+
playback_to(this: FakeServer, plugin: PluginInstance, ids: number, count: number) {
|
|
2781
|
+
this.playback.recipients = Array.from({ length: count }, (_, i) => plugin.memory.cell(ids + i * 4));
|
|
2782
|
+
},
|
|
2783
|
+
light_style(this: FakeServer, plugin: PluginInstance, text: number) {
|
|
2784
|
+
this.lightStyle = plugin.memory.string(text) || 'm';
|
|
2785
|
+
this.engineCalls.push(`LightStyle 0 ${this.lightStyle}`);
|
|
2786
|
+
},
|
|
2787
|
+
// The engine's lightstyle message to one player, as a message he was sent: "" is the server's light.
|
|
2788
|
+
player_light_style(this: FakeServer, plugin: PluginInstance, id: number, text: number) {
|
|
2789
|
+
const player = this.player(id);
|
|
2790
|
+
if (!player || player.bot) return;
|
|
2791
|
+
this.userMessages.push({ name: 'LightStyle', player: id, args: [0, plugin.memory.string(text) || this.lightStyle] });
|
|
2792
|
+
},
|
|
2793
|
+
// HUD messages: the text to each player, as his messages' "hud" line; how it was set, in server.hud.
|
|
2794
|
+
hud_line(this: FakeServer) {
|
|
2795
|
+
return ++this.hudLines;
|
|
2796
|
+
},
|
|
2797
|
+
hud_show(this: FakeServer, plugin: PluginInstance, id: number, line: number, params: number, text: number) {
|
|
2798
|
+
const p = Array.from({ length: 12 }, (_, i) => plugin.memory.number(params + i * 8));
|
|
2799
|
+
this.hud = { color: [p[3], p[4], p[5]], x: p[0], y: p[1], hold: p[7], channel: p[10], line, large: p[11] !== 0 };
|
|
2800
|
+
const message = plugin.memory.string(text);
|
|
2801
|
+
for (const target of id > 0 ? [this.player(id)] : this.players) {
|
|
2802
|
+
if (target && !target.bot) target.show('hud', message);
|
|
2803
|
+
}
|
|
2804
|
+
},
|
|
2805
|
+
hud_clear(this: FakeServer, plugin: PluginInstance, id: number, line: number) {
|
|
2806
|
+
this.engineCalls.push(`HudClear ${id} ${line}`);
|
|
2807
|
+
},
|
|
2808
|
+
// The model a plugin put on each player, by his id: written into his userinfo as the module does.
|
|
2809
|
+
player_model(this: FakeServer, plugin: PluginInstance, id: number, model: number, index: number) {
|
|
2810
|
+
const target = this.player(id);
|
|
2811
|
+
if (!target) return;
|
|
2812
|
+
const name = plugin.memory.string(model);
|
|
2813
|
+
if (name) this.models.set(id, name);
|
|
2814
|
+
else this.models.delete(id);
|
|
2815
|
+
target.info.set('model', name || 'urban');
|
|
2816
|
+
this.engineCalls.push(`Model ${id} ${name || '-'} ${index}`);
|
|
2817
|
+
},
|
|
2818
|
+
player_model_get(this: FakeServer, plugin: PluginInstance, id: number, out: number, max: number) {
|
|
2819
|
+
return plugin.memory.setUtf8(out, max, this.models.get(id) ?? '');
|
|
2820
|
+
},
|
|
2821
|
+
// What each player sees through, by his id: 0 his own eyes.
|
|
2822
|
+
player_view(this: FakeServer, plugin: PluginInstance, id: number, target: number) {
|
|
2823
|
+
this.engineCalls.push(`SetView ${id} ${target || id}`);
|
|
2824
|
+
this.views.set(id, target === id ? 0 : target);
|
|
2825
|
+
},
|
|
2826
|
+
player_view_get(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
2827
|
+
return this.views.get(id) ?? 0;
|
|
2828
|
+
},
|
|
2829
|
+
// Traces: the fake world is empty, unless a test puts a wall (server.walls)
|
|
2830
|
+
// across the way; each trace is written to engineCalls.
|
|
2831
|
+
world_trace_line(this: FakeServer, plugin: PluginInstance, points: number, flags: number, ignore: number, out: number) {
|
|
2832
|
+
const [start, end] = tracePoints(plugin, points);
|
|
2833
|
+
this.engineCalls.push(`TraceLine ${start.join(',')} ${end.join(',')} ${flags} ${ignore}`);
|
|
2834
|
+
this.writeTrace(plugin, out, start, end);
|
|
2835
|
+
},
|
|
2836
|
+
world_trace_hull(this: FakeServer, plugin: PluginInstance, points: number, hull: number, flags: number, ignore: number, out: number) {
|
|
2837
|
+
const [start, end] = tracePoints(plugin, points);
|
|
2838
|
+
this.engineCalls.push(`TraceHull ${start.join(',')} ${end.join(',')} ${hull} ${flags} ${ignore}`);
|
|
2839
|
+
this.writeTrace(plugin, out, start, end);
|
|
2840
|
+
},
|
|
2841
|
+
// Inside a wall is solid; the walls are planes across x.
|
|
2842
|
+
world_contents(this: FakeServer, plugin: PluginInstance, point: number) {
|
|
2843
|
+
const x = tracePoints(plugin, point)[0][0];
|
|
2844
|
+
return this.walls.some(wall => wall.x === x) ? -2 : -1;
|
|
2845
|
+
},
|
|
2846
|
+
entity_drop(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
2847
|
+
this.engineCalls.push(`DropToFloor ${id}`);
|
|
2848
|
+
return 1;
|
|
2849
|
+
},
|
|
2850
|
+
// The log line being told (-1) or one of its arguments, and how many it has.
|
|
2851
|
+
log_text(this: FakeServer, plugin: PluginInstance, index: number, out: number, max: number) {
|
|
2852
|
+
return plugin.memory.setUtf8(out, max, index < 0 ? this.logLine : this.logArgs[index] ?? '');
|
|
2853
|
+
},
|
|
2854
|
+
log_count(this: FakeServer) {
|
|
2855
|
+
return this.logArgs.length;
|
|
2856
|
+
},
|
|
2857
|
+
// Reunion's answers: -1 without it.
|
|
2858
|
+
reunion(this: FakeServer, plugin: PluginInstance, what: number, id: number) {
|
|
2859
|
+
const target = this.players.find(each => each.id === id);
|
|
2860
|
+
if (!this.reunion || !this.chains || !target) return -1;
|
|
2861
|
+
return what === 1 ? target.protocol : Math.max(0, AUTH_TYPES.indexOf(target.authType));
|
|
2862
|
+
},
|
|
2863
|
+
reunion_key(this: FakeServer, plugin: PluginInstance, id: number, out: number, max: number) {
|
|
2864
|
+
const target = this.players.find(each => each.id === id);
|
|
2865
|
+
return plugin.memory.setUtf8(out, max, this.reunion && this.chains ? target?.authKey ?? '' : '');
|
|
2866
|
+
},
|
|
2867
|
+
|
|
2868
|
+
// engfunc(EngFunc_RunPlayerMove, ...) made by the module itself, the angles and the speeds Floats' bits; 0 for no player there, which engfunc then takes.
|
|
2869
|
+
run_player_move(this: FakeServer, plugin: PluginInstance, id: number, pitch: number, yaw: number, roll: number, forward: number, side: number, up: number, buttons: number, impulse: number, msec: number) {
|
|
2870
|
+
if (!this.players.some(each => each.id === id)) return 0;
|
|
2871
|
+
this.runPlayerMove(id, [bitsFloat(pitch), bitsFloat(yaw), bitsFloat(roll)], bitsFloat(forward), bitsFloat(side), bitsFloat(up), buttons, impulse, msec);
|
|
2872
|
+
return 1;
|
|
2139
2873
|
},
|
|
2140
2874
|
|
|
2141
|
-
task(this: FakeServer, plugin: PluginInstance, secondsBits: number, fn: number,
|
|
2875
|
+
task(this: FakeServer, plugin: PluginInstance, secondsBits: number, fn: number, repeat: number) {
|
|
2142
2876
|
const slot = this.takeSlot(plugin, fn, 0, '', 0);
|
|
2143
2877
|
const interval = Math.round(bitsFloat(secondsBits) * 1000);
|
|
2144
|
-
|
|
2145
|
-
|
|
2878
|
+
let timer = 0;
|
|
2879
|
+
while (this.tasks.some(task => task.timer === timer)) timer++;
|
|
2880
|
+
this.tasks.push({ slot, timer, interval, due: this.time + interval, repeat: repeat !== 0, order: this.taskOrder++ });
|
|
2881
|
+
return timer;
|
|
2146
2882
|
},
|
|
2147
2883
|
|
|
2148
|
-
//
|
|
2149
|
-
stop_task(this: FakeServer, plugin: PluginInstance,
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
return before - this.tasks.length;
|
|
2884
|
+
// The calling plugin's timer in that slot.
|
|
2885
|
+
stop_task(this: FakeServer, plugin: PluginInstance, timer: number) {
|
|
2886
|
+
const at = this.tasks.findIndex(task => task.timer === timer && task.slot.plugin === plugin);
|
|
2887
|
+
if (at < 0) return 0;
|
|
2888
|
+
this.tasks.splice(at, 1);
|
|
2889
|
+
return 1;
|
|
2155
2890
|
},
|
|
2156
2891
|
|
|
2157
2892
|
hook(this: FakeServer, plugin: PluginInstance, id: number, fn: number, post: number) {
|
|
@@ -2166,7 +2901,7 @@ export class FakeServer {
|
|
|
2166
2901
|
return this.hookSlots.size;
|
|
2167
2902
|
},
|
|
2168
2903
|
|
|
2169
|
-
//
|
|
2904
|
+
// A Ham Sandwich function hooked on a class: kept by the function's id and the class, for runHam().
|
|
2170
2905
|
ham(this: FakeServer, plugin: PluginInstance, id: number, entityClass: number, fn: number, post: number) {
|
|
2171
2906
|
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, `ham:${id}:${post}`, 0);
|
|
2172
2907
|
const key = `${id}:${plugin.memory.string(entityClass)}:${post ? 'post' : 'pre'}`;
|
|
@@ -2177,13 +2912,158 @@ export class FakeServer {
|
|
|
2177
2912
|
return this.hookSlots.size;
|
|
2178
2913
|
},
|
|
2179
2914
|
|
|
2180
|
-
|
|
2181
|
-
this.
|
|
2915
|
+
hook_on(this: FakeServer, _plugin: PluginInstance, handle: number, on: number) {
|
|
2916
|
+
this.switchHook(handle, on !== 0);
|
|
2917
|
+
},
|
|
2918
|
+
|
|
2919
|
+
// A hook's handler calls its plugin's one listener itself, or the handler again.
|
|
2920
|
+
hook_direct(this: FakeServer, _plugin: PluginInstance, handle: number, target: number, env: number, arg: number) {
|
|
2921
|
+
const slot = this.hookSlots.get(handle);
|
|
2922
|
+
if (slot) this.direct(slot, target, env, arg, false);
|
|
2923
|
+
},
|
|
2924
|
+
|
|
2925
|
+
// The module's hooks of the engine's and the game's functions
|
|
2926
|
+
// (runtime/src/enginehooks.h), kept with the natives' callbacks of the
|
|
2927
|
+
// same things: sendMessage, gameLog, changeCvar, touch, fireForward
|
|
2928
|
+
// and answerCvar call both.
|
|
2929
|
+
msg_hook(this: FakeServer, plugin: PluginInstance, id: number, fn: number) {
|
|
2930
|
+
return this.addModuleHook(plugin, fn, this.moduleMessageHooks, id);
|
|
2931
|
+
},
|
|
2932
|
+
|
|
2933
|
+
msg_argc(this: FakeServer, _plugin: PluginInstance) {
|
|
2934
|
+
return this.hookedMessage?.args.length ?? 0;
|
|
2935
|
+
},
|
|
2936
|
+
|
|
2937
|
+
msg_arg_type(this: FakeServer, _plugin: PluginInstance, index: number) {
|
|
2938
|
+
return this.hookedMessage?.types[index] ?? 0;
|
|
2939
|
+
},
|
|
2940
|
+
|
|
2941
|
+
msg_number(this: FakeServer, _plugin: PluginInstance, index: number) {
|
|
2942
|
+
const message = this.hookedMessage;
|
|
2943
|
+
const value = Number(message?.args[index] ?? 0) || 0;
|
|
2944
|
+
const type = message?.types[index];
|
|
2945
|
+
return type === ARG_COORD || type === ARG_ANGLE ? value : Math.trunc(value);
|
|
2946
|
+
},
|
|
2947
|
+
|
|
2948
|
+
msg_text(this: FakeServer, plugin: PluginInstance, index: number, out: number, max: number) {
|
|
2949
|
+
const value = this.hookedMessage?.args[index];
|
|
2950
|
+
return plugin.memory.setBytes(out, max, typeof value === 'string' ? value : '');
|
|
2951
|
+
},
|
|
2952
|
+
|
|
2953
|
+
msg_set_number(this: FakeServer, _plugin: PluginInstance, index: number, value: number) {
|
|
2954
|
+
const message = this.hookedMessage;
|
|
2955
|
+
const type = message?.types[index];
|
|
2956
|
+
if (!message || type === undefined || type === ARG_STRING) return;
|
|
2957
|
+
message.args[index] = type === ARG_COORD || type === ARG_ANGLE ? value : Math.trunc(value);
|
|
2958
|
+
},
|
|
2959
|
+
|
|
2960
|
+
msg_set_text(this: FakeServer, plugin: PluginInstance, index: number, text: number) {
|
|
2961
|
+
const message = this.hookedMessage;
|
|
2962
|
+
if (message?.types[index] === ARG_STRING) message.args[index] = plugin.memory.string(text);
|
|
2963
|
+
},
|
|
2964
|
+
|
|
2965
|
+
log_hook(this: FakeServer, plugin: PluginInstance, argc: number, filter: number, fn: number) {
|
|
2966
|
+
const text = plugin.memory.string(filter);
|
|
2967
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, '', 0);
|
|
2968
|
+
this.logEvents.push({ argc, filters: text ? [text] : [], slot });
|
|
2969
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2970
|
+
return this.hookSlots.size;
|
|
2971
|
+
},
|
|
2972
|
+
|
|
2973
|
+
cvar_hook(this: FakeServer, plugin: PluginInstance, name: number, fn: number) {
|
|
2974
|
+
const cvar = this.cvars.get(plugin.memory.string(name).toLowerCase());
|
|
2975
|
+
if (!cvar) return 0;
|
|
2976
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, '', 0);
|
|
2977
|
+
cvar.hooks.push(slot);
|
|
2978
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2979
|
+
return this.hookSlots.size;
|
|
2980
|
+
},
|
|
2981
|
+
|
|
2982
|
+
// Every cvar's change is heard as it is made here.
|
|
2983
|
+
cvar_exact() {
|
|
2984
|
+
return 1;
|
|
2985
|
+
},
|
|
2986
|
+
|
|
2987
|
+
state_hook(this: FakeServer, plugin: PluginInstance, classname: number, fn: number) {
|
|
2988
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, '', 0);
|
|
2989
|
+
this.stateHooks.push({ classname: plugin.memory.string(classname), slot });
|
|
2990
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
2991
|
+
return this.hookSlots.size;
|
|
2992
|
+
},
|
|
2993
|
+
// A movement event's touchCount (0) and frameTime (1), from server.playerMove.
|
|
2994
|
+
move_get(this: FakeServer, plugin: PluginInstance, field: number) {
|
|
2995
|
+
return field === 0 ? this.playerMove.touchCount : field === 1 ? this.playerMove.frameTime : 0;
|
|
2996
|
+
},
|
|
2997
|
+
state_get(this: FakeServer, plugin: PluginInstance, field: number) {
|
|
2998
|
+
return this.sending?.[field] ?? 0;
|
|
2999
|
+
},
|
|
3000
|
+
state_set(this: FakeServer, plugin: PluginInstance, field: number, value: number) {
|
|
3001
|
+
if (this.sending) this.sending[field] = value;
|
|
3002
|
+
},
|
|
3003
|
+
|
|
3004
|
+
touch_hook(this: FakeServer, plugin: PluginInstance, touched: number, toucher: number, fn: number) {
|
|
3005
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, '', 0);
|
|
3006
|
+
this.touches.push({ touched: plugin.memory.string(touched), toucher: plugin.memory.string(toucher), slot });
|
|
3007
|
+
this.hookSlots.set(this.hookSlots.size + 1, slot);
|
|
3008
|
+
return this.hookSlots.size;
|
|
3009
|
+
},
|
|
3010
|
+
|
|
3011
|
+
stock_hook(this: FakeServer, plugin: PluginInstance, fn: number, handler: number, post: number) {
|
|
3012
|
+
return this.addModuleHook(plugin, handler, this.stockHooks, `${fn}:${post ? 'post' : 'pre'}`);
|
|
3013
|
+
},
|
|
3014
|
+
|
|
3015
|
+
// A question to the client: answerCvar() answers it by its id.
|
|
3016
|
+
query_cvar(this: FakeServer, plugin: PluginInstance, id: number, name: number) {
|
|
3017
|
+
if (!this.players.some(each => each.id === id)) return 0;
|
|
3018
|
+
const request = ++this.cvarRequests;
|
|
3019
|
+
this.cvarQueries.push({ player: id, cvar: plugin.memory.string(name).toLowerCase(), request });
|
|
3020
|
+
return request;
|
|
2182
3021
|
},
|
|
2183
3022
|
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
3023
|
+
chain_set(this: FakeServer, _plugin: PluginInstance, index: number, value: number) {
|
|
3024
|
+
this.chainSet(index, value);
|
|
3025
|
+
},
|
|
3026
|
+
|
|
3027
|
+
chain_set_text(this: FakeServer, plugin: PluginInstance, index: number, text: number) {
|
|
3028
|
+
this.chainSet(index, plugin.memory.string(text));
|
|
3029
|
+
},
|
|
3030
|
+
|
|
3031
|
+
game_api(this: FakeServer, _plugin: PluginInstance) {
|
|
3032
|
+
return this.chains ? 3 : 0;
|
|
3033
|
+
},
|
|
3034
|
+
|
|
3035
|
+
// One phase of a chain's listeners for game.endRound's dispatch, as module.cpp's chain_dispatch.
|
|
3036
|
+
chain_dispatch(this: FakeServer, plugin: PluginInstance, id: number, post: number, at: number, count: number, result: number) {
|
|
3037
|
+
const kind = tables().hookNames.get(id);
|
|
3038
|
+
const shape = kind === undefined ? undefined : tables().hooks.get(kind);
|
|
3039
|
+
if (!kind || !shape) return 0;
|
|
3040
|
+
const cells: ArgValue[] = Array.from({ length: count }, (_, i) => plugin.memory.cell(at + i * 4));
|
|
3041
|
+
const args: HookArg[] = cells.map((cell, i) => shape.floats.has(i) ? bitsFloat(cell as number) : cell as number);
|
|
3042
|
+
const previous = this.chain;
|
|
3043
|
+
this.chain = { shape, answer: result !== 0, args, cells };
|
|
3044
|
+
let said = 0;
|
|
3045
|
+
try {
|
|
3046
|
+
for (const slot of [...(this.hookchains.get(kind)?.[post ? 'post' : 'pre'] ?? [])]) {
|
|
3047
|
+
if (slot.off) continue;
|
|
3048
|
+
const outcome = this.withCallArgs(cells, () => this.call(slot, cells.slice(0, 4).map(Number), 0));
|
|
3049
|
+
if (outcome === OUTCOME_BREAK) {
|
|
3050
|
+
said |= 2;
|
|
3051
|
+
break;
|
|
3052
|
+
}
|
|
3053
|
+
if (outcome !== 0 && !post) said |= 1;
|
|
3054
|
+
}
|
|
3055
|
+
} finally {
|
|
3056
|
+
this.chain = previous;
|
|
3057
|
+
}
|
|
3058
|
+
cells.forEach((cell, i) => plugin.memory.setCell(at + i * 4, Number(cell)));
|
|
3059
|
+
return said;
|
|
3060
|
+
},
|
|
3061
|
+
|
|
3062
|
+
// ExecuteHam, which ham_bypass comes before, runs no listener here anyway.
|
|
3063
|
+
ham_bypass(this: FakeServer, _plugin: PluginInstance, _fn: number, _id: number) {},
|
|
3064
|
+
|
|
3065
|
+
tag(this: FakeServer, _plugin: PluginInstance, tag: number) {
|
|
3066
|
+
this.pendingTag = tag;
|
|
2187
3067
|
},
|
|
2188
3068
|
|
|
2189
3069
|
export(this: FakeServer, plugin: PluginInstance, name: number, fn: number) {
|
|
@@ -2197,12 +3077,12 @@ export class FakeServer {
|
|
|
2197
3077
|
},
|
|
2198
3078
|
|
|
2199
3079
|
arg(this: FakeServer, plugin: PluginInstance, index: number) {
|
|
2200
|
-
const value = this.callArgs?.[index];
|
|
3080
|
+
const value = index === -1 && this.chain ? this.chainAnswer() : this.callArgs?.[index];
|
|
2201
3081
|
return typeof value === 'number' ? value | 0 : typeof value === 'boolean' ? +value : 0;
|
|
2202
3082
|
},
|
|
2203
3083
|
|
|
2204
3084
|
arg_text(this: FakeServer, plugin: PluginInstance, index: number, out: number, max: number) {
|
|
2205
|
-
const value = this.callArgs?.[index];
|
|
3085
|
+
const value = index === -1 && this.chain ? this.chainAnswer() : this.callArgs?.[index];
|
|
2206
3086
|
return plugin.memory.setBytes(out, max, typeof value === 'string' ? value : '');
|
|
2207
3087
|
},
|
|
2208
3088
|
|
|
@@ -2220,7 +3100,7 @@ export class FakeServer {
|
|
|
2220
3100
|
},
|
|
2221
3101
|
|
|
2222
3102
|
arg_array(this: FakeServer, plugin: PluginInstance, index: number, out: number, count: number) {
|
|
2223
|
-
const given = this.callArgs?.[index];
|
|
3103
|
+
const given = index === -1 && this.chain ? [0, 0, 0] : this.callArgs?.[index];
|
|
2224
3104
|
const value = given instanceof Pointer
|
|
2225
3105
|
? Array.from({ length: count }, (_, i) => given.plugin.memory.cell(given.at + i * 4))
|
|
2226
3106
|
: given;
|
|
@@ -2230,6 +3110,10 @@ export class FakeServer {
|
|
|
2230
3110
|
},
|
|
2231
3111
|
|
|
2232
3112
|
set_arg_array(this: FakeServer, plugin: PluginInstance, index: number, cells: number, count: number) {
|
|
3113
|
+
if (index === -1 && this.chain) {
|
|
3114
|
+
this.chainSet(-1, 0);
|
|
3115
|
+
return count;
|
|
3116
|
+
}
|
|
2233
3117
|
const value = this.callArgs?.[index];
|
|
2234
3118
|
if (!Array.isArray(value) || count < 0) return 0;
|
|
2235
3119
|
for (let i = 0; i < count; i++) value[i] = plugin.memory.cell(cells + i * 4);
|
|
@@ -2268,6 +3152,44 @@ export class FakeServer {
|
|
|
2268
3152
|
if (at) setFieldCell(this.entities.get(id), at.field, cell, at.component);
|
|
2269
3153
|
},
|
|
2270
3154
|
|
|
3155
|
+
// A vector entvar into the plugin's three numbers at `out`.
|
|
3156
|
+
ent_vector(this: FakeServer, plugin: PluginInstance, id: number, offset: number, out: number) {
|
|
3157
|
+
for (let i = 0; i < 3; i++) {
|
|
3158
|
+
const at = tables().entvarAt.get(offset + i * 4);
|
|
3159
|
+
plugin.memory.setNumber(out + i * 8, at ? bitsFloat(fieldCell(this.entities.get(id), at.field, at.component)) : 0);
|
|
3160
|
+
}
|
|
3161
|
+
},
|
|
3162
|
+
|
|
3163
|
+
// What a field's setter sends with the field: emessage_* to one player in the game.
|
|
3164
|
+
send_one(this: FakeServer, plugin: PluginInstance, player: number, type: number) {
|
|
3165
|
+
if (this.callNativeImpl(plugin, 'is_user_connected', [player]) === 0) return 0;
|
|
3166
|
+
return this.callNativeImpl(plugin, 'emessage_begin', [MSG_ONE, type, 0, player]);
|
|
3167
|
+
},
|
|
3168
|
+
|
|
3169
|
+
send_byte(this: FakeServer, plugin: PluginInstance, value: number) {
|
|
3170
|
+
this.writing?.args.push(value);
|
|
3171
|
+
},
|
|
3172
|
+
|
|
3173
|
+
send_long(this: FakeServer, plugin: PluginInstance, value: number) {
|
|
3174
|
+
this.writing?.args.push(value);
|
|
3175
|
+
},
|
|
3176
|
+
|
|
3177
|
+
send_string(this: FakeServer, plugin: PluginInstance, text: number) {
|
|
3178
|
+
this.writing?.args.push(plugin.memory.string(text));
|
|
3179
|
+
},
|
|
3180
|
+
|
|
3181
|
+
send_end(this: FakeServer, plugin: PluginInstance) {
|
|
3182
|
+
this.callNativeImpl(plugin, 'emessage_end', []);
|
|
3183
|
+
},
|
|
3184
|
+
|
|
3185
|
+
// A message of one or two numbers in one call: each written as its two bits of `kinds` say, 0 for none.
|
|
3186
|
+
send_cells(this: FakeServer, plugin: PluginInstance, player: number, type: number, first: number, second: number, kinds: number) {
|
|
3187
|
+
if (this.callNativeImpl(plugin, 'is_user_connected', [player]) === 0 || this.callNativeImpl(plugin, 'emessage_begin', [MSG_ONE, type, 0, player]) === 0) return;
|
|
3188
|
+
if (kinds & 3) this.writing?.args.push(first);
|
|
3189
|
+
if ((kinds >> 2) & 3) this.writing?.args.push(second);
|
|
3190
|
+
this.callNativeImpl(plugin, 'emessage_end', []);
|
|
3191
|
+
},
|
|
3192
|
+
|
|
2271
3193
|
ent_entity(this: FakeServer, plugin: PluginInstance, id: number, offset: number) {
|
|
2272
3194
|
const at = tables().entvarAt.get(offset);
|
|
2273
3195
|
return at ? fieldCell(this.entities.get(id), at.field) : 0;
|
|
@@ -2280,7 +3202,11 @@ export class FakeServer {
|
|
|
2280
3202
|
|
|
2281
3203
|
member_slot(this: FakeServer, plugin: PluginInstance, className: number, name: number) {
|
|
2282
3204
|
const key = `${plugin.memory.string(className)}::${plugin.memory.string(name)}`;
|
|
2283
|
-
|
|
3205
|
+
// A member of ReGameDLL's own is not on a server without it.
|
|
3206
|
+
const regameOnly = ['m_bSkipShowMenu', 'm_bNeededPlayers', 'm_flEscapeRatio', 'm_flTimeLimit', 'm_flGameStartTime', 'm_bTeamBalanced'];
|
|
3207
|
+
if (!this.chains && regameOnly.includes(plugin.memory.string(name))) return -1;
|
|
3208
|
+
// A member the facade reads by its own name (m_iTeam) has reapi's name too.
|
|
3209
|
+
const field = tables().memberNamed.get(key) ?? tables().constants.get(plugin.memory.string(name));
|
|
2284
3210
|
if (field === undefined) throw new Error(`no reapi member is ${key} in the gamedata`);
|
|
2285
3211
|
const slot = this.memberSlots.indexOf(field);
|
|
2286
3212
|
return slot >= 0 ? slot : this.memberSlots.push(field) - 1;
|
|
@@ -2335,6 +3261,14 @@ export class FakeServer {
|
|
|
2335
3261
|
this.setPlayerData(id, plugin.memory.string(key), plugin.memory.string(value), true);
|
|
2336
3262
|
},
|
|
2337
3263
|
|
|
3264
|
+
player_slots(this: FakeServer, plugin: PluginInstance, at: number) {
|
|
3265
|
+
plugin.playerSlots = at;
|
|
3266
|
+
},
|
|
3267
|
+
|
|
3268
|
+
player_names(this: FakeServer, plugin: PluginInstance, at: number) {
|
|
3269
|
+
plugin.playerNames = at;
|
|
3270
|
+
},
|
|
3271
|
+
|
|
2338
3272
|
// playerChange: the module wakes a plugin for the fields it listens for.
|
|
2339
3273
|
player_change_listen(this: FakeServer, plugin: PluginInstance, field: number, fn: number) {
|
|
2340
3274
|
const name = plugin.memory.string(field);
|
|
@@ -2368,16 +3302,21 @@ export class FakeServer {
|
|
|
2368
3302
|
// The dispatcher: a native with a `...` tail, its arguments laid out as
|
|
2369
3303
|
// cells. Every mask letter but `n` is an address in the plugin's memory,
|
|
2370
3304
|
// which is what a Pawn frame holds too - so the native gets the cells as
|
|
2371
|
-
// they are, and reads through the addresses itself
|
|
3305
|
+
// they are, and reads through the addresses itself; an `s` is the
|
|
3306
|
+
// plugin's string, which crosses as a Pawn string on the heap.
|
|
2372
3307
|
call(this: FakeServer, plugin: PluginInstance, id: number, argsPtr: number, maskPtr: number, argc: number) {
|
|
2373
3308
|
const name = tables().dispatched.get(id);
|
|
2374
3309
|
if (!name) throw new Error(`the dispatcher has no native with id ${id}`);
|
|
2375
3310
|
|
|
2376
3311
|
const args: number[] = [];
|
|
2377
|
-
|
|
3312
|
+
const marks: string[] = [];
|
|
3313
|
+
for (let i = 0; i < argc; i++) {
|
|
3314
|
+
args.push(plugin.memory.cell(argsPtr + i * 4));
|
|
3315
|
+
marks.push(String.fromCharCode(plugin.memory.byte(maskPtr + i)));
|
|
3316
|
+
}
|
|
2378
3317
|
|
|
2379
3318
|
if (!this.hasNative(name)) throw unsimulated(name);
|
|
2380
|
-
return this.callNativeImpl(plugin, name,
|
|
3319
|
+
return plugin.memory.across(marks, args, cells => this.callNativeImpl(plugin, name, cells));
|
|
2381
3320
|
},
|
|
2382
3321
|
};
|
|
2383
3322
|
|