@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.
- package/README.md +21 -25
- package/README.ru.md +22 -24
- package/as/amxts.d.ts +63 -3
- package/as/constants.ts +684 -116
- package/as/entities.ts +1905 -1364
- package/as/events.ts +1696 -907
- package/as/facade.ts +1240 -220
- package/as/fetch.ts +1519 -0
- package/as/flags.ts +159 -145
- package/as/hlds.ts +1071 -0
- package/as/hooks.ts +5226 -5379
- package/as/kit.ts +15 -5
- package/as/lib/check.ts +1 -1
- package/as/natives.ts +2658 -2499
- package/as/remote.ts +39 -15
- package/as/tsconfig.json +7 -2
- package/as/vector.ts +12 -9
- package/includes/order.txt +22 -29
- package/includes/sources.json +0 -8
- package/node_modules/assemblyscript/std/assembly/index.d.ts +20 -11
- package/package.json +29 -7
- package/runtime/deps/assemblyscript/dist/asc.js +86 -16
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +29 -29
- package/runtime/licenses/MPL-2.0.txt +373 -0
- package/runtime/licenses/README.md +5 -1
- package/runtime/licenses/curl-COPYING +22 -0
- package/runtime/licenses/libssh2-COPYING +43 -0
- package/runtime/licenses/mbedTLS-LICENSE +553 -0
- package/runtime/natives.txt +108 -105
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +752 -296
- package/runtime/patches/wamr-2.4.5-amxts.patch +148 -0
- package/scripts/apply-docs.ts +2 -2
- package/scripts/auto-imports.ts +2 -2
- package/scripts/build-identity.ts +115 -0
- package/scripts/build-wasm.ts +101 -50
- package/scripts/check.ts +17 -3
- package/scripts/client-messages.ts +246 -0
- package/scripts/compile-cache.ts +4 -2
- package/scripts/compile-pool.ts +128 -0
- package/scripts/compile-worker.ts +23 -0
- package/scripts/compile.ts +156 -59
- package/scripts/docker-server.ts +13 -3
- package/scripts/docs/as/amxts.ts +86 -0
- package/scripts/docs/as/facade.ts +688 -188
- package/scripts/docs/as/fetch.ts +805 -0
- package/scripts/docs/entities.ts +43 -144
- package/scripts/docs/events.ts +17 -28
- package/scripts/docs/game.ts +68 -22
- package/scripts/docs/messages.ts +330 -16
- package/scripts/editor-api.ts +101 -0
- package/scripts/hlds-events.ts +370 -0
- package/scripts/includes.ts +12 -10
- package/scripts/player-fields.ts +16 -15
- package/scripts/prebuilt.ts +65 -23
- package/scripts/prepare.ts +42 -28
- package/scripts/project.ts +120 -20
- package/scripts/reapi-events.ts +98 -0
- package/scripts/shared-modules.ts +85 -23
- package/scripts/source-map.ts +242 -0
- package/scripts/system.ts +1 -1
- package/scripts/typed-commands.ts +263 -0
- package/scripts/typed-configs.ts +23 -12
- package/scripts/ui.ts +3 -1
- package/scripts/upgrade-menus.ts +274 -0
- package/scripts/upgrade-names.ts +384 -0
- package/scripts/upgrade.ts +857 -0
- package/src/cli-api.mjs +37 -4
- package/src/system.mjs +74 -3
- package/src/testing/compile.ts +8 -5
- package/src/testing/index.ts +2 -1
- package/src/testing/natives.ts +199 -5
- package/src/testing/network.ts +531 -0
- package/src/testing/server.ts +572 -48
- package/src/testing/setup.ts +1 -1
- package/src/testing/tables.ts +25 -0
- package/as/modules/http.ts +0 -184
- package/scripts/docs/as/modules/http.ts +0 -66
package/src/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
|
|
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 =
|
|
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`, `
|
|
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
|
}
|
package/src/testing/compile.ts
CHANGED
|
@@ -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,
|
|
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
|
|
107
|
-
* server - so an async function parks and resumes here as it
|
|
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 {
|
|
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
|
|
package/src/testing/index.ts
CHANGED
|
@@ -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
|
|
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.
|
package/src/testing/natives.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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)?.
|
|
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,
|
|
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];
|