@amxts/core 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +21 -25
  2. package/README.ru.md +22 -24
  3. package/as/amxts.d.ts +63 -3
  4. package/as/constants.ts +684 -116
  5. package/as/entities.ts +1905 -1364
  6. package/as/events.ts +1696 -907
  7. package/as/facade.ts +1240 -220
  8. package/as/fetch.ts +1519 -0
  9. package/as/flags.ts +159 -145
  10. package/as/hlds.ts +1071 -0
  11. package/as/hooks.ts +5226 -5379
  12. package/as/kit.ts +15 -5
  13. package/as/lib/check.ts +1 -1
  14. package/as/natives.ts +2658 -2499
  15. package/as/remote.ts +39 -15
  16. package/as/tsconfig.json +7 -2
  17. package/as/vector.ts +12 -9
  18. package/includes/order.txt +22 -29
  19. package/includes/sources.json +0 -8
  20. package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
  21. package/package.json +29 -7
  22. package/runtime/deps/assemblyscript/dist/asc.js +86 -16
  23. package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
  24. package/runtime/licenses/MPL-2.0.txt +373 -0
  25. package/runtime/licenses/README.md +5 -1
  26. package/runtime/licenses/curl-COPYING +22 -0
  27. package/runtime/licenses/libssh2-COPYING +43 -0
  28. package/runtime/licenses/mbedTLS-LICENSE +553 -0
  29. package/runtime/natives.txt +108 -105
  30. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
  31. package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
  32. package/scripts/apply-docs.ts +2 -2
  33. package/scripts/auto-imports.ts +2 -2
  34. package/scripts/build-identity.ts +115 -0
  35. package/scripts/build-wasm.ts +101 -50
  36. package/scripts/check.ts +17 -3
  37. package/scripts/client-messages.ts +246 -0
  38. package/scripts/compile-cache.ts +4 -2
  39. package/scripts/compile-pool.ts +128 -0
  40. package/scripts/compile-worker.ts +23 -0
  41. package/scripts/compile.ts +156 -59
  42. package/scripts/docker-server.ts +13 -3
  43. package/scripts/docs/as/amxts.ts +86 -0
  44. package/scripts/docs/as/facade.ts +688 -188
  45. package/scripts/docs/as/fetch.ts +805 -0
  46. package/scripts/docs/entities.ts +43 -144
  47. package/scripts/docs/events.ts +17 -28
  48. package/scripts/docs/game.ts +68 -22
  49. package/scripts/docs/messages.ts +330 -16
  50. package/scripts/editor-api.ts +101 -0
  51. package/scripts/hlds-events.ts +370 -0
  52. package/scripts/includes.ts +12 -10
  53. package/scripts/player-fields.ts +16 -15
  54. package/scripts/prebuilt.ts +65 -23
  55. package/scripts/prepare.ts +42 -28
  56. package/scripts/project.ts +120 -20
  57. package/scripts/reapi-events.ts +98 -0
  58. package/scripts/shared-modules.ts +85 -23
  59. package/scripts/source-map.ts +242 -0
  60. package/scripts/system.ts +1 -1
  61. package/scripts/typed-commands.ts +263 -0
  62. package/scripts/typed-configs.ts +23 -12
  63. package/scripts/ui.ts +3 -1
  64. package/scripts/upgrade-menus.ts +274 -0
  65. package/scripts/upgrade-names.ts +384 -0
  66. package/scripts/upgrade.ts +857 -0
  67. package/src/cli-api.mjs +37 -4
  68. package/src/system.mjs +74 -3
  69. package/src/testing/compile.ts +8 -5
  70. package/src/testing/index.ts +2 -1
  71. package/src/testing/natives.ts +199 -5
  72. package/src/testing/network.ts +531 -0
  73. package/src/testing/server.ts +572 -48
  74. package/src/testing/setup.ts +1 -1
  75. package/src/testing/tables.ts +25 -0
  76. package/as/modules/http.ts +0 -184
  77. package/scripts/docs/as/modules/http.ts +0 -66
package/src/cli-api.mjs CHANGED
@@ -7,17 +7,19 @@
7
7
  // compiles runs on Bun, as a task the command starts.
8
8
  //
9
9
  // const core = await import('@amxts/core/cli-api');
10
- // core.cliApi 3 - the contract below; the command checks it
10
+ // core.cliApi 5 - the contract below; the command checks it
11
11
  // core.task('build', ['--deploy']) { runtime: 'bun', args: [...] }
12
12
  // core.includeSources().reapi the ReAPI release the API is built from
13
13
  // core.bunBinary() the Bun the tasks run on, installed with the core
14
14
  // core.serverSystem([], env) { system: 'linux', from: 'server', reason: 'hlds_linux' }
15
+ // core.release('windows') where this version's server files are, and where they go
16
+ // core.moduleVersion(file) '0.2.0': the release a server's module is of
15
17
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
16
18
  import { createRequire } from 'node:module';
17
19
  import { dirname, join, resolve } from 'node:path';
18
20
  import process from 'node:process';
19
21
  import { fileURLToPath } from 'node:url';
