@amxts/core 0.1.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/LICENSE +21 -0
- package/README.md +162 -0
- package/README.ru.md +162 -0
- package/amxmodx/base/include/amxconst.inc +594 -0
- package/amxmodx/base/include/amxmisc.inc +902 -0
- package/amxmodx/base/include/amxmodx.inc +3447 -0
- package/amxmodx/base/include/amxmodx_version.inc +20 -0
- package/amxmodx/base/include/cellarray.inc +525 -0
- package/amxmodx/base/include/cellstack.inc +166 -0
- package/amxmodx/base/include/celltrie.inc +412 -0
- package/amxmodx/base/include/core.inc +202 -0
- package/amxmodx/base/include/csstats.inc +298 -0
- package/amxmodx/base/include/csstats_const.inc +29 -0
- package/amxmodx/base/include/cstrike.inc +1287 -0
- package/amxmodx/base/include/cstrike_const.inc +521 -0
- package/amxmodx/base/include/csx.inc +259 -0
- package/amxmodx/base/include/cvars.inc +547 -0
- package/amxmodx/base/include/datapack.inc +161 -0
- package/amxmodx/base/include/dbi.inc +154 -0
- package/amxmodx/base/include/dodconst.inc +152 -0
- package/amxmodx/base/include/dodfun.inc +152 -0
- package/amxmodx/base/include/dodstats.inc +69 -0
- package/amxmodx/base/include/dodx.inc +161 -0
- package/amxmodx/base/include/engine.inc +1343 -0
- package/amxmodx/base/include/engine_const.inc +310 -0
- package/amxmodx/base/include/engine_stocks.inc +248 -0
- package/amxmodx/base/include/esf.inc +70 -0
- package/amxmodx/base/include/esf_const.inc +74 -0
- package/amxmodx/base/include/fakemeta.inc +1127 -0
- package/amxmodx/base/include/fakemeta_const.inc +793 -0
- package/amxmodx/base/include/fakemeta_stocks.inc +395 -0
- package/amxmodx/base/include/fakemeta_util.inc +882 -0
- package/amxmodx/base/include/file.inc +666 -0
- package/amxmodx/base/include/float.inc +422 -0
- package/amxmodx/base/include/fun.inc +333 -0
- package/amxmodx/base/include/gameconfig.inc +93 -0
- package/amxmodx/base/include/geoip.inc +264 -0
- package/amxmodx/base/include/ham_const.inc +4352 -0
- package/amxmodx/base/include/hamsandwich.inc +439 -0
- package/amxmodx/base/include/hlsdk_const.inc +808 -0
- package/amxmodx/base/include/json.inc +777 -0
- package/amxmodx/base/include/lang.inc +119 -0
- package/amxmodx/base/include/message_const.inc +1130 -0
- package/amxmodx/base/include/message_stocks.inc +161 -0
- package/amxmodx/base/include/messages.inc +604 -0
- package/amxmodx/base/include/newmenus.inc +387 -0
- package/amxmodx/base/include/ns.inc +754 -0
- package/amxmodx/base/include/ns_const.inc +196 -0
- package/amxmodx/base/include/nvault.inc +154 -0
- package/amxmodx/base/include/regex.inc +348 -0
- package/amxmodx/base/include/sockets.inc +183 -0
- package/amxmodx/base/include/sorting.inc +107 -0
- package/amxmodx/base/include/sqlx.inc +582 -0
- package/amxmodx/base/include/string.inc +786 -0
- package/amxmodx/base/include/string_const.inc +156 -0
- package/amxmodx/base/include/string_stocks.inc +325 -0
- package/amxmodx/base/include/textparse_ini.inc +214 -0
- package/amxmodx/base/include/textparse_smc.inc +261 -0
- package/amxmodx/base/include/tfcconst.inc +84 -0
- package/amxmodx/base/include/tfcstats.inc +62 -0
- package/amxmodx/base/include/tfcx.inc +148 -0
- package/amxmodx/base/include/time.inc +118 -0
- package/amxmodx/base/include/tsconst.inc +154 -0
- package/amxmodx/base/include/tsfun.inc +198 -0
- package/amxmodx/base/include/tsstats.inc +65 -0
- package/amxmodx/base/include/tsx.inc +88 -0
- package/amxmodx/base/include/vault.inc +60 -0
- package/amxmodx/base/include/vector.inc +131 -0
- package/amxmodx/base/include/xs.inc +1570 -0
- package/as/amxts.d.ts +153 -0
- package/as/constants.ts +4730 -0
- package/as/effects.ts +1449 -0
- package/as/entities.ts +5482 -0
- package/as/events.ts +3054 -0
- package/as/facade.ts +4639 -0
- package/as/flags.ts +299 -0
- package/as/fs.ts +287 -0
- package/as/hooks.ts +18536 -0
- package/as/kit.ts +25 -0
- package/as/lib/check.ts +86 -0
- package/as/modules/http.ts +184 -0
- package/as/natives.ts +11845 -0
- package/as/os.ts +30 -0
- package/as/promise.ts +1392 -0
- package/as/promise.types.d.ts +82 -0
- package/as/remote.ts +288 -0
- package/as/tsconfig.json +22 -0
- package/as/vector.ts +65 -0
- package/bin/amxts.mjs +10 -0
- package/includes/menu_core.inc +355 -0
- package/includes/order.txt +54 -0
- package/includes/sources.json +27 -0
- package/includes/universal_config.inc +355 -0
- package/lint/oxlint-plugin.mjs +77 -0
- package/node_modules/assemblyscript/LICENSE +201 -0
- package/node_modules/assemblyscript/NOTICE +93 -0
- package/node_modules/assemblyscript/package.json +63 -0
- package/node_modules/assemblyscript/std/README.md +6 -0
- package/node_modules/assemblyscript/std/assembly/array.ts +526 -0
- package/node_modules/assemblyscript/std/assembly/arraybuffer.ts +77 -0
- package/node_modules/assemblyscript/std/assembly/atomics.ts +127 -0
- package/node_modules/assemblyscript/std/assembly/bindings/asyncify.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/bindings/dom.ts +291 -0
- package/node_modules/assemblyscript/std/assembly/bindings/node.ts +6 -0
- package/node_modules/assemblyscript/std/assembly/builtins.ts +2631 -0
- package/node_modules/assemblyscript/std/assembly/compat.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/console.ts +42 -0
- package/node_modules/assemblyscript/std/assembly/crypto.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/dataview.ts +181 -0
- package/node_modules/assemblyscript/std/assembly/date.ts +373 -0
- package/node_modules/assemblyscript/std/assembly/diagnostics.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/error.ts +44 -0
- package/node_modules/assemblyscript/std/assembly/function.ts +38 -0
- package/node_modules/assemblyscript/std/assembly/index.d.ts +2822 -0
- package/node_modules/assemblyscript/std/assembly/iterator.ts +35 -0
- package/node_modules/assemblyscript/std/assembly/map.ts +260 -0
- package/node_modules/assemblyscript/std/assembly/math.ts +3289 -0
- package/node_modules/assemblyscript/std/assembly/memory.ts +123 -0
- package/node_modules/assemblyscript/std/assembly/number.ts +388 -0
- package/node_modules/assemblyscript/std/assembly/object.ts +36 -0
- package/node_modules/assemblyscript/std/assembly/performance.ts +9 -0
- package/node_modules/assemblyscript/std/assembly/polyfills.ts +27 -0
- package/node_modules/assemblyscript/std/assembly/process.ts +50 -0
- package/node_modules/assemblyscript/std/assembly/reference.ts +48 -0
- package/node_modules/assemblyscript/std/assembly/regexp.ts +12 -0
- package/node_modules/assemblyscript/std/assembly/rt/README.md +83 -0
- package/node_modules/assemblyscript/std/assembly/rt/common.ts +81 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-incremental.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-minimal.ts +2 -0
- package/node_modules/assemblyscript/std/assembly/rt/index-stub.ts +1 -0
- package/node_modules/assemblyscript/std/assembly/rt/index.d.ts +37 -0
- package/node_modules/assemblyscript/std/assembly/rt/itcms.ts +419 -0
- package/node_modules/assemblyscript/std/assembly/rt/rtrace.ts +15 -0
- package/node_modules/assemblyscript/std/assembly/rt/stub.ts +133 -0
- package/node_modules/assemblyscript/std/assembly/rt/tcms.ts +254 -0
- package/node_modules/assemblyscript/std/assembly/rt/tlsf.ts +592 -0
- package/node_modules/assemblyscript/std/assembly/rt.ts +87 -0
- package/node_modules/assemblyscript/std/assembly/set.ts +220 -0
- package/node_modules/assemblyscript/std/assembly/shared/feature.ts +65 -0
- package/node_modules/assemblyscript/std/assembly/shared/runtime.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/target.ts +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/tsconfig.json +11 -0
- package/node_modules/assemblyscript/std/assembly/shared/typeinfo.ts +72 -0
- package/node_modules/assemblyscript/std/assembly/staticarray.ts +401 -0
- package/node_modules/assemblyscript/std/assembly/string.ts +847 -0
- package/node_modules/assemblyscript/std/assembly/symbol.ts +114 -0
- package/node_modules/assemblyscript/std/assembly/table.ts +16 -0
- package/node_modules/assemblyscript/std/assembly/tsconfig.json +6 -0
- package/node_modules/assemblyscript/std/assembly/typedarray.ts +1945 -0
- package/node_modules/assemblyscript/std/assembly/uri.ts +17 -0
- package/node_modules/assemblyscript/std/assembly/util/bytes.ts +107 -0
- package/node_modules/assemblyscript/std/assembly/util/casemap.ts +497 -0
- package/node_modules/assemblyscript/std/assembly/util/error.ts +54 -0
- package/node_modules/assemblyscript/std/assembly/util/hash.ts +117 -0
- package/node_modules/assemblyscript/std/assembly/util/math.ts +1922 -0
- package/node_modules/assemblyscript/std/assembly/util/memory.ts +290 -0
- package/node_modules/assemblyscript/std/assembly/util/number.ts +873 -0
- package/node_modules/assemblyscript/std/assembly/util/sort.ts +313 -0
- package/node_modules/assemblyscript/std/assembly/util/string.ts +1202 -0
- package/node_modules/assemblyscript/std/assembly/util/uri.ts +275 -0
- package/node_modules/assemblyscript/std/assembly/vector.ts +4 -0
- package/node_modules/assemblyscript/std/assembly.json +16 -0
- package/node_modules/assemblyscript/std/portable/index.d.ts +462 -0
- package/node_modules/assemblyscript/std/portable/index.js +416 -0
- package/node_modules/assemblyscript/std/portable.json +11 -0
- package/node_modules/assemblyscript/std/types/assembly/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/assembly/package.json +3 -0
- package/node_modules/assemblyscript/std/types/portable/index.d.ts +1 -0
- package/node_modules/assemblyscript/std/types/portable/package.json +3 -0
- package/node_modules/assemblyscript/tsconfig-base.json +13 -0
- package/package.json +112 -0
- package/runtime/deps/assemblyscript/LICENSE +201 -0
- package/runtime/deps/assemblyscript/NOTICE +93 -0
- package/runtime/deps/assemblyscript/bin/asc.js +35 -0
- package/runtime/deps/assemblyscript/dist/asc.js +25035 -0
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +337 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/LICENSE +201 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/index.js +28905 -0
- package/runtime/deps/assemblyscript/node_modules/binaryen/package.json +50 -0
- package/runtime/deps/assemblyscript/node_modules/long/LICENSE +202 -0
- package/runtime/deps/assemblyscript/node_modules/long/index.js +1581 -0
- package/runtime/deps/assemblyscript/node_modules/long/package.json +58 -0
- package/runtime/deps/assemblyscript/package.json +114 -0
- package/runtime/host/amxts.inc +85 -0
- package/runtime/licenses/AMXX-LICENSE.txt +34 -0
- package/runtime/licenses/AssemblyScript-LICENSE +201 -0
- package/runtime/licenses/AssemblyScript-NOTICE +93 -0
- package/runtime/licenses/Binaryen-LICENSE +201 -0
- package/runtime/licenses/Bun-LICENSE.md +84 -0
- package/runtime/licenses/GPL-3.0.txt +674 -0
- package/runtime/licenses/LLVM-LICENSE.TXT +279 -0
- package/runtime/licenses/README.md +25 -0
- package/runtime/licenses/WAMR-LICENSE +219 -0
- package/runtime/natives.txt +1095 -0
- package/runtime/patches/README.md +59 -0
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +14174 -0
- package/runtime/patches/wamr-2.4.5-amxts.patch +364 -0
- package/scripts/apply-docs.ts +508 -0
- package/scripts/asc.ts +27 -0
- package/scripts/auto-imports.ts +258 -0
- package/scripts/build-wasm.ts +595 -0
- package/scripts/check.ts +93 -0
- package/scripts/compile-cache.ts +179 -0
- package/scripts/compile.ts +401 -0
- package/scripts/docker-server.ts +73 -0
- package/scripts/docs/as/amxts.ts +210 -0
- package/scripts/docs/as/effects.ts +1684 -0
- package/scripts/docs/as/facade.ts +3302 -0
- package/scripts/docs/as/fs.ts +173 -0
- package/scripts/docs/as/lib/check.ts +68 -0
- package/scripts/docs/as/modules/http.ts +66 -0
- package/scripts/docs/as/os.ts +16 -0
- package/scripts/docs/as/promise.ts +132 -0
- package/scripts/docs/as/promise.types.ts +63 -0
- package/scripts/docs/as/vector.ts +48 -0
- package/scripts/docs/entities.ts +2338 -0
- package/scripts/docs/events.ts +245 -0
- package/scripts/docs/game.ts +497 -0
- package/scripts/docs/messages.ts +148 -0
- package/scripts/file-contents.ts +74 -0
- package/scripts/includes.ts +134 -0
- package/scripts/player-fields.ts +866 -0
- package/scripts/plugin-cache.ts +92 -0
- package/scripts/plugin-natives.ts +1157 -0
- package/scripts/prebuilt.ts +268 -0
- package/scripts/prepare.ts +134 -0
- package/scripts/project.ts +973 -0
- package/scripts/run.ts +28 -0
- package/scripts/shared-modules.ts +906 -0
- package/scripts/system.ts +33 -0
- package/scripts/tracked-fs.ts +115 -0
- package/scripts/typed-configs.ts +788 -0
- package/scripts/ui.ts +77 -0
- package/src/cli-api.mjs +175 -0
- package/src/parser/include-parser.ts +506 -0
- package/src/system.mjs +155 -0
- package/src/testing/compile-cache.ts +31 -0
- package/src/testing/compile.ts +222 -0
- package/src/testing/coroutines.ts +191 -0
- package/src/testing/index.ts +39 -0
- package/src/testing/kits.ts +55 -0
- package/src/testing/memory.ts +125 -0
- package/src/testing/natives.ts +925 -0
- package/src/testing/server.ts +1874 -0
- package/src/testing/setup.ts +63 -0
- package/src/testing/tables.ts +139 -0
- package/src/types.ts +51 -0
|
@@ -0,0 +1,1874 @@
|
|
|
1
|
+
import type { PluginNative } from '../../scripts/plugin-natives';
|
|
2
|
+
import type { Native, NativeCall } from './natives';
|
|
3
|
+
import type { HookShape } from './tables';
|
|
4
|
+
import { pawnLayout } from '../../scripts/plugin-natives';
|
|
5
|
+
// A game server in TypeScript, standing where runtime/src/module.cpp and AMX
|
|
6
|
+
// Mod X stand on a real one.
|
|
7
|
+
//
|
|
8
|
+
// A plugin's wasm imports two things from "env": the bridge externals the
|
|
9
|
+
// facade declares (slot, clcmd, task, on, hook, call, arg_text, say_text ...)
|
|
10
|
+
// and the natives as/natives.ts declares, which take Pawn cells. The bridge is
|
|
11
|
+
// implemented here the way module.cpp implements it - callback slots, the
|
|
12
|
+
// events registry, Fire with its outcome - and the natives in natives.ts,
|
|
13
|
+
// against the players and entities below. An import neither covers throws,
|
|
14
|
+
// naming itself, the first time the plugin calls it.
|
|
15
|
+
import { compile } from './compile';
|
|
16
|
+
import { Coroutines, nextTaskId } from './coroutines';
|
|
17
|
+
import { installKitFor } from './kits';
|
|
18
|
+
import { bitsFloat, floatBits, Memory, utf8Fit } from './memory';
|
|
19
|
+
import { NATIVES, WEAPON_NAMES } from './natives';
|
|
20
|
+
import { constant, tables } from './tables';
|
|
21
|
+
|
|
22
|
+
declare const WebAssembly: any;
|
|
23
|
+
|
|
24
|
+
/** What a test hands the plugin: a cell, a flag, or text. */
|
|
25
|
+
export type Value = number | boolean | string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One argument of a call the plugin is handling, as the caller's frame holds
|
|
29
|
+
* it: a cell, text, or an array (a Pawn array passed by reference - the
|
|
30
|
+
* plugin can read it and write into it).
|
|
31
|
+
*/
|
|
32
|
+
export type ArgValue = Value | number[] | Pointer;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An argument another plugin passed by address - an `any:...` tail - read
|
|
36
|
+
* in its memory as text or as a cell, whichever the native asks for.
|
|
37
|
+
*/
|
|
38
|
+
export class Pointer {
|
|
39
|
+
constructor(readonly plugin: PluginInstance, readonly at: number) {}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** What `server.native()` gives back: the result decoded the way Pawn would read it. */
|
|
43
|
+
export type NativeResult = number | boolean | string | number[] | string[] | null | undefined;
|
|
44
|
+
|
|
45
|
+
/** A path as AMX Mod X takes it, relative to the game folder: `a/b/c.txt`. */
|
|
46
|
+
export function normalizePath(path: string): string {
|
|
47
|
+
return path.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A parameter's include default as the value Pawn passes when the call leaves it out. */
|
|
51
|
+
function pawnDefaultValue(text: string | null): Value {
|
|
52
|
+
if (text === null) return 0;
|
|
53
|
+
if (text === 'true' || text === 'false') return text === 'true';
|
|
54
|
+
if (text.startsWith('"')) return text.slice(1, -1).replace(/\^(.)/g, (_, c: string) => c === 'n' ? '\n' : c === 't' ? '\t' : c);
|
|
55
|
+
return Number(text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A wasm function the server calls back, and how. */
|
|
59
|
+
interface Handler {
|
|
60
|
+
plugin: PluginInstance;
|
|
61
|
+
/** Its index in the plugin's function table. */
|
|
62
|
+
fn: number;
|
|
63
|
+
/** 0: one cell, the player id. 1: four cells. See SHAPE_* in as/facade.ts. */
|
|
64
|
+
shape: number;
|
|
65
|
+
/** A closure's number: fn is then the plugin's dispatcher, called with it first (Handler.tag in module.cpp). */
|
|
66
|
+
tag?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A host public standing in for a handler: `__amxts_cb<index>`. */
|
|
70
|
+
interface Slot extends Handler {
|
|
71
|
+
index: number;
|
|
72
|
+
/** What the call answers when the handler says nothing. */
|
|
73
|
+
fallback: number;
|
|
74
|
+
key: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Task {
|
|
78
|
+
slot: Slot;
|
|
79
|
+
id: number;
|
|
80
|
+
interval: number;
|
|
81
|
+
due: number;
|
|
82
|
+
repeat: boolean;
|
|
83
|
+
order: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Cvar {
|
|
87
|
+
name: string;
|
|
88
|
+
value: string;
|
|
89
|
+
pointer: number;
|
|
90
|
+
hooks: Slot[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The hookchain being run, for GetHookChainReturn and friends. */
|
|
94
|
+
export interface Chain {
|
|
95
|
+
shape: HookShape;
|
|
96
|
+
answer: Value;
|
|
97
|
+
args: Value[];
|
|
98
|
+
/** SetHookChainReturn ran: reapi lets a chain that answers be stopped only then. */
|
|
99
|
+
answered?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What firing a hookchain came to. */
|
|
103
|
+
export interface HookResult {
|
|
104
|
+
/** A pre listener stopped the game's function: preventDefault, or an answer. */
|
|
105
|
+
prevented: boolean;
|
|
106
|
+
/** The chain's answer after every listener: a number, a boolean or text, by the chain. */
|
|
107
|
+
result: Value;
|
|
108
|
+
/** The arguments as the listeners left them (`event.damage = 10`). */
|
|
109
|
+
args: Value[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A user message a plugin sent: ScreenFade, StatusIcon, ... */
|
|
113
|
+
export interface UserMessage {
|
|
114
|
+
name: string;
|
|
115
|
+
/** The player it went to; 0 for everyone. */
|
|
116
|
+
player: number;
|
|
117
|
+
/** What was written, in order: bytes, shorts and longs as numbers, strings as text. */
|
|
118
|
+
args: (number | string)[];
|
|
119
|
+
/** For a temporary effect (`SVC_TEMPENTITY`): who it went to, as `MSG_*` - `0` everyone, `4` those who see `origin`, `8` one player. */
|
|
120
|
+
dest?: number;
|
|
121
|
+
/** For a temporary effect: the point it is seen from, with `dest` `4`. */
|
|
122
|
+
origin?: number[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A forward a plugin sent out through ExecuteForward. */
|
|
126
|
+
export interface SentForward {
|
|
127
|
+
name: string;
|
|
128
|
+
/** Each argument as Pawn got it: a cell, a Float, text, or an array's cells. */
|
|
129
|
+
args: (Value | number[])[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** One line a player was shown. */
|
|
133
|
+
export interface Message {
|
|
134
|
+
variant: 'chat' | 'center' | 'console' | 'notify' | 'hud';
|
|
135
|
+
text: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ServerOptions {
|
|
139
|
+
/** "de_dust2" unless said. */
|
|
140
|
+
map?: string;
|
|
141
|
+
/** 32 unless said. */
|
|
142
|
+
maxPlayers?: number;
|
|
143
|
+
/** The modules hasModule() finds. All of reapi, cstrike, fun, hamsandwich, engine, fakemeta, nvault, resemiclip unless said. */
|
|
144
|
+
modules?: string[];
|
|
145
|
+
/** Cvars the server has before the plugin loads: `{ mp_freezetime: "5" }`. */
|
|
146
|
+
cvars?: Record<string, string>;
|
|
147
|
+
/** Files in the game folder before the plugin loads, by path: `{ "addons/amxmodx/configs/x.ini": "..." }`. */
|
|
148
|
+
files?: Record<string, string>;
|
|
149
|
+
/**
|
|
150
|
+
* The system the server runs on, as `~/os` finds it: "win32" puts the amxts
|
|
151
|
+
* module's amxts_amxx.dll in the modules folder. "linux" unless said.
|
|
152
|
+
*/
|
|
153
|
+
platform?: 'win32' | 'linux';
|
|
154
|
+
/** The server's time zone, what Date's local getters read: "Asia/Yerevan". This machine's unless said. */
|
|
155
|
+
timeZone?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type TeamName = 'UNASSIGNED' | 'TERRORIST' | 'CT' | 'SPECTATOR';
|
|
159
|
+
|
|
160
|
+
export const TEAMS: TeamName[] = ['UNASSIGNED', 'TERRORIST', 'CT', 'SPECTATOR'];
|
|
161
|
+
|
|
162
|
+
export interface JoinOptions {
|
|
163
|
+
team?: TeamName;
|
|
164
|
+
health?: number;
|
|
165
|
+
armor?: number;
|
|
166
|
+
alive?: boolean;
|
|
167
|
+
bot?: boolean;
|
|
168
|
+
authid?: string;
|
|
169
|
+
ip?: string;
|
|
170
|
+
/** Admin flags as users.ini writes them: "abcdefghijklmnopqrstu". "z" - a plain user - unless said. */
|
|
171
|
+
flags?: string;
|
|
172
|
+
origin?: number[];
|
|
173
|
+
/** What he carries; the first is in his hands. A knife unless said. */
|
|
174
|
+
weapons?: string[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const SHAPE_NARROW = 0;
|
|
178
|
+
const SHAPE_WIDE = 1;
|
|
179
|
+
const PLUGIN_HANDLED = 1;
|
|
180
|
+
const HC_SUPERCEDE = 1;
|
|
181
|
+
const HC_BREAK = 2;
|
|
182
|
+
// A Ham Sandwich hook's answer that blocks the game's function.
|
|
183
|
+
const HAM_SUPERCEDE = 4;
|
|
184
|
+
// A message argument's types, as get_msg_argtype answers.
|
|
185
|
+
const ARG_BYTE = 1;
|
|
186
|
+
const ARG_COORD = 6;
|
|
187
|
+
const ARG_STRING = 7;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Date#getTimezoneOffset() in a time zone: minutes from its wall clock at
|
|
191
|
+
* that moment to UTC, UTC+4 being -240. This machine's without one.
|
|
192
|
+
*/
|
|
193
|
+
function timezoneOffset(time: number, timeZone: string | undefined): number {
|
|
194
|
+
if (timeZone === undefined) return new Date(time).getTimezoneOffset();
|
|
195
|
+
const parts = new Intl.DateTimeFormat('en-US', { timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' }).formatToParts(time);
|
|
196
|
+
const part = (type: string) => Number(parts.find(p => p.type === type)?.value);
|
|
197
|
+
const wall = Date.UTC(part('year'), part('month') - 1, part('day'), part('hour'), part('minute'), part('second'));
|
|
198
|
+
return (Math.floor(time / 1000) * 1000 - wall) / 60_000;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Any entity: its entvars and members, as the natives read and write them. */
|
|
202
|
+
export class FakeEntity {
|
|
203
|
+
/** Keyed by the constant's value, and `value:element` for an array member. */
|
|
204
|
+
readonly fields = new Map<string, number | number[] | string>();
|
|
205
|
+
|
|
206
|
+
constructor(readonly server: FakeServer, readonly id: number, classname: string) {
|
|
207
|
+
this.set('var_classname', classname);
|
|
208
|
+
this.set('var_gravity', 1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A field by its constant's name, with or without the prefix:
|
|
213
|
+
* `get("gravity")`, `get("var_renderfx")`, `get("m_iHideHUD")`. A float
|
|
214
|
+
* is a number, a vector an array of three.
|
|
215
|
+
*/
|
|
216
|
+
get(name: string, element?: number): number | number[] | string {
|
|
217
|
+
return this.fields.get(FakeEntity.key(fieldOf(name), element)) ?? 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
set(name: string, value: number | number[] | string, element?: number): void {
|
|
221
|
+
this.fields.set(FakeEntity.key(fieldOf(name), element), value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static key(field: number, element?: number): string {
|
|
225
|
+
return element === undefined ? `${field}` : `${field}:${element}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
get classname(): string { return String(this.get('var_classname')); }
|
|
229
|
+
|
|
230
|
+
get origin(): number[] { return vector(this.get('var_origin')); }
|
|
231
|
+
set origin(value: number[]) { this.set('var_origin', [...value]); }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function vector(value: number | number[] | string): number[] {
|
|
235
|
+
return Array.isArray(value) ? value : [0, 0, 0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function fieldOf(name: string): number {
|
|
239
|
+
const { constants } = tables();
|
|
240
|
+
const value = constants.get(name) ?? constants.get(`var_${name}`) ?? constants.get(`m_${name}`);
|
|
241
|
+
if (value === undefined) throw new Error(`no entvar or member named ${name}`);
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** A weapon a player carries. */
|
|
246
|
+
export class FakeWeapon extends FakeEntity {
|
|
247
|
+
constructor(server: FakeServer, id: number, readonly kind: string, owner: number) {
|
|
248
|
+
super(server, id, kind);
|
|
249
|
+
this.set('m_iId', WEAPON_NAMES.indexOf(kind));
|
|
250
|
+
this.set('var_owner', owner);
|
|
251
|
+
this.set('m_pPlayer', owner);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Which of m_rgpPlayerItems a weapon goes in: 1 primary, 2 pistol, 3 knife, 4 grenades, 5 C4. */
|
|
256
|
+
function slotOf(kind: string): number {
|
|
257
|
+
if (kind === 'weapon_knife') return 3;
|
|
258
|
+
if (kind === 'weapon_c4') return 5;
|
|
259
|
+
if (/grenade|flashbang/.test(kind)) return 4;
|
|
260
|
+
if (/p228|elite|fiveseven|usp|glock18|deagle/.test(kind)) return 2;
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** A player: what the natives read, and what he was shown. */
|
|
265
|
+
export class FakePlayer extends FakeEntity {
|
|
266
|
+
connected = true;
|
|
267
|
+
alive: boolean;
|
|
268
|
+
readonly bot: boolean;
|
|
269
|
+
readonly authid: string;
|
|
270
|
+
readonly ip: string;
|
|
271
|
+
/** Admin flags as bits: ADMIN_* . */
|
|
272
|
+
flags: number;
|
|
273
|
+
/** SPEAK_* flags: `muted` is SPEAK_MUTED. */
|
|
274
|
+
speak = 0;
|
|
275
|
+
/** Backpack ammo by weapon name. */
|
|
276
|
+
readonly ammo = new Map<string, number>();
|
|
277
|
+
/** Every line he was shown, in order. */
|
|
278
|
+
readonly messages: Message[] = [];
|
|
279
|
+
/** What was run in his console: client_cmd, engclient_cmd. */
|
|
280
|
+
readonly commands: string[] = [];
|
|
281
|
+
/** His userinfo keys - `lang` says his language. */
|
|
282
|
+
readonly info = new Map<string, string>();
|
|
283
|
+
|
|
284
|
+
constructor(server: FakeServer, id: number, public name: string, options: JoinOptions) {
|
|
285
|
+
super(server, id, 'player');
|
|
286
|
+
this.alive = options.alive ?? true;
|
|
287
|
+
this.bot = options.bot ?? false;
|
|
288
|
+
this.authid = options.authid ?? (this.bot ? 'BOT' : `STEAM_0:0:${id}`);
|
|
289
|
+
this.ip = options.ip ?? `127.0.0.${id}:27005`;
|
|
290
|
+
this.flags = flagBits(options.flags ?? 'z');
|
|
291
|
+
this.team = options.team ?? 'CT';
|
|
292
|
+
this.health = options.health ?? 100;
|
|
293
|
+
this.armor = options.armor ?? 0;
|
|
294
|
+
this.set('var_max_health', 100);
|
|
295
|
+
this.set('var_netname', name);
|
|
296
|
+
this.set('var_flags', constant('FL_CLIENT') | constant('FL_ONGROUND'));
|
|
297
|
+
this.origin = options.origin ?? [0, 0, 0];
|
|
298
|
+
|
|
299
|
+
for (const weapon of options.weapons ?? ['weapon_knife']) this.give(weapon);
|
|
300
|
+
const first = this.items[0];
|
|
301
|
+
if (first) this.set('m_pActiveItem', first.id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get health(): number { return Number(this.get('var_health')); }
|
|
305
|
+
set health(value: number) { this.set('var_health', value); }
|
|
306
|
+
|
|
307
|
+
get armor(): number { return Number(this.get('var_armorvalue')); }
|
|
308
|
+
set armor(value: number) { this.set('var_armorvalue', value); }
|
|
309
|
+
|
|
310
|
+
get frags(): number { return Number(this.get('var_frags')); }
|
|
311
|
+
set frags(value: number) { this.set('var_frags', value); }
|
|
312
|
+
|
|
313
|
+
get deaths(): number { return Number(this.get('m_iDeaths')); }
|
|
314
|
+
set deaths(value: number) { this.set('m_iDeaths', value); }
|
|
315
|
+
|
|
316
|
+
get team(): TeamName { return TEAMS[Number(this.get('m_iTeam'))] ?? 'UNASSIGNED'; }
|
|
317
|
+
set team(value: TeamName) { this.set('m_iTeam', Math.max(TEAMS.indexOf(value), 0)); }
|
|
318
|
+
|
|
319
|
+
get muted(): boolean { return (this.speak & constant('SPEAK_MUTED')) !== 0; }
|
|
320
|
+
|
|
321
|
+
/** The chat lines he was shown, one a line, colour bytes taken out. */
|
|
322
|
+
get chat(): string { return this.lines('chat'); }
|
|
323
|
+
/** What was printed in the middle of his screen. */
|
|
324
|
+
get center(): string { return this.lines('center'); }
|
|
325
|
+
/** What was printed in his console. */
|
|
326
|
+
get console(): string { return this.lines('console'); }
|
|
327
|
+
/** HUD messages. */
|
|
328
|
+
get hud(): string { return this.lines('hud'); }
|
|
329
|
+
|
|
330
|
+
private lines(variant: Message['variant']): string {
|
|
331
|
+
return this.messages.filter(m => m.variant === variant).map(m => m.text).join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** A line for him. @internal */
|
|
335
|
+
show(variant: Message['variant'], text: string): void {
|
|
336
|
+
// The colour codes ^1-^4 a chat line carries are not text.
|
|
337
|
+
// oxlint-disable-next-line no-control-regex
|
|
338
|
+
this.messages.push({ variant, text: text.replace(/[\x01-\x04]/g, '') });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Forgets what he was shown, for a test that checks one step at a time. */
|
|
342
|
+
clearMessages(): void {
|
|
343
|
+
this.messages.length = 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Every weapon he carries, slot by slot. */
|
|
347
|
+
get items(): FakeWeapon[] {
|
|
348
|
+
const list: FakeWeapon[] = [];
|
|
349
|
+
for (let slot = 0; slot < 6; slot++) {
|
|
350
|
+
let id = Number(this.get('m_rgpPlayerItems', slot));
|
|
351
|
+
while (id > 0) {
|
|
352
|
+
const weapon = this.server.entities.get(id);
|
|
353
|
+
if (!(weapon instanceof FakeWeapon)) break;
|
|
354
|
+
list.push(weapon);
|
|
355
|
+
id = Number(weapon.get('m_pNext'));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return list;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** What is in his hands. */
|
|
362
|
+
get activeItem(): FakeWeapon | null {
|
|
363
|
+
const weapon = this.server.entities.get(Number(this.get('m_pActiveItem')));
|
|
364
|
+
return weapon instanceof FakeWeapon ? weapon : null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Hands him a weapon, at the end of its slot's chain. */
|
|
368
|
+
give(kind: string): FakeWeapon {
|
|
369
|
+
const weapon = new FakeWeapon(this.server, this.server.nextEntityId(), kind, this.id);
|
|
370
|
+
this.server.entities.set(weapon.id, weapon);
|
|
371
|
+
|
|
372
|
+
const slot = slotOf(kind);
|
|
373
|
+
const first = Number(this.get('m_rgpPlayerItems', slot));
|
|
374
|
+
if (first <= 0) {
|
|
375
|
+
this.set('m_rgpPlayerItems', weapon.id, slot);
|
|
376
|
+
} else {
|
|
377
|
+
let last = this.server.entities.get(first)!;
|
|
378
|
+
while (Number(last.get('m_pNext')) > 0) last = this.server.entities.get(Number(last.get('m_pNext')))!;
|
|
379
|
+
last.set('m_pNext', weapon.id);
|
|
380
|
+
}
|
|
381
|
+
return weapon;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Takes every weapon away. */
|
|
385
|
+
stripWeapons(): void {
|
|
386
|
+
for (const weapon of this.items) this.server.entities.delete(weapon.id);
|
|
387
|
+
for (let slot = 0; slot < 6; slot++) this.set('m_rgpPlayerItems', 0, slot);
|
|
388
|
+
this.set('m_pActiveItem', 0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** `say <text>` - a chat line, and a chat command if it is one. True if a plugin swallowed it. */
|
|
392
|
+
say(text: string): boolean {
|
|
393
|
+
return this.chatLine('say', text);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** `say_team <text>`. */
|
|
397
|
+
sayTeam(text: string): boolean {
|
|
398
|
+
return this.chatLine('say_team', text);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private chatLine(command: string, text: string): boolean {
|
|
402
|
+
if (this.server.clientCommand(this, [command, text])) return true;
|
|
403
|
+
|
|
404
|
+
// Nobody took it: the game shows it, to everyone or to the team.
|
|
405
|
+
for (const player of this.server.players) {
|
|
406
|
+
if (command === 'say' || player.team === this.team) player.show('chat', `${this.name}: ${text}`);
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** A console command, split the way the engine splits it: `amx_slap "Some One" 5`. True if handled. */
|
|
412
|
+
command(line: string): boolean {
|
|
413
|
+
return this.server.clientCommand(this, splitCommand(line));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Leaves the server: the disconnect events, then the slot is free. */
|
|
417
|
+
disconnect(options: { dropped?: boolean; reason?: string } = {}): void {
|
|
418
|
+
const dropped = options.dropped ?? false;
|
|
419
|
+
const reason = options.reason ?? 'Client sent \'drop\'';
|
|
420
|
+
this.server.fire('client_disconnected', this.id, dropped, reason, 192);
|
|
421
|
+
this.server.clearPlayerData(this.id);
|
|
422
|
+
this.server.fire('client_disconnect', this.id);
|
|
423
|
+
this.connected = false;
|
|
424
|
+
this.alive = false;
|
|
425
|
+
this.server.fire('client_remove', this.id, dropped, reason);
|
|
426
|
+
this.server.entities.delete(this.id);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** A line a plugin prints to the server's console, into the log. */
|
|
431
|
+
function logLine(this: FakeServer, plugin: PluginInstance, message: number): void {
|
|
432
|
+
this.logLines.push(plugin.memory.string(message));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** users.ini letters as AMX Mod X reads them: `a` is bit 0, `b` bit 1 ... */
|
|
436
|
+
export function flagBits(letters: string): number {
|
|
437
|
+
let bits = 0;
|
|
438
|
+
for (const letter of letters) {
|
|
439
|
+
const at = letter.charCodeAt(0) - 97;
|
|
440
|
+
if (at >= 0 && at < 26) bits |= 1 << at;
|
|
441
|
+
}
|
|
442
|
+
return bits;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** What stops a test that calls a native nothing here answers. */
|
|
446
|
+
function unsimulated(name: string): Error {
|
|
447
|
+
return new Error(`the plugin called the native "${name}", which the fake server does not simulate: answer it in the test with server.defineNative("${name}", (call, args) => ...)`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** A command line as the engine splits it: on spaces, a quoted part kept whole. */
|
|
451
|
+
function splitCommand(line: string): string[] {
|
|
452
|
+
return [...line.matchAll(/"([^"]*)"|(\S+)/g)].map(m => m[1] ?? m[2]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** A loaded plugin: its instance and its memory. */
|
|
456
|
+
export class PluginInstance {
|
|
457
|
+
instance: any;
|
|
458
|
+
memory!: Memory;
|
|
459
|
+
/** What plugin() said about it. */
|
|
460
|
+
info = { name: '', version: '', author: '', description: '' };
|
|
461
|
+
|
|
462
|
+
/** The coroutine scheduler, for a plugin that awaits; null for one that does not. */
|
|
463
|
+
readonly coroutines: Coroutines | null;
|
|
464
|
+
|
|
465
|
+
constructor(readonly server: FakeServer, readonly source: string, module: any, readonly natives: PluginNative[] = [], binary?: Uint8Array) {
|
|
466
|
+
this.coroutines = binary ? new Coroutines(binary) : null;
|
|
467
|
+
const scheduler = this.coroutines?.imports() ?? {};
|
|
468
|
+
const env: Record<string, (...args: any[]) => any> = {};
|
|
469
|
+
for (const { module: from, name } of WebAssembly.Module.imports(module)) {
|
|
470
|
+
if (from === 'env') env[name] = Object.hasOwn(scheduler, name) ? scheduler[name] : this.importFor(name);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
this.instance = new WebAssembly.Instance(module, { env });
|
|
474
|
+
this.memory = new Memory(this.instance.exports);
|
|
475
|
+
this.coroutines?.attach(this.instance.exports);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Runs the file's top level. */
|
|
479
|
+
start(): void {
|
|
480
|
+
this.server.within(this, () => this.instance.exports._start?.());
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
get table(): any {
|
|
484
|
+
return this.instance.exports.table;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private importFor(name: string): (...args: any[]) => any {
|
|
488
|
+
if (Object.hasOwn(this.server.bridge, name)) {
|
|
489
|
+
const bridge = (this.server.bridge as Record<string, (...args: any[]) => any>)[name];
|
|
490
|
+
return (...args: number[]) => bridge.call(this.server, this, ...args);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (this.server.hasNative(name)) return (...args: number[]) => this.server.callNativeImpl(this, name, args);
|
|
494
|
+
|
|
495
|
+
// Another plugin's `export function`, called through ~/natives as AMX
|
|
496
|
+
// Mod X would route it; whether one exports it is known only at the call.
|
|
497
|
+
return (...cells: number[]) => {
|
|
498
|
+
if (this.server.exported.has(name)) return this.server.callFromPlugin(this, name, cells);
|
|
499
|
+
throw unsimulated(name);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The server a plugin runs on in a test.
|
|
506
|
+
*
|
|
507
|
+
* ```ts
|
|
508
|
+
* const server = await loadPlugin("plugins/myplugin.ts");
|
|
509
|
+
* const player = server.join("Alice", { team: "CT" });
|
|
510
|
+
* player.say("/tour");
|
|
511
|
+
* server.advance(5000);
|
|
512
|
+
* ```
|
|
513
|
+
*/
|
|
514
|
+
export class FakeServer {
|
|
515
|
+
readonly map: string;
|
|
516
|
+
readonly maxPlayers: number;
|
|
517
|
+
readonly modules: Set<string>;
|
|
518
|
+
/** Milliseconds since the map started; advance() moves it. */
|
|
519
|
+
time = 0;
|
|
520
|
+
/** Date.now() when the map started. */
|
|
521
|
+
readonly startedAt = Date.now();
|
|
522
|
+
/** The server's time zone; this machine's when undefined. */
|
|
523
|
+
readonly timeZone: string | undefined;
|
|
524
|
+
|
|
525
|
+
readonly plugins: PluginInstance[] = [];
|
|
526
|
+
/** Players by slot, 1 to maxPlayers; weapons and other entities after them. */
|
|
527
|
+
readonly entities = new Map<number, FakeEntity>();
|
|
528
|
+
/** What console.log and server_print wrote, one entry a line. */
|
|
529
|
+
readonly logLines: string[] = [];
|
|
530
|
+
/** What server_cmd ran. */
|
|
531
|
+
readonly commands: string[] = [];
|
|
532
|
+
/** Forwards the plugins sent out with ExecuteForward, in order. */
|
|
533
|
+
readonly forwards: SentForward[] = [];
|
|
534
|
+
/** nVault files, by name. */
|
|
535
|
+
readonly vaults = new Map<string, Map<string, string>>();
|
|
536
|
+
/**
|
|
537
|
+
* The game folder's files, by path relative to it
|
|
538
|
+
* (`addons/amxmodx/data/x.txt`): what fs and the file natives read and
|
|
539
|
+
* write. A test fills it before the plugin reads, and reads it after.
|
|
540
|
+
*/
|
|
541
|
+
readonly files = new Map<string, Uint8Array>();
|
|
542
|
+
/** Folders that are there with nothing in them; a file's folders are there anyway. */
|
|
543
|
+
readonly folders = new Set<string>(['addons', 'addons/amxmodx', 'addons/amxmodx/configs', 'addons/amxmodx/data']);
|
|
544
|
+
/** What get_localinfo answers: the folders AMX Mod X was told about. */
|
|
545
|
+
readonly localinfo = new Map<string, string>([
|
|
546
|
+
['amxx_basedir', 'addons/amxmodx'],
|
|
547
|
+
['amxx_configsdir', 'addons/amxmodx/configs'],
|
|
548
|
+
['amxx_datadir', 'addons/amxmodx/data'],
|
|
549
|
+
['amxx_modulesdir', 'addons/amxmodx/modules'],
|
|
550
|
+
]);
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* The dynamic arrays (cellarray.inc) plugins made, by handle: an item is
|
|
554
|
+
* its cells, text a byte a cell. `cellArrayStrings(handle)` and
|
|
555
|
+
* `cellArrayCells(handle)` read one the way a Pawn plugin would.
|
|
556
|
+
*/
|
|
557
|
+
readonly cellArrays = new Map<number, { cellSize: number; items: number[][] }>();
|
|
558
|
+
private nextCellArray = 1;
|
|
559
|
+
/** fopen's and open_dir's handles. @internal */
|
|
560
|
+
readonly handles = new Map<number, { path: string; position: number } | { entries: string[]; next: number }>();
|
|
561
|
+
|
|
562
|
+
/** Everything below is the module's state. @internal */
|
|
563
|
+
readonly events = new Map<string, Handler[]>();
|
|
564
|
+
readonly subscriptions = new Map<string, { handler: Handler; tag: number }[]>();
|
|
565
|
+
readonly slots: Slot[] = [];
|
|
566
|
+
readonly clientCommands = new Map<string, Slot[]>();
|
|
567
|
+
readonly serverCommands = new Map<string, Slot[]>();
|
|
568
|
+
readonly hookchains = new Map<string, { pre: Slot[]; post: Slot[] }>();
|
|
569
|
+
readonly tasks: Task[] = [];
|
|
570
|
+
readonly exported = new Map<string, Handler>();
|
|
571
|
+
readonly cvars = new Map<string, Cvar>();
|
|
572
|
+
readonly vaultHandles: string[] = [];
|
|
573
|
+
readonly forwardHandles: { name: string; types: number[] }[] = [];
|
|
574
|
+
hud = { color: [200, 100, 0], x: -1, y: 0.35, hold: 12, channel: -1 };
|
|
575
|
+
|
|
576
|
+
/** The game rules' members a plugin set (set_member_game), by member. */
|
|
577
|
+
readonly rules = new Map<number, number>();
|
|
578
|
+
/** Rounds a plugin ended with rg_round_end, in order. */
|
|
579
|
+
readonly roundEnds: { status: number; event: number; delay: number; message: string; sound: string; trigger: boolean }[] = [];
|
|
580
|
+
/** Sounds played: from an entity (emit_sound, rh_emit_sound2), or to a player alone (SendAudio, rg_send_audio). */
|
|
581
|
+
readonly sounds: { entity: number; sample: string }[] = [];
|
|
582
|
+
/**
|
|
583
|
+
* User messages plugins sent, in order: the message's name, the player it
|
|
584
|
+
* went to (0 for everyone), and what was written, write_* by write_*.
|
|
585
|
+
*/
|
|
586
|
+
readonly userMessages: UserMessage[] = [];
|
|
587
|
+
/** The ids get_user_msgid handed out, by name - ShowMenu's is the game's 96. @internal */
|
|
588
|
+
readonly messageIds = new Map<string, number>([['ShowMenu', 96]]);
|
|
589
|
+
/** The message between message_begin and message_end. @internal */
|
|
590
|
+
writing: UserMessage | null = null;
|
|
591
|
+
/** Told of every message as it ends: the menu kit reads ShowMenu here. @internal */
|
|
592
|
+
readonly messageListeners: ((message: UserMessage) => void)[] = [];
|
|
593
|
+
/** resemiclip's masks, by player: bit N set - player N+1 is not solid to him. */
|
|
594
|
+
readonly semiclipMasks = new Map<number, number>();
|
|
595
|
+
/** Whether a plugin took resemiclip's rules over (resemiclip_take_control). */
|
|
596
|
+
semiclipControlled = false;
|
|
597
|
+
/** The progress bar each player was last shown (rg_send_bartime), in seconds. */
|
|
598
|
+
readonly bartimes = new Map<number, number>();
|
|
599
|
+
/** RegisterHam's callbacks, by `<function id>:<class>:pre|post`. */
|
|
600
|
+
readonly hams = new Map<string, Slot[]>();
|
|
601
|
+
/**
|
|
602
|
+
* The entity functions plugins ran - `weapon.deploy()`, `entity.use(...)` -
|
|
603
|
+
* in order: the Ham_* constant's number, the entity, the arguments after it
|
|
604
|
+
* as cells, and whether every plugin's listeners ran on it (ExecuteHamB).
|
|
605
|
+
*/
|
|
606
|
+
readonly hamCalls: { fn: number; entity: number; args: number[]; hooks: boolean }[] = [];
|
|
607
|
+
/** register_message's callbacks, by the message's id. */
|
|
608
|
+
readonly messageHooks = new Map<number, Slot[]>();
|
|
609
|
+
/** The message register_message's callbacks are reading and writing: its arguments and their ARG_* types. @internal */
|
|
610
|
+
hookedMessage: { args: (number | string)[]; types: number[] } | null = null;
|
|
611
|
+
/** register_touch's callbacks. */
|
|
612
|
+
readonly touches: { touched: string; toucher: string; slot: Slot }[] = [];
|
|
613
|
+
/** query_client_cvar's questions, waiting for answerCvar(). */
|
|
614
|
+
readonly cvarQueries: { player: number; cvar: string; slot: Slot }[] = [];
|
|
615
|
+
/** What precache_model and precache_sound were asked for, in order: the index is the place + 1. */
|
|
616
|
+
readonly precached: string[] = [];
|
|
617
|
+
/**
|
|
618
|
+
* The fields plugins add to Player, by slot and field name, as the module
|
|
619
|
+
* keeps them: a number
|
|
620
|
+
* (a boolean is 1 or 0) or a text. Cleared when a player leaves.
|
|
621
|
+
*/
|
|
622
|
+
readonly playerData = new Map<number, Map<string, number | string>>();
|
|
623
|
+
/** Which of those texts are lists of player ids ("3,5"), by `slot:key`: a player who leaves goes from them. */
|
|
624
|
+
readonly playerLists = new Set<string>();
|
|
625
|
+
/**
|
|
626
|
+
* The playerchange listeners, as the module keeps them: per plugin, the
|
|
627
|
+
* fields it hears ("" for every one) and the trampoline that hands a change
|
|
628
|
+
* to its listeners.
|
|
629
|
+
*/
|
|
630
|
+
readonly fieldListeners: { plugin: PluginInstance; fn: number; fields: string[] }[] = [];
|
|
631
|
+
/** The change being told, which the player_change_* imports read. @internal */
|
|
632
|
+
fieldChange: { key: string; previous: number | string | undefined; value: number | string } | null = null;
|
|
633
|
+
|
|
634
|
+
/** The arguments of the callback that is running, for arg() and arg_text(). */
|
|
635
|
+
callArgs: ArgValue[] | null = null;
|
|
636
|
+
/** The command being handled, for read_argv. */
|
|
637
|
+
argv: string[] = [];
|
|
638
|
+
chain: Chain | null = null;
|
|
639
|
+
current: PluginInstance | null = null;
|
|
640
|
+
private outcome = 0;
|
|
641
|
+
private outcomeSaid = false;
|
|
642
|
+
private taskOrder = 0;
|
|
643
|
+
private hookHandles = 0;
|
|
644
|
+
private entityIds: number;
|
|
645
|
+
|
|
646
|
+
constructor(options: ServerOptions = {}) {
|
|
647
|
+
this.map = options.map ?? 'de_dust2';
|
|
648
|
+
this.maxPlayers = options.maxPlayers ?? 32;
|
|
649
|
+
this.modules = new Set(options.modules ?? ['reapi', 'cstrike', 'fun', 'hamsandwich', 'engine', 'fakemeta', 'nvault', 'resemiclip']);
|
|
650
|
+
this.entityIds = this.maxPlayers + 1;
|
|
651
|
+
this.timeZone = options.timeZone;
|
|
652
|
+
// AMX Mod X's own, which a test may set: the languages.
|
|
653
|
+
const cvars = { amx_language: 'en', amx_client_languages: '1', ...options.cvars };
|
|
654
|
+
for (const [name, value] of Object.entries(cvars)) this.createCvar(name, value);
|
|
655
|
+
for (const [path, text] of Object.entries(options.files ?? {})) this.writeFile(path, text);
|
|
656
|
+
if (options.platform === 'win32') this.writeFile('addons/amxmodx/modules/amxts_amxx.dll', '');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The test kits of the module packages loaded here, by package name: what
|
|
661
|
+
* each kit's install() gave back (defineTestKit). @internal
|
|
662
|
+
*/
|
|
663
|
+
readonly kits = new Map<string, unknown>();
|
|
664
|
+
/** Natives a test kit answers on this server, over the fake's own. */
|
|
665
|
+
private readonly ownNatives = new Map<string, Native>();
|
|
666
|
+
/** The `export function` natives of the loaded plugins, by name: the first plugin's where two export one. */
|
|
667
|
+
private readonly pluginNatives = new Map<string, PluginNative>();
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Answers a native on this server - for a module's test kit, which adds
|
|
671
|
+
* what its plugin calls (a menu's show_menu, callfunc into Pawn). Before
|
|
672
|
+
* the plugins load: a plugin is given its natives as it loads.
|
|
673
|
+
*/
|
|
674
|
+
defineNative(name: string, native: Native): void {
|
|
675
|
+
this.ownNatives.set(name, native);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** Whether a native is answered here: by a test kit, or by the fake itself. @internal */
|
|
679
|
+
hasNative(name: string): boolean {
|
|
680
|
+
return this.ownNatives.has(name) || Object.hasOwn(NATIVES, name);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Loads a plugin: its top level runs now, its init when start() is called.
|
|
685
|
+
* A module package by name - "@amxts/menu-core" - is its plugin, and its
|
|
686
|
+
* test kit, when it has one, is installed first.
|
|
687
|
+
*/
|
|
688
|
+
async load(source: string): Promise<PluginInstance> {
|
|
689
|
+
await installKitFor(this, source);
|
|
690
|
+
const { module, natives, binary, async } = await compile(source);
|
|
691
|
+
const plugin = new PluginInstance(this, source, module, natives, async ? binary : undefined);
|
|
692
|
+
this.plugins.push(plugin);
|
|
693
|
+
for (const native of natives) {
|
|
694
|
+
if (!this.pluginNatives.has(native.name)) this.pluginNatives.set(native.name, native);
|
|
695
|
+
}
|
|
696
|
+
plugin.start();
|
|
697
|
+
return plugin;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** plugin_precache, plugin_init and plugin_cfg, as a map start sends them. */
|
|
701
|
+
start(): void {
|
|
702
|
+
this.fire('plugin_precache');
|
|
703
|
+
this.fire('plugin_init');
|
|
704
|
+
this.fire('plugin_cfg');
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ------------------------------------------------------------ what a test reads
|
|
708
|
+
|
|
709
|
+
/** Everything logged, one line a line. */
|
|
710
|
+
get log(): string {
|
|
711
|
+
return this.logLines.join('\n');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** The players on the server. */
|
|
715
|
+
get players(): FakePlayer[] {
|
|
716
|
+
return [...this.entities.values()].filter((e): e is FakePlayer => e instanceof FakePlayer && e.connected);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
player(id: number): FakePlayer | undefined {
|
|
720
|
+
const entity = this.entities.get(id);
|
|
721
|
+
return entity instanceof FakePlayer && entity.connected ? entity : undefined;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** A cvar's value, or undefined when the server has no such cvar. */
|
|
725
|
+
cvar(name: string): string | undefined {
|
|
726
|
+
return this.cvars.get(name.toLowerCase())?.value;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Sets a cvar as the console would: `mp_freezetime 5`, and its change listeners hear it. */
|
|
730
|
+
setCvar(name: string, value: string | number): void {
|
|
731
|
+
const cvar = this.cvars.get(name.toLowerCase()) ?? this.createCvar(name, '');
|
|
732
|
+
this.changeCvar(cvar, String(value));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** A file's text, or undefined when there is no such file. */
|
|
736
|
+
file(path: string): string | undefined {
|
|
737
|
+
const bytes = this.files.get(normalizePath(path));
|
|
738
|
+
return bytes && new TextDecoder().decode(bytes);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** Puts a file in the game folder, as UTF-8. */
|
|
742
|
+
writeFile(path: string, text: string): void {
|
|
743
|
+
this.files.set(normalizePath(path), new TextEncoder().encode(text));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ------------------------------------------------------------ dictionaries
|
|
747
|
+
|
|
748
|
+
/** The dictionaries' lines by language, then key: what register_dictionary loaded and translate() added. */
|
|
749
|
+
readonly dictionary = new Map<string, Map<string, string>>();
|
|
750
|
+
/** Every key in the order it came, as GetLangTransKey numbers them. */
|
|
751
|
+
private readonly langKeys: string[] = [];
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Lines as a dictionary file writes them - `^n` a line break, `^4` chat's
|
|
755
|
+
* green, `\y` a menu's yellow: `server.translate({ MYPLUGIN_HELLO: "Hello, %s" })`,
|
|
756
|
+
* `server.translate({ ... }, "ru")`.
|
|
757
|
+
*/
|
|
758
|
+
translate(lines: Record<string, string>, language = 'en'): void {
|
|
759
|
+
for (const [key, text] of Object.entries(lines)) this.addLangLine(language, key, text);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Reads `addons/amxmodx/data/lang/<file>` as register_dictionary does:
|
|
764
|
+
* `[en]` sections of `KEY = text` lines. False when there is no such file.
|
|
765
|
+
*/
|
|
766
|
+
loadDictionary(file: string): boolean {
|
|
767
|
+
const text = this.file(`addons/amxmodx/data/lang/${file}`);
|
|
768
|
+
if (text === undefined) return false;
|
|
769
|
+
let language = '';
|
|
770
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
771
|
+
const line = raw.trim();
|
|
772
|
+
const section = line.match(/^\[(\w+)\]$/);
|
|
773
|
+
const at = line.indexOf('=');
|
|
774
|
+
if (section) language = section[1].toLowerCase();
|
|
775
|
+
else if (language && at > 0 && !line.startsWith(';')) this.addLangLine(language, line.slice(0, at).trim(), line.slice(at + 1).trim());
|
|
776
|
+
}
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* The key's line for a player (`0`: the server), as LookupLangKey finds
|
|
782
|
+
* it: in the player's language, then the server's, then English.
|
|
783
|
+
*/
|
|
784
|
+
lookupLang(key: string, id: number): string | undefined {
|
|
785
|
+
const own = id > 0 ? this.languageOf(id) : undefined;
|
|
786
|
+
for (const language of [own, this.cvar('amx_language') || 'en', 'en']) {
|
|
787
|
+
const text = language ? this.dictionary.get(language)?.get(key) : undefined;
|
|
788
|
+
if (text !== undefined) return text;
|
|
789
|
+
}
|
|
790
|
+
return undefined;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* The language AMX Mod X reads a player's text in (playerlang): his `lang`
|
|
795
|
+
* setinfo while `amx_client_languages` is on and it starts with a letter,
|
|
796
|
+
* `amx_language` otherwise.
|
|
797
|
+
*/
|
|
798
|
+
languageOf(id: number): string {
|
|
799
|
+
const own = this.cvar('amx_client_languages') !== '0' ? this.player(id)?.info.get('lang') ?? '' : '';
|
|
800
|
+
return /^[a-z]/i.test(own) ? own : this.cvar('amx_language') ?? 'en';
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** The key's number, as GetLangTransKey gives it; -1 when no dictionary has it. */
|
|
804
|
+
langKey(key: string): number {
|
|
805
|
+
return this.langKeys.indexOf(key);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** One line, with the escapes AMX Mod X turns into characters as it loads a file. */
|
|
809
|
+
private addLangLine(language: string, key: string, text: string): void {
|
|
810
|
+
const escapes: Record<string, string> = { n: '\n', t: '\t', 1: '\x01', 2: '\x02', 3: '\x03', 4: '\x04' };
|
|
811
|
+
const lines = this.dictionary.get(language) ?? new Map<string, string>();
|
|
812
|
+
lines.set(key, text.replace(/\^([nt1-4])/g, (_, code: string) => escapes[code]));
|
|
813
|
+
this.dictionary.set(language, lines);
|
|
814
|
+
if (!this.langKeys.includes(key)) this.langKeys.push(key);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Whether a folder is there: named in `folders`, or holding a file. */
|
|
818
|
+
folderExists(path: string): boolean {
|
|
819
|
+
const folder = normalizePath(path);
|
|
820
|
+
if (folder === '' || this.folders.has(folder)) return true;
|
|
821
|
+
for (const file of this.files.keys()) {
|
|
822
|
+
if (file.startsWith(`${folder}/`)) return true;
|
|
823
|
+
}
|
|
824
|
+
for (const known of this.folders) {
|
|
825
|
+
if (known.startsWith(`${folder}/`)) return true;
|
|
826
|
+
}
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** ArrayCreate. @internal */
|
|
831
|
+
createCellArray(cellSize: number): number {
|
|
832
|
+
const handle = this.nextCellArray++;
|
|
833
|
+
this.cellArrays.set(handle, { cellSize, items: [] });
|
|
834
|
+
return handle;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** A dynamic array's items as text (ArrayGetString), or null for a handle that is not there - Invalid_Array is 0. */
|
|
838
|
+
cellArrayStrings(handle: number): string[] | null {
|
|
839
|
+
const array = this.cellArrays.get(handle);
|
|
840
|
+
if (!array) return null;
|
|
841
|
+
return array.items.map((item) => {
|
|
842
|
+
const end = item.indexOf(0);
|
|
843
|
+
return new TextDecoder().decode(Uint8Array.from(end < 0 ? item : item.slice(0, end)));
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** A dynamic array's items as cells - each item's cells when it holds more than one - or null. */
|
|
848
|
+
cellArrayCells(handle: number): (number | number[])[] | null {
|
|
849
|
+
const array = this.cellArrays.get(handle);
|
|
850
|
+
if (!array) return null;
|
|
851
|
+
return array.items.map(item => array.cellSize === 1 ? item[0] : [...item]);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/** A storage's nVault, as a Map a test can fill before the plugin reads it. */
|
|
855
|
+
vault(name: string): Map<string, string> {
|
|
856
|
+
let vault = this.vaults.get(name);
|
|
857
|
+
if (!vault) this.vaults.set(name, vault = new Map());
|
|
858
|
+
return vault;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// ------------------------------------------------------------ what a test does
|
|
862
|
+
|
|
863
|
+
/** A player connects: client_connect, client_authorized, client_putinserver. */
|
|
864
|
+
join(name: string, options: JoinOptions = {}): FakePlayer {
|
|
865
|
+
let id = 1;
|
|
866
|
+
while (id <= this.maxPlayers && this.player(id)) id++;
|
|
867
|
+
if (id > this.maxPlayers) throw new Error(`the server is full (${this.maxPlayers} slots)`);
|
|
868
|
+
|
|
869
|
+
const player = new FakePlayer(this, id, name, options);
|
|
870
|
+
this.entities.set(id, player);
|
|
871
|
+
|
|
872
|
+
this.fire('client_connect', id);
|
|
873
|
+
this.fire('client_authorized', id, player.authid);
|
|
874
|
+
this.fire('client_putinserver', id);
|
|
875
|
+
return player;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Moves the clock forward and runs every timer that comes due on the way,
|
|
880
|
+
* in order - a repeating one as many times as it fits.
|
|
881
|
+
*/
|
|
882
|
+
advance(ms: number): void {
|
|
883
|
+
const until = this.time + ms;
|
|
884
|
+
|
|
885
|
+
for (;;) {
|
|
886
|
+
const due = this.tasks
|
|
887
|
+
.filter(t => t.due <= until)
|
|
888
|
+
.sort((a, b) => a.due - b.due || a.order - b.order)[0];
|
|
889
|
+
if (!due) break;
|
|
890
|
+
|
|
891
|
+
this.time = due.due;
|
|
892
|
+
if (due.repeat) due.due += due.interval;
|
|
893
|
+
else this.tasks.splice(this.tasks.indexOf(due), 1);
|
|
894
|
+
|
|
895
|
+
this.withCallArgs([due.id], () => this.call(due.slot, [due.id], 0));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
this.time = until;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Raises a forward the host plugin relays - "client_putinserver",
|
|
903
|
+
* "plugin_cfg" - or one a Pawn plugin sends: every handler and every
|
|
904
|
+
* Forward.subscribe() of that name hears it. Text arrives as text, a
|
|
905
|
+
* boolean as 1 or 0, an array as its cells (a Float's as its bits). As in
|
|
906
|
+
* the module, a listener reads every argument from the call's context and
|
|
907
|
+
* is handed the first cell. Returns the highest answer, as the module does.
|
|
908
|
+
*/
|
|
909
|
+
fire(name: string, ...args: ArgValue[]): number {
|
|
910
|
+
const first = typeof args[0] === 'number' || typeof args[0] === 'boolean' ? Number(args[0]) : 0;
|
|
911
|
+
return this.withCallArgs(args, () => {
|
|
912
|
+
this.deliver(name);
|
|
913
|
+
|
|
914
|
+
let result = 0;
|
|
915
|
+
for (const handler of [...(this.events.get(name) ?? [])]) {
|
|
916
|
+
const one = this.call(handler, [first], 0);
|
|
917
|
+
if (one > result) result = one;
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Every Forward.subscribe() of a forward, called with its tag; the arguments are the context's. */
|
|
924
|
+
private deliver(forward: string): void {
|
|
925
|
+
for (const { handler, tag } of [...(this.subscriptions.get(forward) ?? [])]) this.call(handler, [tag], 0);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Runs a reapi hookchain the way the game would: the pre listeners, then
|
|
930
|
+
* the game's own function unless one of them stopped it, then the post
|
|
931
|
+
* listeners.
|
|
932
|
+
*
|
|
933
|
+
* ```ts
|
|
934
|
+
* server.fireHook("takeDamage", [victim.id, 0, attacker.id, 30.0, 2]);
|
|
935
|
+
* server.fireHook("flPlayerFallDamage", [player.id], { result: 40 });
|
|
936
|
+
* ```
|
|
937
|
+
*
|
|
938
|
+
* `event` is the name game.addEventListener takes, or reapi's short one
|
|
939
|
+
* ("take_damage"). `args` are the chain's arguments in order; a float is
|
|
940
|
+
* written as a number and goes as one. `result` is what the game's own
|
|
941
|
+
* function answers when it runs - what a post listener reads as
|
|
942
|
+
* event.result.
|
|
943
|
+
*/
|
|
944
|
+
fireHook(event: string, args: Value[] = [], options: { result?: Value } = {}): HookResult {
|
|
945
|
+
const shape = tables().hooks.get(event);
|
|
946
|
+
if (!shape) throw new Error(`no hookchain named "${event}" in as/hooks.ts`);
|
|
947
|
+
|
|
948
|
+
const cells = args.map((a, i) => typeof a === 'string'
|
|
949
|
+
? a
|
|
950
|
+
: typeof a === 'boolean'
|
|
951
|
+
? (a ? 1 : 0)
|
|
952
|
+
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
953
|
+
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
954
|
+
|
|
955
|
+
const nothing: Value = shape.answer === constant('ATYPE_BOOL') ? false : shape.answer === constant('ATYPE_STRING') ? '' : 0;
|
|
956
|
+
const chain: Chain = { shape, answer: nothing, args: [...args] };
|
|
957
|
+
const listeners = this.hookchains.get(shape.kind) ?? { pre: [], post: [] };
|
|
958
|
+
|
|
959
|
+
const previous = this.chain;
|
|
960
|
+
this.chain = chain;
|
|
961
|
+
|
|
962
|
+
try {
|
|
963
|
+
let prevented = false;
|
|
964
|
+
let broken = false;
|
|
965
|
+
|
|
966
|
+
for (const slot of [...listeners.pre]) {
|
|
967
|
+
const said = this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
968
|
+
// What reapi answers a plugin that blocks a chain with a return value
|
|
969
|
+
// before setting one - a run time error, and the block does not hold.
|
|
970
|
+
if ((said === HC_SUPERCEDE || said === HC_BREAK) && shape.answer >= 0 && !chain.answered) {
|
|
971
|
+
throw new Error(`${event}: Can't suppress original function call without new return value set, so you must call SetHookChainReturn.`);
|
|
972
|
+
}
|
|
973
|
+
if (said === HC_SUPERCEDE) prevented = true;
|
|
974
|
+
if (said === HC_BREAK) {
|
|
975
|
+
prevented = broken = true;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!prevented) chain.answer = options.result ?? nothing;
|
|
981
|
+
|
|
982
|
+
if (!broken) {
|
|
983
|
+
for (const slot of [...listeners.post]) {
|
|
984
|
+
this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return { prevented, result: chain.answer, args: chain.args };
|
|
989
|
+
} finally {
|
|
990
|
+
this.chain = previous;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Sends a message to a client the way the game would, through the
|
|
996
|
+
* register_message callbacks of its name: they read and change its
|
|
997
|
+
* arguments, and may stop it.
|
|
998
|
+
*
|
|
999
|
+
* ```ts
|
|
1000
|
+
* server.sendMessage("TextMsg", [3, "#Round_Draw"], { player: alice });
|
|
1001
|
+
* const sent = server.sendMessage("RoundTime", [120]); // sent.args: [90] with a listener that writes 90
|
|
1002
|
+
* ```
|
|
1003
|
+
*
|
|
1004
|
+
* A text argument is a string; a whole number is written as a byte, a
|
|
1005
|
+
* fraction as a coordinate, unless `types` gives each argument's ARG_*.
|
|
1006
|
+
* Returns whether a callback stopped it, and the arguments as they left.
|
|
1007
|
+
*/
|
|
1008
|
+
sendMessage(name: string, args: (number | string)[], options: { player?: FakePlayer; types?: number[] } = {}): { prevented: boolean; args: (number | string)[] } {
|
|
1009
|
+
const id = this.messageIds.get(name);
|
|
1010
|
+
const message = {
|
|
1011
|
+
args: [...args],
|
|
1012
|
+
types: options.types ?? args.map(a => typeof a === 'string' ? ARG_STRING : Number.isInteger(a) ? ARG_BYTE : ARG_COORD),
|
|
1013
|
+
};
|
|
1014
|
+
const receiver = options.player?.id ?? 0;
|
|
1015
|
+
const previous = this.hookedMessage;
|
|
1016
|
+
this.hookedMessage = message;
|
|
1017
|
+
try {
|
|
1018
|
+
let prevented = false;
|
|
1019
|
+
for (const slot of [...(id !== undefined ? this.messageHooks.get(id) ?? [] : [])]) {
|
|
1020
|
+
const handed = [id ?? 0, receiver ? 1 : 0, receiver, 0];
|
|
1021
|
+
if (this.withCallArgs(handed, () => this.call(slot, handed, 0)) === PLUGIN_HANDLED) prevented = true;
|
|
1022
|
+
}
|
|
1023
|
+
return { prevented, args: message.args };
|
|
1024
|
+
} finally {
|
|
1025
|
+
this.hookedMessage = previous;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Runs one entity's function the way the game would, through the Ham
|
|
1031
|
+
* Sandwich hooks registered for its class: the pre listeners, then the
|
|
1032
|
+
* game's own function unless one of them blocked it, then the post
|
|
1033
|
+
* listeners.
|
|
1034
|
+
*
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* server.fireHam("primaryAttack", knife);
|
|
1037
|
+
* server.fireHam("takeDamage", box, [0, alice.id, 30.0, 2], { result: 1 });
|
|
1038
|
+
* ```
|
|
1039
|
+
*
|
|
1040
|
+
* `event` is the name game.addEventListener takes; `args` are the
|
|
1041
|
+
* function's arguments after the entity, a float as a number and a vector
|
|
1042
|
+
* as an array of three.
|
|
1043
|
+
*/
|
|
1044
|
+
fireHam(event: string, entity: FakeEntity | number, args: Value[] = [], options: { result?: Value } = {}): HookResult {
|
|
1045
|
+
const shape = tables().hooks.get(event);
|
|
1046
|
+
if (shape?.ham === undefined) throw new Error(`no Ham Sandwich function under the event "${event}" in as/hooks.ts`);
|
|
1047
|
+
return this.runHam(shape, typeof entity === 'number' ? entity : entity.id, args, options.result);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* fireHam's work, and ExecuteHamB's: `args` without the entity.
|
|
1052
|
+
* @internal
|
|
1053
|
+
*/
|
|
1054
|
+
runHam(shape: HookShape, id: number, args: (Value | number[])[], result?: Value): HookResult {
|
|
1055
|
+
const classname = this.entities.get(id)?.classname ?? (id >= 1 && id <= this.maxPlayers ? 'player' : '');
|
|
1056
|
+
const all = [id, ...args];
|
|
1057
|
+
const cells: ArgValue[] = all.map((a, i) => Array.isArray(a)
|
|
1058
|
+
? a.map(floatBits)
|
|
1059
|
+
: typeof a === 'string'
|
|
1060
|
+
? a
|
|
1061
|
+
: typeof a === 'boolean'
|
|
1062
|
+
? (a ? 1 : 0)
|
|
1063
|
+
: shape.floats.has(i) ? floatBits(a) : a | 0);
|
|
1064
|
+
const handed = cells.slice(0, 4).map(c => typeof c === 'number' ? c : 0);
|
|
1065
|
+
const nothing: Value = shape.answer === constant('ATYPE_STRING') ? '' : 0;
|
|
1066
|
+
const chain: Chain = { shape, answer: nothing, args: all.map(a => Array.isArray(a) ? 0 : a) };
|
|
1067
|
+
const listeners = (post: boolean) => this.hams.get(`${shape.ham}:${classname}:${post ? 'post' : 'pre'}`) ?? [];
|
|
1068
|
+
|
|
1069
|
+
const previous = this.chain;
|
|
1070
|
+
this.chain = chain;
|
|
1071
|
+
|
|
1072
|
+
try {
|
|
1073
|
+
let prevented = false;
|
|
1074
|
+
for (const slot of [...listeners(false)]) {
|
|
1075
|
+
if (this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback)) >= HAM_SUPERCEDE) prevented = true;
|
|
1076
|
+
}
|
|
1077
|
+
if (!prevented) chain.answer = result ?? nothing;
|
|
1078
|
+
for (const slot of [...listeners(true)]) this.withCallArgs(cells, () => this.call(slot, handed, slot.fallback));
|
|
1079
|
+
return { prevented, result: chain.answer, args: chain.args };
|
|
1080
|
+
} finally {
|
|
1081
|
+
this.chain = previous;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* One of the module's own natives for Pawn plugins, as a Pawn plugin calls
|
|
1087
|
+
* it: the amxts_*_player_data natives over the fields plugins add to Player
|
|
1088
|
+
* (runtime/host/amxts.inc).
|
|
1089
|
+
*
|
|
1090
|
+
* ```ts
|
|
1091
|
+
* server.amxtsNative("amxts_get_player_data", bot.id, "spawnProtected"); // 1
|
|
1092
|
+
* server.amxtsNative("amxts_set_player_data_string", bot.id, "tag", "x");
|
|
1093
|
+
* ```
|
|
1094
|
+
*
|
|
1095
|
+
* A number comes back whole, a Float as a number, a text as the text
|
|
1096
|
+
* `out[]` holds (`len` is the last argument, 4095 when left out).
|
|
1097
|
+
*/
|
|
1098
|
+
amxtsNative(name: string, ...args: Value[]): number | string {
|
|
1099
|
+
const [id, key, value] = [Number(args[0]), String(args[1]), args[2]];
|
|
1100
|
+
switch (name) {
|
|
1101
|
+
case 'amxts_get_player_data': return Math.trunc(this.playerNumber(id, key)) | 0;
|
|
1102
|
+
case 'amxts_set_player_data':
|
|
1103
|
+
this.setPlayerData(id, key, Math.trunc(Number(value)) | 0);
|
|
1104
|
+
return 1;
|
|
1105
|
+
case 'amxts_get_player_data_float': return bitsFloat(floatBits(this.playerNumber(id, key)));
|
|
1106
|
+
case 'amxts_set_player_data_float':
|
|
1107
|
+
this.setPlayerData(id, key, bitsFloat(floatBits(Number(value))));
|
|
1108
|
+
return 1;
|
|
1109
|
+
case 'amxts_get_player_data_string': {
|
|
1110
|
+
const bytes = new TextEncoder().encode(this.playerText(id, key));
|
|
1111
|
+
const length = utf8Fit(bytes, Math.max(0, value === undefined ? 4095 : Number(value)));
|
|
1112
|
+
return new TextDecoder().decode(bytes.subarray(0, length));
|
|
1113
|
+
}
|
|
1114
|
+
case 'amxts_set_player_data_string':
|
|
1115
|
+
this.setPlayerData(id, key, String(value ?? ''));
|
|
1116
|
+
return 1;
|
|
1117
|
+
}
|
|
1118
|
+
throw new Error(`the module has no native named "${name}"`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** A Player field's number: 0 when never written, or a text. @internal */
|
|
1122
|
+
playerNumber(id: number, key: string): number {
|
|
1123
|
+
const value = this.playerData.get(id)?.get(key);
|
|
1124
|
+
return typeof value === 'number' ? value : 0;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** A Player field's text: "" when never written, or a number. @internal */
|
|
1128
|
+
playerText(id: number, key: string): string {
|
|
1129
|
+
const value = this.playerData.get(id)?.get(key);
|
|
1130
|
+
return typeof value === 'string' ? value : '';
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** Writes a Player field, as the module does: only for a slot, and a change is told. @internal */
|
|
1134
|
+
setPlayerData(id: number, key: string, value: number | string, players = false): void {
|
|
1135
|
+
if (id < 0 || id > 32) return;
|
|
1136
|
+
const previous = this.playerData.get(id)?.get(key);
|
|
1137
|
+
const changed = typeof value === 'number'
|
|
1138
|
+
? this.playerNumber(id, key) !== value || this.playerText(id, key) !== ''
|
|
1139
|
+
: this.playerText(id, key) !== value || this.playerNumber(id, key) !== 0;
|
|
1140
|
+
const list = `${id}:${key}`;
|
|
1141
|
+
if (typeof value === 'number') this.playerLists.delete(list);
|
|
1142
|
+
else if (players) this.playerLists.add(list);
|
|
1143
|
+
if (!this.playerData.has(id)) this.playerData.set(id, new Map());
|
|
1144
|
+
this.playerData.get(id)!.set(key, value);
|
|
1145
|
+
if (changed) this.fieldChanged(id, key, previous);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Every plugin listening for `key` hears it changed on player `id`, once, as the module tells them. */
|
|
1149
|
+
private fieldChanged(id: number, key: string, previous: number | string | undefined): void {
|
|
1150
|
+
if (this.fieldListeners.length === 0) return;
|
|
1151
|
+
const outer = this.fieldChange;
|
|
1152
|
+
this.fieldChange = { key, previous, value: this.playerData.get(id)!.get(key)! };
|
|
1153
|
+
try {
|
|
1154
|
+
for (const { plugin, fn, fields } of [...this.fieldListeners]) {
|
|
1155
|
+
if (fields.some(field => field === '' || key === field || key.startsWith(`${field}.`))) this.call({ plugin, fn, shape: SHAPE_NARROW }, [id], 0);
|
|
1156
|
+
}
|
|
1157
|
+
} finally {
|
|
1158
|
+
this.fieldChange = outer;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** A player left: his fields go, and he goes from everyone's lists of players. Nobody is told. @internal */
|
|
1163
|
+
clearPlayerData(id: number): void {
|
|
1164
|
+
for (const key of this.playerData.get(id)?.keys() ?? []) this.playerLists.delete(`${id}:${key}`);
|
|
1165
|
+
this.playerData.delete(id);
|
|
1166
|
+
|
|
1167
|
+
for (const list of this.playerLists) {
|
|
1168
|
+
const [slot, key] = [Number(list.slice(0, list.indexOf(':'))), list.slice(list.indexOf(':') + 1)];
|
|
1169
|
+
const ids = this.playerText(slot, key);
|
|
1170
|
+
this.playerData.get(slot)!.set(key, ids.split(',').filter(one => one !== '' && Number(one) !== id).join(','));
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Calls a native a plugin exported with nativeFn, as another plugin would.
|
|
1176
|
+
* Returns what it gave back with ret(); a `setArg` it made lands in `args`.
|
|
1177
|
+
*/
|
|
1178
|
+
callNative(name: string, ...args: ArgValue[]): number {
|
|
1179
|
+
const handler = this.exported.get(name);
|
|
1180
|
+
if (!handler) throw new Error(`no plugin exports a native named "${name}"`);
|
|
1181
|
+
const handed = args.slice(0, 4).map(a => typeof a === 'number' ? a : typeof a === 'boolean' ? +a : 0);
|
|
1182
|
+
return this.withCallArgs(args, () => this.call(handler, handed, 0));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Calls a plugin's own native - an `export function` of its entry file -
|
|
1187
|
+
* the way a Pawn plugin would, with its `.inc` signature: text as a Pawn
|
|
1188
|
+
* string, a `Float` as its bits, an array followed by its size, and an
|
|
1189
|
+
* out-buffer for a string or array result. Returns the result as Pawn
|
|
1190
|
+
* reads it back: the text in `out[]` for a string (null for a `string |
|
|
1191
|
+
* null` that returned false), the cells written for an array, a float for
|
|
1192
|
+
* a `Float`, a boolean for a `boolean`.
|
|
1193
|
+
*/
|
|
1194
|
+
native(name: string, ...args: ArgValue[]): NativeResult {
|
|
1195
|
+
return this.nativeWithRoom(name, 4095, args);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** native(), with an out-buffer of `room` cells: `charsmax(out)` for a string, `max` for an array. */
|
|
1199
|
+
nativeWithRoom(name: string, room: number, args: ArgValue[]): NativeResult {
|
|
1200
|
+
const handler = this.exported.get(name);
|
|
1201
|
+
const native = this.pluginNatives.get(name);
|
|
1202
|
+
if (!handler || !native) throw new Error(`no plugin exports a native named "${name}" with export function`);
|
|
1203
|
+
|
|
1204
|
+
// The .inc's order: the result's buffer sits before the first parameter
|
|
1205
|
+
// with a default. A parameter left out takes its default, as Pawn fills it in.
|
|
1206
|
+
// A native that implements an include lays them out as its declaration
|
|
1207
|
+
// does: out-arguments where it puts them, a tail passed by address.
|
|
1208
|
+
const layout = pawnLayout(native);
|
|
1209
|
+
const frame: ArgValue[] = [];
|
|
1210
|
+
const outputs: { at: number; kind: string }[] = [];
|
|
1211
|
+
let out = -1;
|
|
1212
|
+
const fixed = layout.filter(slot => 'param' in slot && !slot.byRef).length;
|
|
1213
|
+
for (const slot of layout) {
|
|
1214
|
+
if ('out' in slot) {
|
|
1215
|
+
out = frame.length;
|
|
1216
|
+
if (native.result === 'string' || native.result === 'string?') frame.push('', room);
|
|
1217
|
+
else frame.push([], room);
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
if ('output' in slot) {
|
|
1221
|
+
outputs.push({ at: frame.length, kind: slot.output });
|
|
1222
|
+
if (slot.output === 'string') frame.push('', room);
|
|
1223
|
+
else if (slot.output === 'ints' || slot.output === 'floats') frame.push([], room);
|
|
1224
|
+
else frame.push([0]);
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if ('format' in slot) {
|
|
1228
|
+
for (const value of args.slice(fixed)) {
|
|
1229
|
+
frame.push(typeof value === 'number' ? [value] : typeof value === 'boolean' ? [+value] : value);
|
|
1230
|
+
}
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const { param, index, byRef } = slot;
|
|
1234
|
+
if (byRef) {
|
|
1235
|
+
// A tail parameter: only what the test passed.
|
|
1236
|
+
const value = args[index];
|
|
1237
|
+
if (index < args.length) frame.push(typeof value === 'string' ? value : [Number(value)]);
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const value = index < args.length ? args[index] : pawnDefaultValue(param.pawnDefault);
|
|
1241
|
+
if (param.kind === 'float') {
|
|
1242
|
+
frame.push(floatBits(Number(value)));
|
|
1243
|
+
} else if (param.kind === 'vector') {
|
|
1244
|
+
frame.push((value as number[]).map(floatBits));
|
|
1245
|
+
} else if (param.kind === 'ints' || param.kind === 'floats') {
|
|
1246
|
+
const list = value as number[];
|
|
1247
|
+
frame.push(param.kind === 'floats' ? list.map(floatBits) : [...list], list.length);
|
|
1248
|
+
} else {
|
|
1249
|
+
frame.push(value as Value);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const cell = this.withCallArgs(frame, () => this.call(handler, [], 0));
|
|
1254
|
+
|
|
1255
|
+
// Several outputs, or one through a reference: what the native wrote,
|
|
1256
|
+
// in order - or null when it returned false.
|
|
1257
|
+
if (outputs.length > 1 || outputs.some(o => o.kind === 'ref' || o.kind === 'float-ref')) {
|
|
1258
|
+
if (cell === 0) return null;
|
|
1259
|
+
return outputs.map(({ at, kind }) => {
|
|
1260
|
+
const value = frame[at];
|
|
1261
|
+
if (kind === 'ref') return String((value as number[])[0]);
|
|
1262
|
+
if (kind === 'float-ref') return String(bitsFloat((value as number[])[0]));
|
|
1263
|
+
return value as string;
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (outputs.length === 1) out = outputs[0].at;
|
|
1267
|
+
|
|
1268
|
+
switch (native.result) {
|
|
1269
|
+
// Pawn gets a cell even from a native that returns nothing: a player
|
|
1270
|
+
// native answers 1 when it ran, 0 for a bad slot (plugin-natives.ts).
|
|
1271
|
+
case 'void': return cell;
|
|
1272
|
+
case 'int': case 'tag': case 'array': return cell;
|
|
1273
|
+
case 'float': return bitsFloat(cell);
|
|
1274
|
+
case 'bool': return cell !== 0;
|
|
1275
|
+
case 'string': return frame[out] as string;
|
|
1276
|
+
case 'string?': return cell !== 0 ? frame[out] as string : null;
|
|
1277
|
+
case 'ints': return (frame[out] as number[]).slice(0, cell);
|
|
1278
|
+
case 'floats': return (frame[out] as number[]).slice(0, cell).map(bitsFloat);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* A plugin calling another plugin's `export function` native
|
|
1284
|
+
* through its ~/natives wrapper: the cells it pushed - text and buffers as
|
|
1285
|
+
* addresses in its memory - turned into the frame the native reads, and
|
|
1286
|
+
* what the native wrote into a buffer copied back, as AMX Mod X does.
|
|
1287
|
+
* @internal
|
|
1288
|
+
*/
|
|
1289
|
+
callFromPlugin(caller: PluginInstance, name: string, cells: number[]): number {
|
|
1290
|
+
const handler = this.exported.get(name)!;
|
|
1291
|
+
const native = this.pluginNatives.get(name);
|
|
1292
|
+
if (!native) return this.call(handler, cells.slice(0, 4), 0);
|
|
1293
|
+
|
|
1294
|
+
const frame: ArgValue[] = [];
|
|
1295
|
+
const buffers: { at: number; pointer: number; room: number }[] = [];
|
|
1296
|
+
let next = 0;
|
|
1297
|
+
for (const slot of pawnLayout(native)) {
|
|
1298
|
+
if ('out' in slot || ('output' in slot && slot.output !== 'ref' && slot.output !== 'float-ref')) {
|
|
1299
|
+
const pointer = cells[next++];
|
|
1300
|
+
const room = cells[next++];
|
|
1301
|
+
buffers.push({ at: frame.length, pointer, room });
|
|
1302
|
+
const text = 'out' in slot ? native.result === 'string' || native.result === 'string?' : slot.output === 'string';
|
|
1303
|
+
frame.push(text ? '' : [], room);
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
if ('output' in slot) {
|
|
1307
|
+
const pointer = cells[next++];
|
|
1308
|
+
buffers.push({ at: frame.length, pointer, room: 1 });
|
|
1309
|
+
frame.push([caller.memory.cell(pointer)]);
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if ('format' in slot || slot.byRef) {
|
|
1313
|
+
// The tail, by address in the caller's memory.
|
|
1314
|
+
while (next < cells.length) frame.push(new Pointer(caller, cells[next++]));
|
|
1315
|
+
if ('format' in slot) continue;
|
|
1316
|
+
break;
|
|
1317
|
+
}
|
|
1318
|
+
const { kind } = slot.param;
|
|
1319
|
+
if (kind === 'string') {
|
|
1320
|
+
frame.push(caller.memory.text(cells[next++]));
|
|
1321
|
+
} else if (kind === 'vector') {
|
|
1322
|
+
frame.push([0, 1, 2].map(i => caller.memory.cell(cells[next] + i * 4)));
|
|
1323
|
+
next++;
|
|
1324
|
+
} else if (kind === 'ints' || kind === 'floats') {
|
|
1325
|
+
const pointer = cells[next++];
|
|
1326
|
+
const size = cells[next++];
|
|
1327
|
+
frame.push(Array.from({ length: size }, (_, i) => caller.memory.cell(pointer + i * 4)), size);
|
|
1328
|
+
} else {
|
|
1329
|
+
frame.push(cells[next++] ?? 0);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const result = this.withCallArgs(frame, () => this.call(handler, [], 0));
|
|
1334
|
+
|
|
1335
|
+
for (const { at, pointer, room } of buffers) {
|
|
1336
|
+
const value = frame[at];
|
|
1337
|
+
if (typeof value === 'string') caller.memory.setText(pointer, room, value);
|
|
1338
|
+
else if (Array.isArray(value)) value.slice(0, room).forEach((cell, i) => caller.memory.setCell(pointer + i * 4, cell));
|
|
1339
|
+
}
|
|
1340
|
+
return result;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// ------------------------------------------------------------ the module's half
|
|
1344
|
+
|
|
1345
|
+
/** Fire in module.cpp: one handler, its outcome, and the state put back. @internal */
|
|
1346
|
+
call(handler: Handler, args: number[], fallback: number): number {
|
|
1347
|
+
const fn = handler.plugin.table.get(handler.fn);
|
|
1348
|
+
const cells = handler.shape === SHAPE_WIDE ? [0, 1, 2, 3].map(i => args[i] ?? 0) : [args[0] ?? 0];
|
|
1349
|
+
if (handler.tag) cells.unshift(handler.tag);
|
|
1350
|
+
|
|
1351
|
+
const said = this.outcomeSaid;
|
|
1352
|
+
const before = this.outcome;
|
|
1353
|
+
this.outcomeSaid = false;
|
|
1354
|
+
|
|
1355
|
+
try {
|
|
1356
|
+
this.within(handler.plugin, () => fn(...cells));
|
|
1357
|
+
return this.outcomeSaid ? this.outcome : fallback;
|
|
1358
|
+
} finally {
|
|
1359
|
+
this.outcomeSaid = said;
|
|
1360
|
+
this.outcome = before;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Runs `body` as `plugin`: what its natives are called on behalf of. @internal */
|
|
1365
|
+
within<T>(plugin: PluginInstance, body: () => T): T {
|
|
1366
|
+
const previous = this.current;
|
|
1367
|
+
this.current = plugin;
|
|
1368
|
+
const coroutines = plugin.coroutines;
|
|
1369
|
+
if (coroutines) coroutines.depth++;
|
|
1370
|
+
try {
|
|
1371
|
+
return body();
|
|
1372
|
+
} finally {
|
|
1373
|
+
this.current = previous;
|
|
1374
|
+
// The plugin's jobs - what an await was waiting for - once nothing of it is on the stack.
|
|
1375
|
+
if (coroutines && --coroutines.depth === 0 && coroutines.waiting) {
|
|
1376
|
+
const outer = this.current;
|
|
1377
|
+
this.current = plugin;
|
|
1378
|
+
coroutines.depth++;
|
|
1379
|
+
try {
|
|
1380
|
+
coroutines.drain();
|
|
1381
|
+
} finally {
|
|
1382
|
+
coroutines.depth--;
|
|
1383
|
+
this.current = outer;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** The arguments arg() and arg_text() read while `body` runs. @internal */
|
|
1390
|
+
withCallArgs<T>(args: ArgValue[], body: () => T): T {
|
|
1391
|
+
const previous = this.callArgs;
|
|
1392
|
+
this.callArgs = args;
|
|
1393
|
+
try {
|
|
1394
|
+
return body();
|
|
1395
|
+
} finally {
|
|
1396
|
+
this.callArgs = previous;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** A client command: client_command, then every handler of it until one takes it. @internal */
|
|
1401
|
+
clientCommand(player: FakePlayer, argv: string[]): boolean {
|
|
1402
|
+
return this.withArgv(argv, () =>
|
|
1403
|
+
this.fire('client_command', player.id) >= PLUGIN_HANDLED || this.handleCommand(this.clientCommands, player.id));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* A line typed in the server console or sent over rcon, split the way the
|
|
1408
|
+
* engine splits it: every `register_srvcmd` handler of it until one takes
|
|
1409
|
+
* it. True if handled.
|
|
1410
|
+
*/
|
|
1411
|
+
serverCommand(line: string): boolean {
|
|
1412
|
+
return this.withArgv(splitCommand(line), () => this.handleCommand(this.serverCommands, 0));
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/** The handlers of the command in argv, in order, until one takes it. */
|
|
1416
|
+
private handleCommand(handlers: Map<string, Slot[]>, id: number): boolean {
|
|
1417
|
+
return [...(handlers.get(this.argv[0].toLowerCase()) ?? [])].some((slot) => {
|
|
1418
|
+
const args = [id, 0, slot.index];
|
|
1419
|
+
return this.withCallArgs(args, () => this.call(slot, args, slot.fallback)) >= PLUGIN_HANDLED;
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** The command read_argv reads while `body` runs. */
|
|
1424
|
+
private withArgv<T>(argv: string[], body: () => T): T {
|
|
1425
|
+
const previous = this.argv;
|
|
1426
|
+
this.argv = argv;
|
|
1427
|
+
try {
|
|
1428
|
+
return body();
|
|
1429
|
+
} finally {
|
|
1430
|
+
this.argv = previous;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/** A slot by the public name a native was given: "__amxts_cb3". @internal */
|
|
1435
|
+
slotByPublic(name: string): Slot {
|
|
1436
|
+
const match = name.match(/^__amxts_cb(\d+)$/);
|
|
1437
|
+
const slot = match ? this.slots[Number(match[1])] : undefined;
|
|
1438
|
+
if (!slot) throw new Error(`the fake server knows no public named "${name}"`);
|
|
1439
|
+
return slot;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* One entity moving into another: the register_touch callbacks whose
|
|
1444
|
+
* classes match ("*" or "" is any), called as the engine module calls
|
|
1445
|
+
* them - (touched, toucher). The largest answer, as the module takes it.
|
|
1446
|
+
*/
|
|
1447
|
+
touch(toucher: FakeEntity, touched: FakeEntity): number {
|
|
1448
|
+
const matches = (name: string, entity: FakeEntity) => name === '' || name === '*' || name === entity.classname;
|
|
1449
|
+
let answer = 0;
|
|
1450
|
+
for (const { slot } of this.touches.filter(t => matches(t.touched, touched) && matches(t.toucher, toucher))) {
|
|
1451
|
+
answer = Math.max(answer, this.withCallArgs([touched.id, toucher.id], () => this.call(slot, [touched.id, toucher.id], 0)));
|
|
1452
|
+
}
|
|
1453
|
+
return answer;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** A client answering query_client_cvar: every question about that cvar it had. */
|
|
1457
|
+
answerCvar(player: FakePlayer, cvar: string, value: string): void {
|
|
1458
|
+
for (const query of this.cvarQueries.filter(q => q.player === player.id && q.cvar === cvar)) {
|
|
1459
|
+
this.withCallArgs([player.id, cvar, value], () => this.call(query.slot, [player.id, 0, 0, 0], 0));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** An entity on the map, as create_entity makes one. */
|
|
1464
|
+
createEntity(classname: string): FakeEntity {
|
|
1465
|
+
const created = new FakeEntity(this, this.nextEntityId(), classname);
|
|
1466
|
+
this.entities.set(created.id, created);
|
|
1467
|
+
return created;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/** @internal */
|
|
1471
|
+
nextEntityId(): number {
|
|
1472
|
+
return this.entityIds++;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** @internal */
|
|
1476
|
+
createCvar(name: string, value: string): Cvar {
|
|
1477
|
+
const cvar: Cvar = { name, value, pointer: this.cvars.size + 1, hooks: [] };
|
|
1478
|
+
this.cvars.set(name.toLowerCase(), cvar);
|
|
1479
|
+
return cvar;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/** @internal */
|
|
1483
|
+
cvarByPointer(pointer: number): Cvar | undefined {
|
|
1484
|
+
for (const cvar of this.cvars.values()) {
|
|
1485
|
+
if (cvar.pointer === pointer) return cvar;
|
|
1486
|
+
}
|
|
1487
|
+
return undefined;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/** A new value, and hook_cvar_change's callbacks told (pcvar, old, new). @internal */
|
|
1491
|
+
changeCvar(cvar: Cvar, value: string): void {
|
|
1492
|
+
const old = cvar.value;
|
|
1493
|
+
cvar.value = value;
|
|
1494
|
+
if (old === value) return;
|
|
1495
|
+
|
|
1496
|
+
for (const slot of [...cvar.hooks]) {
|
|
1497
|
+
const args = [cvar.pointer, old, value];
|
|
1498
|
+
this.withCallArgs(args, () => this.call(slot, [cvar.pointer, 0, 0], slot.fallback));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/** A native from natives.ts, called by `plugin`. @internal */
|
|
1503
|
+
callNativeImpl(plugin: PluginInstance, name: string, args: number[]): number {
|
|
1504
|
+
const call: NativeCall = { server: this, plugin, memory: plugin.memory };
|
|
1505
|
+
const result = (this.ownNatives.get(name) ?? NATIVES[name])(call, args);
|
|
1506
|
+
return typeof result === 'number' ? result | 0 : 0;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/** Modules a plugin runs for the others (module.cpp's g_services), and the bytes of the call in flight. @internal */
|
|
1510
|
+
private services: { name: string; hash: number; plugin: number }[] = [];
|
|
1511
|
+
private rpcRequest = new Uint8Array(0);
|
|
1512
|
+
private rpcReply = new Uint8Array(0);
|
|
1513
|
+
private rpcResult = new Uint8Array(0);
|
|
1514
|
+
|
|
1515
|
+
/**
|
|
1516
|
+
* The externals the facade declares, as module.cpp registers
|
|
1517
|
+
* them in g_wasmNatives. Each is called with the plugin that called it.
|
|
1518
|
+
* @internal
|
|
1519
|
+
*/
|
|
1520
|
+
readonly bridge = {
|
|
1521
|
+
// Shared modules: module.cpp's w_serve, w_owner, w_rpc and the three that move the bytes.
|
|
1522
|
+
amxts_serve(this: FakeServer, plugin: PluginInstance, name: number, hash: number) {
|
|
1523
|
+
const wanted = plugin.memory.string(name);
|
|
1524
|
+
const index = this.plugins.indexOf(plugin);
|
|
1525
|
+
const known = this.services.find(s => s.name === wanted);
|
|
1526
|
+
if (known) Object.assign(known, { hash, plugin: index });
|
|
1527
|
+
else this.services.push({ name: wanted, hash, plugin: index });
|
|
1528
|
+
},
|
|
1529
|
+
|
|
1530
|
+
amxts_owner(this: FakeServer, plugin: PluginInstance, name: number, hash: number) {
|
|
1531
|
+
const wanted = plugin.memory.string(name);
|
|
1532
|
+
const index = this.services.findIndex(s => s.name === wanted);
|
|
1533
|
+
if (index < 0 || this.services[index].plugin < 0) return -1;
|
|
1534
|
+
return this.services[index].hash === hash ? index : -2;
|
|
1535
|
+
},
|
|
1536
|
+
|
|
1537
|
+
amxts_rpc(this: FakeServer, plugin: PluginInstance, service: number, target: number, data: number, length: number) {
|
|
1538
|
+
const index = service >= 0 ? (this.services[service]?.plugin ?? -1) : target;
|
|
1539
|
+
const callee = this.plugins[index];
|
|
1540
|
+
const rpc = callee?.instance.exports.__amxts_rpc;
|
|
1541
|
+
if (!rpc) return -1;
|
|
1542
|
+
this.rpcRequest = new Uint8Array(plugin.instance.exports.memory.buffer, data, length).slice();
|
|
1543
|
+
this.rpcReply = new Uint8Array(0);
|
|
1544
|
+
this.within(callee, () => rpc(length, this.plugins.indexOf(plugin)));
|
|
1545
|
+
const answer = new Uint8Array(4 + this.rpcReply.length);
|
|
1546
|
+
new DataView(answer.buffer).setInt32(0, index, true);
|
|
1547
|
+
answer.set(this.rpcReply, 4);
|
|
1548
|
+
this.rpcResult = answer;
|
|
1549
|
+
return answer.length;
|
|
1550
|
+
},
|
|
1551
|
+
|
|
1552
|
+
amxts_rpc_take(this: FakeServer, plugin: PluginInstance, to: number) {
|
|
1553
|
+
plugin.memory.setRaw(to, this.rpcRequest);
|
|
1554
|
+
this.rpcRequest = new Uint8Array(0);
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
amxts_rpc_reply(this: FakeServer, plugin: PluginInstance, data: number, length: number) {
|
|
1558
|
+
this.rpcReply = new Uint8Array(plugin.instance.exports.memory.buffer, data, length).slice();
|
|
1559
|
+
},
|
|
1560
|
+
|
|
1561
|
+
amxts_rpc_result(this: FakeServer, plugin: PluginInstance, to: number) {
|
|
1562
|
+
plugin.memory.setRaw(to, this.rpcResult);
|
|
1563
|
+
this.rpcResult = new Uint8Array(0);
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
abort(this: FakeServer, plugin: PluginInstance, message: number, file: number, line: number, column: number) {
|
|
1567
|
+
const text = message ? plugin.memory.string(message) : 'assertion failed';
|
|
1568
|
+
throw new Error(`abort: ${text} (${file ? plugin.memory.string(file) : '?'}:${line}:${column})`);
|
|
1569
|
+
},
|
|
1570
|
+
|
|
1571
|
+
seed() {
|
|
1572
|
+
return Date.now();
|
|
1573
|
+
},
|
|
1574
|
+
|
|
1575
|
+
// On the fake clock, so advance() moves them: the map starts at the
|
|
1576
|
+
// real moment the server was made.
|
|
1577
|
+
'Date.now': function (this: FakeServer) {
|
|
1578
|
+
return this.startedAt + this.time;
|
|
1579
|
+
},
|
|
1580
|
+
'Date.getTimezoneOffset': function (this: FakeServer, _plugin: PluginInstance, time: number) {
|
|
1581
|
+
return timezoneOffset(time, this.timeZone);
|
|
1582
|
+
},
|
|
1583
|
+
'performance.now': function (this: FakeServer) {
|
|
1584
|
+
return this.time;
|
|
1585
|
+
},
|
|
1586
|
+
|
|
1587
|
+
'console.log': logLine,
|
|
1588
|
+
'console.info': logLine,
|
|
1589
|
+
'console.debug': logLine,
|
|
1590
|
+
'console.warn': function (this: FakeServer, plugin: PluginInstance, message: number) {
|
|
1591
|
+
this.logLines.push(`warning: ${plugin.memory.string(message)}`);
|
|
1592
|
+
},
|
|
1593
|
+
'console.error': function (this: FakeServer, plugin: PluginInstance, message: number) {
|
|
1594
|
+
this.logLines.push(`error: ${plugin.memory.string(message)}`);
|
|
1595
|
+
},
|
|
1596
|
+
'console.assert': function (this: FakeServer, plugin: PluginInstance, condition: number, message: number) {
|
|
1597
|
+
if (!condition) this.logLines.push(`assertion failed: ${plugin.memory.string(message)}`);
|
|
1598
|
+
},
|
|
1599
|
+
'console.time': function () {},
|
|
1600
|
+
'console.timeLog': function () {},
|
|
1601
|
+
'console.timeEnd': function () {},
|
|
1602
|
+
|
|
1603
|
+
print_client(this: FakeServer, plugin: PluginInstance, id: number, channel: number, message: number) {
|
|
1604
|
+
const variant = channel === 4 ? 'center' : channel === 3 ? 'chat' : channel === 2 ? 'console' : 'notify';
|
|
1605
|
+
const text = plugin.memory.string(message);
|
|
1606
|
+
for (const player of id > 0 ? [this.player(id)] : this.players) player?.show(variant, text);
|
|
1607
|
+
},
|
|
1608
|
+
|
|
1609
|
+
say_text(this: FakeServer, plugin: PluginInstance, id: number, text: number) {
|
|
1610
|
+
const line = plugin.memory.string(text);
|
|
1611
|
+
for (const player of id > 0 ? [this.player(id)] : this.players) player?.show('chat', line);
|
|
1612
|
+
},
|
|
1613
|
+
|
|
1614
|
+
get_name(this: FakeServer, plugin: PluginInstance, id: number, out: number, max: number) {
|
|
1615
|
+
return plugin.memory.setBytes(out, max, this.player(id)?.name ?? '');
|
|
1616
|
+
},
|
|
1617
|
+
|
|
1618
|
+
get_health(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1619
|
+
return Math.trunc(this.player(id)?.health ?? 0);
|
|
1620
|
+
},
|
|
1621
|
+
|
|
1622
|
+
// The module's is set_user_health.
|
|
1623
|
+
set_health(this: FakeServer, plugin: PluginInstance, id: number, hp: number) {
|
|
1624
|
+
NATIVES.set_user_health({ server: this, plugin, memory: plugin.memory }, [id, hp]);
|
|
1625
|
+
},
|
|
1626
|
+
|
|
1627
|
+
outcome(this: FakeServer, plugin: PluginInstance, value: number) {
|
|
1628
|
+
this.outcome = value;
|
|
1629
|
+
this.outcomeSaid = true;
|
|
1630
|
+
},
|
|
1631
|
+
|
|
1632
|
+
on(this: FakeServer, plugin: PluginInstance, name: number, fn: number, shape: number) {
|
|
1633
|
+
const event = plugin.memory.string(name);
|
|
1634
|
+
let list = this.events.get(event);
|
|
1635
|
+
if (!list) this.events.set(event, list = []);
|
|
1636
|
+
list.push({ plugin, fn, shape });
|
|
1637
|
+
},
|
|
1638
|
+
|
|
1639
|
+
subscribe(this: FakeServer, plugin: PluginInstance, name: number, fn: number, tag: number) {
|
|
1640
|
+
const forward = plugin.memory.string(name);
|
|
1641
|
+
let list = this.subscriptions.get(forward);
|
|
1642
|
+
if (!list) this.subscriptions.set(forward, list = []);
|
|
1643
|
+
list.push({ handler: { plugin, fn, shape: SHAPE_NARROW }, tag });
|
|
1644
|
+
},
|
|
1645
|
+
|
|
1646
|
+
// No forward is relayed by a host plugin here, so every one a
|
|
1647
|
+
// TypeScript plugin emits reaches its subscribers this way: `s` a
|
|
1648
|
+
// string, `a` an array as its count and then its cells, the rest cells.
|
|
1649
|
+
emit_local(this: FakeServer, plugin: PluginInstance, name: number, mask: number, cells: number, argc: number) {
|
|
1650
|
+
const forward = plugin.memory.string(name);
|
|
1651
|
+
const types = plugin.memory.string(mask);
|
|
1652
|
+
const { memory } = plugin;
|
|
1653
|
+
const args: ArgValue[] = [];
|
|
1654
|
+
for (let i = 0; i < argc; i++) {
|
|
1655
|
+
const cell = memory.cell(cells + i * 4);
|
|
1656
|
+
if (types[i] === 's') args.push(memory.string(cell));
|
|
1657
|
+
else if (types[i] === 'a') args.push(Array.from({ length: memory.cell(cell) }, (_, k) => memory.cell(cell + 4 + k * 4)));
|
|
1658
|
+
else args.push(cell);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
this.withCallArgs(args, () => this.deliver(forward));
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1664
|
+
slot(this: FakeServer, plugin: PluginInstance, fn: number, shape: number, key: number, fallback: number) {
|
|
1665
|
+
return this.takeSlot(plugin, fn, shape, plugin.memory.string(key), fallback).index;
|
|
1666
|
+
},
|
|
1667
|
+
|
|
1668
|
+
clcmd(this: FakeServer, plugin: PluginInstance, pattern: number, fn: number, flags: number, info: number, shape: number) {
|
|
1669
|
+
const name = plugin.memory.string(pattern);
|
|
1670
|
+
const slot = this.takeSlot(plugin, fn, shape, `clcmd:${name}`, PLUGIN_HANDLED);
|
|
1671
|
+
let list = this.clientCommands.get(name.toLowerCase());
|
|
1672
|
+
if (!list) this.clientCommands.set(name.toLowerCase(), list = []);
|
|
1673
|
+
list.push(slot);
|
|
1674
|
+
return slot.index;
|
|
1675
|
+
},
|
|
1676
|
+
|
|
1677
|
+
task(this: FakeServer, plugin: PluginInstance, secondsBits: number, fn: number, id: number, repeat: number) {
|
|
1678
|
+
const slot = this.takeSlot(plugin, fn, 0, '', 0);
|
|
1679
|
+
const interval = Math.round(bitsFloat(secondsBits) * 1000);
|
|
1680
|
+
this.tasks.push({ slot, id, interval, due: this.time + interval, repeat: repeat !== 0, order: this.taskOrder++ });
|
|
1681
|
+
return slot.index;
|
|
1682
|
+
},
|
|
1683
|
+
|
|
1684
|
+
// remove_task with `outside`: every task of that id, whoever armed it.
|
|
1685
|
+
stop_task(this: FakeServer, plugin: PluginInstance, id: number) {
|
|
1686
|
+
const before = this.tasks.length;
|
|
1687
|
+
for (let i = this.tasks.length - 1; i >= 0; i--) {
|
|
1688
|
+
if (this.tasks[i].id === id) this.tasks.splice(i, 1);
|
|
1689
|
+
}
|
|
1690
|
+
return before - this.tasks.length;
|
|
1691
|
+
},
|
|
1692
|
+
|
|
1693
|
+
hook(this: FakeServer, plugin: PluginInstance, id: number, fn: number, post: number) {
|
|
1694
|
+
const kind = tables().hookNames.get(id);
|
|
1695
|
+
if (!kind) throw new Error(`no hookchain with the number ${id} in as/constants.ts`);
|
|
1696
|
+
|
|
1697
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, `hook:${id}:${post}`, 0);
|
|
1698
|
+
let chain = this.hookchains.get(kind);
|
|
1699
|
+
if (!chain) this.hookchains.set(kind, chain = { pre: [], post: [] });
|
|
1700
|
+
(post ? chain.post : chain.pre).push(slot);
|
|
1701
|
+
return ++this.hookHandles;
|
|
1702
|
+
},
|
|
1703
|
+
|
|
1704
|
+
// RegisterHam: kept by the function's id and the class, for hamCall().
|
|
1705
|
+
ham(this: FakeServer, plugin: PluginInstance, id: number, entityClass: number, fn: number, post: number) {
|
|
1706
|
+
const slot = this.takeSlot(plugin, fn, SHAPE_WIDE, `ham:${id}:${post}`, 0);
|
|
1707
|
+
const key = `${id}:${plugin.memory.string(entityClass)}:${post ? 'post' : 'pre'}`;
|
|
1708
|
+
const list = this.hams.get(key) ?? [];
|
|
1709
|
+
list.push(slot);
|
|
1710
|
+
this.hams.set(key, list);
|
|
1711
|
+
return ++this.hookHandles;
|
|
1712
|
+
},
|
|
1713
|
+
|
|
1714
|
+
tag(this: FakeServer, _plugin: PluginInstance, tag: number) {
|
|
1715
|
+
this.pendingTag = tag;
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
// A plugin that never awaits still takes its timers' handles from here.
|
|
1719
|
+
co_id(this: FakeServer, _plugin: PluginInstance) {
|
|
1720
|
+
return nextTaskId();
|
|
1721
|
+
},
|
|
1722
|
+
|
|
1723
|
+
export(this: FakeServer, plugin: PluginInstance, name: number, fn: number) {
|
|
1724
|
+
this.exported.set(plugin.memory.string(name), { plugin, fn, shape: SHAPE_WIDE, tag: this.takeTag() });
|
|
1725
|
+
return this.exported.size - 1;
|
|
1726
|
+
},
|
|
1727
|
+
|
|
1728
|
+
plugin(this: FakeServer, plugin: PluginInstance, name: number, version: number, author: number, description: number) {
|
|
1729
|
+
const text = (p: number) => plugin.memory.string(p);
|
|
1730
|
+
plugin.info = { name: text(name), version: text(version), author: text(author), description: text(description) };
|
|
1731
|
+
},
|
|
1732
|
+
|
|
1733
|
+
arg(this: FakeServer, plugin: PluginInstance, index: number) {
|
|
1734
|
+
const value = this.callArgs?.[index];
|
|
1735
|
+
return typeof value === 'number' ? value | 0 : typeof value === 'boolean' ? +value : 0;
|
|
1736
|
+
},
|
|
1737
|
+
|
|
1738
|
+
arg_text(this: FakeServer, plugin: PluginInstance, index: number, out: number, max: number) {
|
|
1739
|
+
const value = this.callArgs?.[index];
|
|
1740
|
+
return plugin.memory.setBytes(out, max, typeof value === 'string' ? value : '');
|
|
1741
|
+
},
|
|
1742
|
+
|
|
1743
|
+
// A string argument whole: its UTF-8 length, and as much of it as fits.
|
|
1744
|
+
arg_string(this: FakeServer, plugin: PluginInstance, index: number, out: number, max: number) {
|
|
1745
|
+
const value = this.callArgs?.[index];
|
|
1746
|
+
const text = value instanceof Pointer ? value.plugin.memory.text(value.at) : typeof value === 'string' ? value : '';
|
|
1747
|
+
return plugin.memory.setUtf8(out, max, text);
|
|
1748
|
+
},
|
|
1749
|
+
|
|
1750
|
+
// How many cells a forward's array argument has; -1 for any other.
|
|
1751
|
+
arg_length(this: FakeServer, plugin: PluginInstance, index: number) {
|
|
1752
|
+
const value = this.callArgs?.[index];
|
|
1753
|
+
return Array.isArray(value) ? value.length : -1;
|
|
1754
|
+
},
|
|
1755
|
+
|
|
1756
|
+
arg_array(this: FakeServer, plugin: PluginInstance, index: number, out: number, count: number) {
|
|
1757
|
+
const given = this.callArgs?.[index];
|
|
1758
|
+
const value = given instanceof Pointer
|
|
1759
|
+
? Array.from({ length: count }, (_, i) => given.plugin.memory.cell(given.at + i * 4))
|
|
1760
|
+
: given;
|
|
1761
|
+
if (!Array.isArray(value) || !out || count <= 0) return 0;
|
|
1762
|
+
for (let i = 0; i < count; i++) plugin.memory.setCell(out + i * 4, value[i] ?? 0);
|
|
1763
|
+
return count;
|
|
1764
|
+
},
|
|
1765
|
+
|
|
1766
|
+
set_arg_array(this: FakeServer, plugin: PluginInstance, index: number, cells: number, count: number) {
|
|
1767
|
+
const value = this.callArgs?.[index];
|
|
1768
|
+
if (!Array.isArray(value) || count < 0) return 0;
|
|
1769
|
+
for (let i = 0; i < count; i++) value[i] = plugin.memory.cell(cells + i * 4);
|
|
1770
|
+
return count;
|
|
1771
|
+
},
|
|
1772
|
+
|
|
1773
|
+
// set_param_byref: through the reference - a one-cell array here.
|
|
1774
|
+
set_arg(this: FakeServer, plugin: PluginInstance, index: number, value: number) {
|
|
1775
|
+
if (!this.callArgs || index < 0 || index >= this.callArgs.length) return 0;
|
|
1776
|
+
const target = this.callArgs[index];
|
|
1777
|
+
if (Array.isArray(target)) target[0] = value;
|
|
1778
|
+
else this.callArgs[index] = value;
|
|
1779
|
+
return 1;
|
|
1780
|
+
},
|
|
1781
|
+
|
|
1782
|
+
// set_amxstring: at most `max` bytes, cut where a UTF-8 character starts.
|
|
1783
|
+
set_arg_text(this: FakeServer, plugin: PluginInstance, index: number, text: number, max: number) {
|
|
1784
|
+
if (!this.callArgs || index < 0 || index >= this.callArgs.length || max <= 0) return 0;
|
|
1785
|
+
const bytes = new TextEncoder().encode(plugin.memory.string(text));
|
|
1786
|
+
const length = utf8Fit(bytes, max);
|
|
1787
|
+
this.callArgs[index] = new TextDecoder().decode(bytes.subarray(0, length));
|
|
1788
|
+
return length;
|
|
1789
|
+
},
|
|
1790
|
+
|
|
1791
|
+
// The fields plugins add to Player: the imports their generated accessors call.
|
|
1792
|
+
player_data_get(this: FakeServer, plugin: PluginInstance, id: number, key: number) {
|
|
1793
|
+
return this.playerNumber(id, plugin.memory.string(key));
|
|
1794
|
+
},
|
|
1795
|
+
|
|
1796
|
+
player_data_set(this: FakeServer, plugin: PluginInstance, id: number, key: number, value: number) {
|
|
1797
|
+
this.setPlayerData(id, plugin.memory.string(key), value);
|
|
1798
|
+
},
|
|
1799
|
+
|
|
1800
|
+
// The text's UTF-8 length, and as much of it as fits.
|
|
1801
|
+
player_data_get_text(this: FakeServer, plugin: PluginInstance, id: number, key: number, out: number, max: number) {
|
|
1802
|
+
return plugin.memory.setUtf8(out, max, this.playerText(id, plugin.memory.string(key)));
|
|
1803
|
+
},
|
|
1804
|
+
|
|
1805
|
+
player_data_set_text(this: FakeServer, plugin: PluginInstance, id: number, key: number, value: number) {
|
|
1806
|
+
this.setPlayerData(id, plugin.memory.string(key), plugin.memory.string(value));
|
|
1807
|
+
},
|
|
1808
|
+
|
|
1809
|
+
// A Player[] field: the ids as text, "3,5".
|
|
1810
|
+
player_data_set_players(this: FakeServer, plugin: PluginInstance, id: number, key: number, value: number) {
|
|
1811
|
+
this.setPlayerData(id, plugin.memory.string(key), plugin.memory.string(value), true);
|
|
1812
|
+
},
|
|
1813
|
+
|
|
1814
|
+
// playerchange: the module wakes a plugin for the fields it listens for.
|
|
1815
|
+
player_change_listen(this: FakeServer, plugin: PluginInstance, field: number, fn: number) {
|
|
1816
|
+
const name = plugin.memory.string(field);
|
|
1817
|
+
const known = this.fieldListeners.find(one => one.plugin === plugin);
|
|
1818
|
+
if (!known) this.fieldListeners.push({ plugin, fn, fields: [name] });
|
|
1819
|
+
else if (!known.fields.includes(name)) known.fields.push(name);
|
|
1820
|
+
},
|
|
1821
|
+
|
|
1822
|
+
// The change being told: 1 its number before, 2 after.
|
|
1823
|
+
player_change_get(this: FakeServer, _plugin: PluginInstance, which: number) {
|
|
1824
|
+
const value = which === 1 ? this.fieldChange?.previous : this.fieldChange?.value;
|
|
1825
|
+
return typeof value === 'number' ? value : 0;
|
|
1826
|
+
},
|
|
1827
|
+
|
|
1828
|
+
// 0 its key, 1 its text before, 2 after.
|
|
1829
|
+
player_change_get_text(this: FakeServer, plugin: PluginInstance, which: number, out: number, max: number) {
|
|
1830
|
+
const change = this.fieldChange;
|
|
1831
|
+
const value = which === 0 ? change?.key : which === 1 ? change?.previous : change?.value;
|
|
1832
|
+
return plugin.memory.setUtf8(out, max, typeof value === 'string' ? value : '');
|
|
1833
|
+
},
|
|
1834
|
+
|
|
1835
|
+
argc(this: FakeServer) {
|
|
1836
|
+
return this.callArgs?.length ?? 0;
|
|
1837
|
+
},
|
|
1838
|
+
|
|
1839
|
+
// Only a plugin's exported native has a caller, and here a test is it.
|
|
1840
|
+
caller() {
|
|
1841
|
+
return -1;
|
|
1842
|
+
},
|
|
1843
|
+
|
|
1844
|
+
// The dispatcher: a native with a `...` tail, its arguments laid out as
|
|
1845
|
+
// cells. Every mask letter but `n` is an address in the plugin's memory,
|
|
1846
|
+
// which is what a Pawn frame holds too - so the native gets the cells as
|
|
1847
|
+
// they are, and reads through the addresses itself.
|
|
1848
|
+
call(this: FakeServer, plugin: PluginInstance, id: number, argsPtr: number, maskPtr: number, argc: number) {
|
|
1849
|
+
const name = tables().dispatched.get(id);
|
|
1850
|
+
if (!name) throw new Error(`the dispatcher has no native with id ${id}`);
|
|
1851
|
+
|
|
1852
|
+
const args: number[] = [];
|
|
1853
|
+
for (let i = 0; i < argc; i++) args.push(plugin.memory.cell(argsPtr + i * 4));
|
|
1854
|
+
|
|
1855
|
+
if (!this.hasNative(name)) throw unsimulated(name);
|
|
1856
|
+
return this.callNativeImpl(plugin, name, args);
|
|
1857
|
+
},
|
|
1858
|
+
};
|
|
1859
|
+
|
|
1860
|
+
/** What `tag()` said for the next registration (w_tag in module.cpp). */
|
|
1861
|
+
private pendingTag = 0;
|
|
1862
|
+
|
|
1863
|
+
private takeTag(): number {
|
|
1864
|
+
const tag = this.pendingTag;
|
|
1865
|
+
this.pendingTag = 0;
|
|
1866
|
+
return tag;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
private takeSlot(plugin: PluginInstance, fn: number, shape: number, key: string, fallback: number): Slot {
|
|
1870
|
+
const slot: Slot = { plugin, fn, shape, fallback, key, index: this.slots.length, tag: this.takeTag() };
|
|
1871
|
+
this.slots.push(slot);
|
|
1872
|
+
return slot;
|
|
1873
|
+
}
|
|
1874
|
+
}
|