20
- import { wamrcPath } from './system.mjs';
22
+ import { manifestName, serverFiles, serverImage, wamrcPath } from './system.mjs';
21
23
 
22
24
  /** The core's folder: the package this file ships in. */
23
25
  export const coreDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -32,7 +34,7 @@ export const version = String(pkg.version);
32
34
  * returns changes in a way an older command would misread; the command says
33
35
  * which of the two to update when they differ.
34
36
  */
35
- export const cliApi = 3;
37
+ export const cliApi = 5;
36
38
 
37
39
  /** Whether the core runs from a checkout (a .git folder beside it) rather than from npm. */
38
40
  export const fromSource = existsSync(join(coreDir, '.git'));
@@ -62,6 +64,7 @@ const SCRIPTS = {
62
64
  prepare: 'prepare.ts',
63
65
  build: 'build-wasm.ts',
64
66
  check: 'check.ts',
67
+ upgrade: 'upgrade.ts',
65
68
  };
66
69
 
67
70
  /**
@@ -78,6 +81,9 @@ const SCRIPTS = {
78
81
  * `--docker`: for the Docker server that mounts the project - no deploy,
79
82
  * Linux only, its console shown);
80
83
  * - `check` checks a module package before it is published;
84
+ * - `upgrade` rewrites the project's code to this core's API, listing each change;
85
+ * `--dry-run` writes nothing, `--report <file>` writes `{ changes, left }` there as JSON
86
+ * and leaves what is left to do by hand to the caller to print;
81
87
  * - `typecheck` runs TypeScript over the project, after `prepare`.
82
88
  *
83
89
  * A failure is printed by the task itself and ends it with a non-zero code.
@@ -97,7 +103,7 @@ export function task(name, args = []) {
97
103
 
98
104
  /**
99
105
  * The third-party includes this core's API is generated from, as
100
- * includes/sources.json pins them, by id: `reapi`, `easy_http`, `resemiclip`,
106
+ * includes/sources.json pins them, by id: `reapi`, `resemiclip`,
101
107
  * each `{ name, version, license, home, url, sha256, include? }` - `url` gives
102
108
  * a file whose sha256 is `sha256`; for a .zip, `include` is the folder in it
103
109
  * whose .inc files are the includes. The command fetches ReAPI's for a
@@ -150,6 +156,33 @@ export function bunBinary() {
150
156
  */
151
157
  export { describeSystem, serverSystem } from './system.mjs';
152
158
 
159
+ /**
160
+ * This version's GitHub Release, for a server of one system: `url` is where
161
+ * its files are (AMXTS_RELEASE_URL - a URL or a folder - takes another),
162
+ * `manifest` the file there that lists each one's sha256, `files` the ones a
163
+ * server runs and where they go under its game folder (cstrike), the module
164
+ * first; a `tool` only where the server has one. `image` is the server image
165
+ * of this version, for a server in Docker.
166
+ *
167
+ * @param {'windows' | 'linux'} system
168
+ */
169
+ export function release(system) {
170
+ return {
171
+ version,
172
+ url: process.env.AMXTS_RELEASE_URL || `https://github.com/amxts/amxts/releases/download/v${version}/`,
173
+ manifest: manifestName(system),
174
+ files: serverFiles(system),
175
+ image: serverImage(version),
176
+ };
177
+ }
178
+
179
+ /**
180
+ * The release a module file is of - `0.2.0`, read from the ABI string the
181
+ * module carries, without loading it - or null when there is no file or it
182
+ * carries none.
183
+ */
184
+ export { moduleVersion } from './system.mjs';
185
+
153
186
  /** The TypeScript the core builds with: the command reads amxts.config.ts with its parser. */
154
187
  export function typescript() {
155
188
  return require('typescript');
package/src/system.mjs CHANGED
@@ -13,9 +13,9 @@
13
13
  //
14
14
  // Plain JavaScript: the build (scripts/system.ts) and the amxts command, through
15
15
  // cli-api, both read it.
16
- import { existsSync, readdirSync } from 'node:fs';
16
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
17
17
  import { createRequire } from 'node:module';
18
- import { dirname, join, resolve } from 'node:path';
18
+ import { basename, dirname, join, resolve } from 'node:path';
19
19
  import process from 'node:process';
20
20
  import { fileURLToPath } from 'node:url';
21
21
 
@@ -38,6 +38,52 @@ export const TARGET_ABI = { windows: 'msvc', linux: 'gnu' };
38
38
  /** The amxts module's file, as AMX Mod X on that system looks it up for `amxts_amxx` in modules.ini. @type {Record<System, string>} */
39
39
  export const MODULE_FILE = { windows: 'amxts_amxx.dll', linux: 'amxts_amxx_i386.so' };
40
40
 
41
+ /**
42
+ * The files of a system's GitHub Release that a server runs: the name each is
43
+ * attached under, and where it goes under the game folder (cstrike). A `tool`
44
+ * is the compiler for `.ts` plugins written on the server, which a server has
45
+ * only when its kit put it there.
46
+ * @param {System} system
47
+ * @returns {{ asset: string, path: string, tool: boolean }[]} the module first, then the tools
48
+ */
49
+ export function serverFiles(system) {
50
+ const suffix = `${system}-x64`;
51
+ return [
52
+ { asset: MODULE_FILE[system], path: `addons/amxmodx/modules/${MODULE_FILE[system]}`, tool: false },
53
+ { asset: executable(`amxts-compile-${suffix}`, system), path: `addons/amxts/tools/${executable('amxts-compile', system)}`, tool: true },
54
+ { asset: executable(`wamrc-${suffix}`, system), path: `addons/amxts/tools/${executable('wamrc', system)}`, tool: true },
55
+ ];
56
+ }
57
+
58
+ /** A system's release manifest, attached beside its files: the version, and each file's size and sha256. */
59
+ export function manifestName(system) {
60
+ return `amxts-${system}.json`;
61
+ }
62
+
63
+ /** The server image of a version. */
64
+ export function serverImage(version) {
65
+ return `ghcr.io/amxts/server:${version}`;
66
+ }
67
+
68
+ // The ABI the module loads plugins of, which the module carries as a string
69
+ // (AMXTS_ABI in the generated runtime/src/embedded.h): `0.2.0+abi.1a2b3c4d`.
70
+ const MODULE_ABI = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\+abi\.[0-9a-f]{8}/;
71
+
72
+ /**
73
+ * The amxts release a module file is of, read from the ABI string it
74
+ * carries - the file is read, not loaded, so a module a running server holds
75
+ * is read as well. Null when there is no file or no such string.
76
+ * @param {string} file
77
+ * @returns {string | null} the version: `0.2.0`
78
+ */
79
+ export function moduleVersion(file) {
80
+ try {
81
+ return MODULE_ABI.exec(readFileSync(file, 'latin1'))?.[1] ?? null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
41
87
  /**
42
88
  * `windows`, `win32`, `linux`, any case; null for anything else.
43
89
  * @param {string | undefined | null} value
@@ -50,6 +96,31 @@ export function parseSystem(value) {
50
96
  return null;
51
97
  }
52
98
 
99
+ // The folders AMXTS_SERVER may name above the server's addons/amxts, by what
100
+ // is below them: hlds's own (cstrike/addons), the game's (addons).
101
+ const ABOVE_ADDONS = [join('cstrike', 'addons'), 'addons'];
102
+
103
+ /**
104
+ * The server's addons/amxts folder, from what AMXTS_SERVER holds: that
105
+ * folder itself, or the hlds folder, cstrike or cstrike/addons above it. A
106
+ * path that is not there yet is taken as addons/amxts, as it is written -
107
+ * dev builds, and deploys once it is - and so is a folder named amxts, or
108
+ * one holding the module's plugins.ini. Throws, naming where it looked, for
109
+ * a folder that is none of them.
110
+ * @param {string} path
111
+ * @returns {string} the folder; '' for ''
112
+ */
113
+ export function serverFolder(path) {
114
+ if (!path) return '';
115
+ const dir = resolve(path);
116
+ if (!existsSync(dir) || basename(dir).toLowerCase() === 'amxts' || existsSync(join(dir, 'plugins.ini'))) return path;
117
+ if (basename(dir).toLowerCase() === 'addons') return join(dir, 'amxts');
118
+ const addons = ABOVE_ADDONS.map(below => join(dir, below)).find(folder => existsSync(folder));
119
+ if (addons) return join(addons, 'amxts');
120
+ const looked = [...ABOVE_ADDONS, 'plugins.ini'].map(below => join(dir, below).replace(/\\/g, '/'));
121
+ throw new Error(`AMXTS_SERVER=${path.replace(/\\/g, '/')} is not a server: there is no ${looked.join(', ')}. Set it to the server's folder (where hlds is), its cstrike, or cstrike/addons/amxts.`);
122
+ }
123
+
53
124
  /**
54
125
  * What a server's files say it runs. `amxtsDir` is AMXTS_SERVER: the server's
55
126
  * addons/amxts, so hlds sits three folders up. Null when nothing there says.
@@ -101,7 +172,7 @@ export function serverSystem(argv = process.argv, env = process.env) {
101
172
  if (!system) throw new Error(`AMXTS_SERVER_OS=${env.AMXTS_SERVER_OS}: windows or linux`);
102
173
  return { system, from: 'env' };
103
174
  }
104
- const found = detectServerSystem(env.AMXTS_SERVER ?? '');
175
+ const found = detectServerSystem(serverFolder(env.AMXTS_SERVER ?? ''));
105
176
  if (found) return { system: found.system, from: 'server', reason: found.reason };
106
177
  return { system: HOST_SYSTEM, from: 'host' };
107
178
  }
@@ -6,10 +6,11 @@ import { spawnSync } from 'node:child_process';
6
6
  import { readFileSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { join, relative, resolve } from 'node:path';
8
8
  import { ascMain } from '../../scripts/asc';
9
- import { ASYNC_EXPORTS, asyncify, BASE_EXPORTS, compileToWasm, HoistImports, HOOD_EXPORTS, importsOf } from '../../scripts/compile';
9
+ import { ASYNC_EXPORTS, BASE_EXPORTS, compileToWasm, finishing, HoistImports, HOOD_EXPORTS, importsOf } from '../../scripts/compile';
10
10
  import { playerFieldsBuild } from '../../scripts/player-fields';
11
11
  import { nativesBeside, nativesTransform, setNativesBeside } from '../../scripts/plugin-natives';
12
12
  import { ascPath, CONFIG_FILE, currentProjectDir, sourcesFor } from '../../scripts/project';
13
+ import { targetTransforms } from '../../scripts/reapi-events';
13
14
  import { sharedModulesBuild } from '../../scripts/shared-modules';
14
15
  import { existsSync, hashOf } from '../../scripts/tracked-fs';
15
16
  import { cached } from './compile-cache';
@@ -69,6 +70,7 @@ function moduleOwner(source: string): string | null {
69
70
  if (!/^@?[\w.-]+(?:\/[\w.-]+)?$/.test(source) || source.endsWith('.ts')) return null;
70
71
  const sources = sourcesFor(PLUGINS_ROOT);
71
72
  const pkg = sources.project.modules.find(each => each.name === source);
73
+ if (pkg?.library) throw new Error(`${source} is a library: it runs inside the plugins that import it - load one of them`);
72
74
  return pkg ? sources.ownerSource(pkg) : null;
73
75
  }
74
76
 
@@ -103,15 +105,16 @@ async function build(path: string): Promise<Compiled> {
103
105
 
104
106
  /**
105
107
  * A plugin that makes a promise is compiled again with the scheduler's
106
- * exports and run through Asyncify, as scripts/compile.ts builds it for a
107
- * server - so an async function parks and resumes here as it does there.
108
+ * exports, and run through Asyncify when it can park, as scripts/compile.ts
109
+ * builds it for a server - so an async function parks and resumes here as it
110
+ * does there.
108
111
  */
109
112
  async function buildFresh(path: string): Promise<Omit<Compiled, 'module'>> {
110
113
  const first = await buildWith(path, BASE_EXPORTS);
111
114
  if (!importsOf(first.binary).has('env.co_wake')) return first;
112
115
 
113
116
  const second = await buildWith(path, ASYNC_EXPORTS);
114
- return { natives: second.natives, binary: asyncify(second.binary), async: true };
117
+ return { ...second, async: true };
115
118
  }
116
119
 
117
120
  async function buildWith(path: string, hoodExports: string): Promise<Omit<Compiled, 'module'>> {
@@ -143,7 +146,7 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
143
146
  if (typeof contents !== 'string') binary = contents;
144
147
  },
145
148
  listFiles: () => [],
146
- transforms: [HoistImports, nativesTransform(entry, natives, PLUGINS_ROOT), playerFields.transform, shared.transform],
149
+ transforms: [HoistImports, nativesTransform(entry, natives, PLUGINS_ROOT), playerFields.transform, shared.transform, ...targetTransforms(sources.project.config?.target), finishing(hoodExports, null)],
147
150
  },
148
151
  );
149
152
 
@@ -26,7 +26,8 @@ export { constant } from './tables';
26
26
 
27
27
  /**
28
28
  * Compiles a plugin (once per test run), loads it into a new fake server and
29
- * starts the map: its top level runs, then plugin_init and plugin_cfg.
29
+ * starts the map: its top level runs, then plugin_init, plugin_cfg and
30
+ * OnConfigsExecuted.
30
31
  *
31
32
  * Several plugins go into one server as an array - they hear each other's
32
33
  * forwards and natives, as on a real one.
@@ -10,7 +10,7 @@
10
10
  // answers stops the test with its name; add it below, keeping to what the real
11
11
  // one does closely enough for a test to be believed.
12
12
  import type { Memory } from './memory';
13
- import type { FakePlayer, FakeServer, PluginInstance, SentForward, TeamName, Value } from './server';
13
+ import type { FakePlayer, FakeServer, HookArg, PluginInstance, SentForward, TeamName, Value } from './server';
14
14
  import { bitsFloat, floatBits } from './memory';
15
15
  import { FakeEntity, flagBits, normalizePath, TEAMS } from './server';
16
16
  import { constant, tables } from './tables';
@@ -98,6 +98,33 @@ function cellOf(field: number, value: number | number[] | string): number {
98
98
  return tables().floatFields.has(field) ? floatBits(value) : value | 0;
99
99
  }
100
100
 
101
+ /**
102
+ * A field's cell as the module reads it in memory (runtime/src/fields.h): a
103
+ * whole number, a float's bits, an entity's index; `element` is a vector's
104
+ * component or an array member's element.
105
+ */
106
+ export function fieldCell(target: FakeEntity | undefined, field: number, element = 0): number {
107
+ const { arrayFields } = tables();
108
+ const value = target?.fields.get(FakeEntity.key(field, arrayFields.has(field) ? element : undefined));
109
+ if (Array.isArray(value)) return floatBits(value[element] ?? 0);
110
+ return value === undefined || (element > 0 && !arrayFields.has(field)) ? 0 : cellOf(field, value);
111
+ }
112
+
113
+ /** Writes a field's cell as the module does: a float's bits, one component of a vector, one element of an array. */
114
+ export function setFieldCell(target: FakeEntity | undefined, field: number, cell: number, element = 0): void {
115
+ if (!target) return;
116
+ const { arrayFields, floatFields, vectorFields } = tables();
117
+ const key = FakeEntity.key(field, arrayFields.has(field) ? element : undefined);
118
+ if (vectorFields.has(field)) {
119
+ const before = target.fields.get(key);
120
+ const vector = Array.isArray(before) ? [...before] : [0, 0, 0];
121
+ vector[element] = bitsFloat(cell);
122
+ target.fields.set(key, vector);
123
+ return;
124
+ }
125
+ target.fields.set(key, floatFields.has(field) ? bitsFloat(cell) : cell);
126
+ }
127
+
101
128
  /** get_entvar and get_member: a vector into the tail's buffer, text into its buffer, a cell returned. */
102
129
  function readField(call: NativeCall, target: FakeEntity | undefined, field: number, tail: number[], element?: number): number {
103
130
  if (!target) return 0;
@@ -229,8 +256,13 @@ function executeHam(call: NativeCall, fn: number, id: number, tail: number[], ho
229
256
  return Number(call.server.runHam(shape, id, args).result) | 0;
230
257
  }
231
258
 
259
+ /** A vector given by address: its three numbers. */
260
+ function vectorAt(call: NativeCall, at: number): number[] {
261
+ return [0, 1, 2].map(k => call.memory.float(at + k * 4));
262
+ }
263
+
232
264
  /** SetHamParam*: `which` counts from 1, the entity itself. */
233
- function setHamParam(call: NativeCall, which: number, value: Value): number {
265
+ function setHamParam(call: NativeCall, which: number, value: HookArg): number {
234
266
  chain(call).args[which - 1] = value;
235
267
  return 1;
236
268
  }
@@ -265,6 +297,9 @@ function sendText(call: NativeCall, id: number, variant: 'chat' | 'center' | 'co
265
297
 
266
298
  const PRINT_VARIANTS = ['', 'notify', 'console', 'chat', 'center'] as const;
267
299
 
300
+ // Reunion's client_auth_type by its number, as the player's API names each.
301
+ const AUTH_TYPES = ['unknown', 'dproto', 'steam', 'steamEmu', 'revEmu', 'oldRevEmu', 'hltv', 'sc2009', 'avsmp', 'sxei', 'revEmu2013', 'sse3'];
302
+
268
303
  /** A Pawn string of UTF-8 bytes - a path, a line of a file - as text. */
269
304
  function bytesText(call: NativeCall, pointer: number): string {
270
305
  return call.memory.text(pointer, 65536);
@@ -420,8 +455,52 @@ export const NATIVES: Record<string, Native> = {
420
455
  get_gametime: c => floatBits(c.server.time / 1000),
421
456
  LibraryExists: (c, [name]) => +c.server.modules.has(c.memory.text(name)),
422
457
 
458
+ menu_create: (c, [title, handler]) => c.server.createMenu(c.memory.text(title), c.server.slotByPublic(c.memory.text(handler))),
459
+ menu_additem: (c, [menu, name, _info, _access, callback]) => {
460
+ c.server.menu(menu).items.push({ name: c.memory.text(name), callback });
461
+ return 1;
462
+ },
463
+ menu_makecallback: (c, [fn]) => c.server.menuCallbacks.push(c.server.slotByPublic(c.memory.text(fn))) - 1,
464
+ menu_item_setname: (c, [menu, item, name]) => {
465
+ const entry = c.server.menu(menu).items[item];
466
+ if (!entry) return 0;
467
+ entry.name = c.memory.text(name);
468
+ return 1;
469
+ },
470
+ menu_items: (c, [menu]) => c.server.menu(menu).items.length,
471
+ menu_display: (c, [id, menu, page]) => {
472
+ const target = player(c, id);
473
+ if (!target) throw new Error(`menu_display: player ${id} is not in game`);
474
+ c.server.displayMenu(target, c.server.menu(menu), page);
475
+ },
476
+ menu_destroy: (c, [menu]) => {
477
+ c.server.destroyMenu(menu);
478
+ return 1;
479
+ },
480
+ // The tail comes by address: a number's cell, or a string's.
481
+ menu_setprop: (c, [menu, prop, value]) => {
482
+ const made = c.server.menu(menu);
483
+ const props: Record<string, () => void> = {
484
+ MPROP_PERPAGE: () => made.perPage = c.memory.cell(value),
485
+ MPROP_EXIT: () => made.exit = c.memory.cell(value) !== constant('MEXIT_NEVER'),
486
+ MPROP_BACKNAME: () => made.back = c.memory.text(value),
487
+ MPROP_NEXTNAME: () => made.next = c.memory.text(value),
488
+ MPROP_EXITNAME: () => made.exitName = c.memory.text(value),
489
+ MPROP_TITLE: () => made.title = c.memory.text(value),
490
+ MPROP_NUMBER_COLOR: () => made.numberColor = c.memory.text(value),
491
+ };
492
+ const set = Object.entries(props).find(([name]) => constant(name) === prop)?.[1];
493
+ if (!set) throw new Error(`menu_setprop: the fake server knows no menu property ${prop}`);
494
+ set();
495
+ return 1;
496
+ },
423
497
  server_cmd: (c, [format, ...tail]) => {
424
- c.server.commands.push(formatPawn(c, c.memory.text(format), tail));
498
+ const line = formatPawn(c, c.memory.text(format), tail);
499
+ c.server.commands.push(line);
500
+ // The engine's kick: the player by userid leaves, with the reason.
501
+ const kick = line.match(/^kick #(\d+)(?: "(.*)")?/);
502
+ const kicked = kick && c.server.players.find(one => one.userid === Number(kick[1]));
503
+ if (kicked) kicked.disconnect({ reason: kick[2] ?? 'Kicked' });
425
504
  },
426
505
  server_print: (c, [format, ...tail]) => {
427
506
  c.server.logLines.push(formatPawn(c, c.memory.text(format), tail));
@@ -448,6 +527,7 @@ export const NATIVES: Record<string, Native> = {
448
527
 
449
528
  read_argc: c => c.server.argv.length,
450
529
  read_argv: (c, [index, buffer, length]) => c.memory.setText(buffer, length, c.server.argv[index] ?? ''),
530
+ get_user_userid: (c, [id]) => player(c, id)?.userid ?? -1,
451
531
  read_args: (c, [buffer, length]) => c.memory.setText(buffer, length, c.server.argv.slice(1).join(' ')),
452
532
 
453
533
  // ------------------------------------------------------------ players
@@ -456,8 +536,13 @@ export const NATIVES: Record<string, Native> = {
456
536
  is_user_alive: (c, [id]) => +!!player(c, id)?.alive,
457
537
  is_user_bot: (c, [id]) => +!!player(c, id)?.bot,
458
538
  is_user_hltv: () => 0,
539
+ // reapi's Reunion natives: they are there only when reapi finds Reunion.
540
+ has_reunion: c => +(c.server.modules.has('reapi') && c.server.reunion),
541
+ REU_GetAuthtype: (c, [id]) => Math.max(0, AUTH_TYPES.indexOf(player(c, id)?.authType ?? '')),
542
+ REU_GetProtocol: (c, [id]) => player(c, id)?.protocol ?? 0,
543
+ REU_GetAuthKey: (c, [id, buffer, length]) => c.memory.setText(buffer, length, player(c, id)?.authKey ?? ''),
459
544
  get_user_name: (c, [id, buffer, length]) => c.memory.setText(buffer, length, id === 0 ? 'Console' : player(c, id)?.name ?? ''),
460
- get_user_authid: (c, [id, buffer, length]) => c.memory.setText(buffer, length, player(c, id)?.authid ?? ''),
545
+ get_user_authid: (c, [id, buffer, length]) => c.memory.setText(buffer, length, player(c, id)?.steamId ?? ''),
461
546
  get_user_ip: (c, [id, buffer, length, withoutPort]) => {
462
547
  const ip = player(c, id)?.ip ?? '';
463
548
  return c.memory.setText(buffer, length, withoutPort ? ip.replace(/:\d+$/, '') : ip);
@@ -601,6 +686,8 @@ export const NATIVES: Record<string, Native> = {
601
686
  create_entity: (c, [classname]) => c.server.createEntity(c.memory.text(classname)).id,
602
687
  remove_entity: (c, [id]) => +c.server.entities.delete(id),
603
688
  is_valid_ent: (c, [id]) => +c.server.entities.has(id),
689
+ // 2: the entity has the game's private data, which every fake one has.
690
+ pev_valid: (c, [id]) => (player(c, id) || c.server.entities.has(id) ? 2 : 0),
604
691
  find_ent_by_class: (c, [start, classname]) => {
605
692
  const name = c.memory.text(classname);
606
693
  const found = [...c.server.entities.values()].find(e => e.id > start && e.classname === name);
@@ -799,6 +886,11 @@ export const NATIVES: Record<string, Native> = {
799
886
  c.server.semiclipMasks.set(id, mask | 0);
800
887
  },
801
888
  resemiclip_get_user_mask: (c, [id]) => c.server.semiclipMasks.get(id) ?? 0,
889
+ // A hook's handle switched off and on, as reapi and Ham Sandwich do: a switched-off one does not call the plugin.
890
+ DisableHookChain: (c, [handle]) => c.server.switchHook(handle, false),
891
+ EnableHookChain: (c, [handle]) => c.server.switchHook(handle, true),
892
+ DisableHamForward: (c, [handle]) => c.server.switchHook(handle, false),
893
+ EnableHamForward: (c, [handle]) => c.server.switchHook(handle, true),
802
894
  register_touch: (c, [touched, toucher, callback]) => {
803
895
  c.server.touches.push({ touched: c.memory.text(touched), toucher: c.memory.text(toucher), slot: c.server.slotByPublic(c.memory.text(callback)) });
804
896
  return c.server.touches.length;
@@ -874,6 +966,19 @@ export const NATIVES: Record<string, Native> = {
874
966
  for (const listener of c.server.messageListeners) listener(message);
875
967
  return 1;
876
968
  },
969
+ // emessage_end: the message goes out through the engine, so every plugin's
970
+ // register_message hooks hear it on its way - a plugin's message_end they do not.
971
+ emessage_end: (c) => {
972
+ const message = c.server.writing;
973
+ if (!message) return 1;
974
+ c.server.writing = null;
975
+ const target = c.server.player(message.player);
976
+ const sent = c.server.sendMessage(message.name, message.args, target ? { player: target } : {});
977
+ if (sent.prevented) return 1;
978
+ message.args = sent.args;
979
+ c.server.writing = message;
980
+ return NATIVES.message_end(c, []) as number;
981
+ },
877
982
  write_coord_f: (c, [value]) => {
878
983
  c.server.writing?.args.push(bitsFloat(value));
879
984
  return 1;
@@ -887,7 +992,7 @@ export const NATIVES: Record<string, Native> = {
887
992
  SetHamParamEntity: (c, [which, value]) => setHamParam(c, which, value),
888
993
  SetHamParamFloat: (c, [which, value]) => setHamParam(c, which, bitsFloat(value)),
889
994
  SetHamParamString: (c, [which, text]) => setHamParam(c, which, c.memory.text(text)),
890
- SetHamParamVector: (c, [which]) => setHamParam(c, which, 0),
995
+ SetHamParamVector: (c, [which, at]) => setHamParam(c, which, vectorAt(c, at)),
891
996
  SetHamReturnInteger: (c, [value]) => setHamReturn(c, value),
892
997
  SetHamReturnEntity: (c, [value]) => setHamReturn(c, value),
893
998
  SetHamReturnFloat: (c, [value]) => setHamReturn(c, bitsFloat(value)),
@@ -901,6 +1006,89 @@ export const NATIVES: Record<string, Native> = {
901
1006
  set_dhudmessage: (c, [r, g, b, x, y, _effects, _fxtime, hold]) => {
902
1007
  c.server.hud = { color: [r, g, b], x: bitsFloat(x), y: bitsFloat(y), hold: bitsFloat(hold), channel: -1 };
903
1008
  },
1009
+ // A server without reapi hears ReGameDLL's events through these (as/hlds.ts).
1010
+ // register_logevent(function[], argsnum, ...filters): server.gameLog() calls it.
1011
+ register_logevent: (c, [callback, argc, ...filters]) => {
1012
+ c.server.logEvents.push({ argc, filters: filters.map(at => c.memory.text(at)), slot: c.server.slotByPublic(c.memory.text(callback)) });
1013
+ return c.server.logEvents.length;
1014
+ },
1015
+ read_logdata: (c, [buffer, length]) => c.memory.setText(buffer, length, c.server.logLine),
1016
+ read_logargc: c => c.server.logArgs.length,
1017
+ read_logargv: (c, [index, buffer, length]) => c.memory.setText(buffer, length, c.server.logArgs[index] ?? ''),
1018
+ // A line logged through the engine, where the logevents hear it.
1019
+ elog_message: (c, [format, ...tail]) => {
1020
+ c.server.gameLog(formatPawn(c, c.memory.text(format), tail));
1021
+ return 1;
1022
+ },
1023
+ // register_event(event[], function[], flags[], cond[], ...): server.sendMessage() calls it once the message is sent.
1024
+ register_event: (c, [name, callback, _flags, ...conditions]) => {
1025
+ c.server.messageEvents.push({ name: c.memory.text(name), conditions: conditions.map(at => c.memory.text(at)), slot: c.server.slotByPublic(c.memory.text(callback)) });
1026
+ return c.server.messageEvents.length;
1027
+ },
1028
+ register_forward: (c, [fn, callback, post]) => {
1029
+ const key = `${fn}:${post ? 'post' : 'pre'}`;
1030
+ c.server.fakemetaForwards.set(key, [...(c.server.fakemetaForwards.get(key) ?? []), c.server.slotByPublic(c.memory.text(callback))]);
1031
+ return c.server.fakemetaForwards.size;
1032
+ },
1033
+ forward_return: (c, [type, value]) => {
1034
+ c.server.forwardAnswer = type === constant('FMV_STRING') ? c.memory.text(value) : c.memory.cell(value);
1035
+ return 1;
1036
+ },
1037
+ get_orig_retval: c => c.server.origRetval,
1038
+ register_clcmd: (c, [name, callback]) => {
1039
+ const command = c.memory.text(name).toLowerCase();
1040
+ c.server.clientCommands.set(command, [...(c.server.clientCommands.get(command) ?? []), c.server.slotByPublic(c.memory.text(callback))]);
1041
+ return 1;
1042
+ },
1043
+ // The engine's functions the hood calls: an event's index, and who hears whom.
1044
+ // The engine's and the game's functions, each argument of the tail by
1045
+ // address, as fakemeta reads them; what a plugin calls is in engineCalls.
1046
+ engfunc: (c, [type, ...tail]) => {
1047
+ const [cell, float, vector, text] = [(i: number) => c.memory.cell(tail[i]), (i: number) => c.memory.float(tail[i]), (i: number) => c.memory.vector(tail[i]).join(','), (i: number) => c.memory.text(tail[i])];
1048
+ const call = (line: string) => c.server.engineCalls.push(line);
1049
+ if (type === constant('EngFunc_PrecacheEvent')) return c.server.precached.indexOf(text(1)) + 1 || c.server.precached.push(text(1));
1050
+ if (type === constant('EngFunc_SetClientListening')) c.server.listening.push([cell(0), cell(1), cell(2)]);
1051
+ if (type === constant('EngFunc_PrecacheModel')) return c.server.precached.indexOf(text(0)) + 1 || c.server.precached.push(text(0));
1052
+ if (type === constant('EngFunc_TraceLine')) call(`TraceLine ${vector(0)} ${vector(1)} ${cell(2)} ${cell(3)} ${tail.length > 4 ? cell(4) : '-'}`);
1053
+ if (type === constant('EngFunc_VecToAngles')) c.memory.setVector(tail[1], [0, 90, 0]);
1054
+ if (type === constant('EngFunc_Time')) return floatBits(c.server.time / 1000);
1055
+ if (type === constant('EngFunc_CreateFakeClient')) {
1056
+ if (c.server.players.length >= c.server.maxPlayers) return 0;
1057
+ return c.server.join(text(0), { bot: true, team: 'UNASSIGNED', alive: false }).id;
1058
+ }
1059
+ if (type === constant('EngFunc_RunPlayerMove')) {
1060
+ const bot = player(c, cell(0));
1061
+ const [, yaw] = c.memory.vector(tail[1]);
1062
+ const [forward, msec] = [float(2), cell(7)];
1063
+ call(`RunPlayerMove ${cell(0)} ${vector(1)} ${forward} ${float(3)} ${float(4)} ${cell(5)} ${cell(6)} ${msec}`);
1064
+ if (bot) bot.origin = [bot.origin[0] + Math.cos(yaw * Math.PI / 180) * forward * msec / 1000, bot.origin[1] + Math.sin(yaw * Math.PI / 180) * forward * msec / 1000, bot.origin[2]];
1065
+ }
1066
+ return 0;
1067
+ },
1068
+ dllfunc: (c, [type, ...tail]) => {
1069
+ if (type === constant('DLLFunc_GetGameDescription')) c.memory.setText(tail[0], c.memory.cell(tail[1]), 'Counter-Strike');
1070
+ if (type === constant('DLLFunc_ClientConnect')) {
1071
+ c.server.engineCalls.push(`ClientConnect ${c.memory.cell(tail[0])} ${c.memory.text(tail[1])} ${c.memory.text(tail[2])}`);
1072
+ // The game turns away a name it does not like, and says why.
1073
+ if (!c.memory.text(tail[1]).startsWith('Rejected')) return 1;
1074
+ c.memory.setText(tail[3], c.memory.cell(tail[4]), 'No room for you');
1075
+ return 0;
1076
+ }
1077
+ if (type === constant('DLLFunc_ClientPutInServer')) c.server.engineCalls.push(`ClientPutInServer ${c.memory.cell(tail[0])}`);
1078
+ return 0;
1079
+ },
1080
+ global_get: (c, [field, ...tail]) => {
1081
+ if (field === constant('glb_frametime') && tail[0]) c.memory.setFloat(tail[0], 0.01);
1082
+ return 0;
1083
+ },
1084
+ get_ent_data: (c, [id, _class, member, element]) => fieldCell(entity(c, id) ?? player(c, id), constant(c.memory.text(member)), element),
1085
+ cs_get_user_money: (c, [id]) => fieldCell(player(c, id), constant('m_iAccount')),
1086
+ find_ent_by_model: (c, [start, classname, model]) => {
1087
+ const [wanted, path] = [c.memory.text(classname), c.memory.text(model)];
1088
+ const found = [...c.server.entities.values()].find(one => one.id > start && one.classname === wanted && one.get('var_model') === path);
1089
+ return found?.id ?? 0;
1090
+ },
1091
+ get_timeleft: () => 0,
904
1092
  CreateHudSyncObj: () => ++hudSyncObjects,
905
1093
  ShowSyncHudMsg: (c, [id, _sync, format, ...tail]) => sendText(c, id, 'hud', formatPawn(c, c.memory.text(format), tail)),
906
1094
  ClearSyncHud: () => 1,
@@ -921,5 +1109,11 @@ const SAME_AS: Record<string, string> = {
921
1109
  write_char: 'write_byte',
922
1110
  write_short: 'write_byte',
923
1111
  write_long: 'write_byte',
1112
+ // A message sent through the engine, which every plugin's message hooks hear (emessage_end).
1113
+ emessage_begin: 'message_begin',
1114
+ ewrite_byte: 'write_byte',
1115
+ ewrite_long: 'write_byte',
1116
+ ewrite_short: 'write_byte',
1117
+ ewrite_string: 'write_string',
924
1118
  };
925
1119
  for (const [name, same] of Object.entries(SAME_AS)) NATIVES[name] = NATIVES[same];