@amxts/core 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/README.ru.md +5 -5
- package/as/amxts.d.ts +13 -3
- package/as/entities.ts +571 -501
- package/as/events.ts +3323 -557
- package/as/facade.ts +3079 -718
- package/as/fetch.ts +2 -2
- package/as/fs.ts +352 -59
- package/as/hlds.ts +121 -120
- package/as/hooks.ts +10585 -5177
- package/as/lib/check.ts +3 -2
- package/as/natives.ts +856 -1272
- package/as/promise.ts +37 -27
- package/as/vector.ts +94 -14
- package/includes/order.txt +17 -19
- package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
- package/package.json +9 -6
- package/runtime/deps/assemblyscript/dist/asc.js +136 -42
- package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
- package/runtime/licenses/README.md +9 -7
- package/runtime/licenses/ReGameDLL-LICENSE +21 -0
- package/runtime/licenses/ReHLDS-LICENSE +21 -0
- package/runtime/licenses/zlib-LICENSE +22 -0
- package/runtime/natives.txt +618 -523
- package/runtime/patches/README.md +87 -1
- package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
- package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
- package/scripts/auto-imports.ts +1 -1
- package/scripts/build-identity.ts +102 -65
- package/scripts/build-wasm.ts +17 -13
- package/scripts/check.ts +4 -1
- package/scripts/client-messages.ts +73 -1
- package/scripts/compile-cache.ts +11 -8
- package/scripts/compile-pool.ts +6 -3
- package/scripts/compile-worker.ts +23 -23
- package/scripts/compile.ts +142 -19
- package/scripts/docs/as/amxts.ts +10 -4
- package/scripts/docs/as/facade.ts +1779 -275
- package/scripts/docs/as/fetch.ts +2 -2
- package/scripts/docs/as/fs.ts +190 -6
- package/scripts/docs/as/vector.ts +81 -0
- package/scripts/docs/entities.ts +12 -0
- package/scripts/docs/events.ts +49 -13
- package/scripts/docs/game.ts +6 -0
- package/scripts/docs/messages.ts +10 -1
- package/scripts/hlds-events.ts +11 -11
- package/scripts/includes.ts +3 -4
- package/scripts/leaf-natives.ts +84 -0
- package/scripts/player-fields.ts +1 -1
- package/scripts/plugin-natives.ts +1 -1
- package/scripts/prebuilt.ts +42 -11
- package/scripts/project.ts +63 -4
- package/scripts/reapi-events.ts +9 -9
- package/scripts/server-env.ts +145 -0
- package/scripts/shared-modules.ts +23 -7
- package/scripts/system.ts +1 -1
- package/scripts/tracked-fs.ts +11 -2
- package/scripts/typed-commands.ts +153 -15
- package/scripts/upgrade-calls.ts +225 -0
- package/scripts/upgrade-names.ts +1 -4
- package/scripts/upgrade.ts +47 -34
- package/src/cli-api.mjs +3 -2
- package/src/system.mjs +21 -11
- package/src/testing/compile.ts +31 -8
- package/src/testing/coroutines.ts +0 -10
- package/src/testing/memory.ts +89 -2
- package/src/testing/natives.ts +67 -30
- package/src/testing/server.ts +1094 -155
- package/src/testing/tables.ts +23 -6
- package/src/typecheck.mjs +110 -0
- package/runtime/licenses/Bun-LICENSE.md +0 -84
package/scripts/build-wasm.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { createSocket } from 'node:dgram';
|
|
|
35
35
|
// generic one. Both
|
|
36
36
|
// must come from the same WAMR release as the module, or the loader reports
|
|
37
37
|
// "unknown binary version" (CONTRIBUTING.md builds both from one checkout).
|
|
38
|
-
import { copyFileSync,
|
|
38
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, watch, writeFileSync } from 'node:fs';
|
|
39
39
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
40
40
|
import { includePath } from './compile';
|
|
41
41
|
import { compileAll } from './compile-pool';
|
|
@@ -45,7 +45,8 @@ import { serverIncludes } from './includes';
|
|
|
45
45
|
import { pluginCache } from './plugin-cache';
|
|
46
46
|
import { includeName } from './plugin-natives';
|
|
47
47
|
import { fromRegistry, prebuiltOf, prebuiltSurface } from './prebuilt';
|
|
48
|
-
import { CORE_DIR, CORE_PLUGINS, loadProject, modulesInUse, pluginList, projectPlugins, sourcesFor } from './project';
|
|
48
|
+
import { CORE_DIR, CORE_PLUGINS, loadProject, modulesInUse, pluginList, projectPlugins, sourcesFor, staleCopies } from './project';
|
|
49
|
+
import { aotSection, ENV_SECTION, envProblems, parseDotenv } from './server-env';
|
|
49
50
|
import { sharedModulesBuild } from './shared-modules';
|
|
50
51
|
import { describeSystem, serverFolder, serverSystem, SYSTEM_NAME, WAMRC_PACKAGE, wamrcPath } from './system';
|
|
51
52
|
import { c, live, log, progress, since } from './ui';
|
|
@@ -162,6 +163,10 @@ function ownPlugins(): Target[] {
|
|
|
162
163
|
return projectPlugins(project).map(source => ({ source, name: basename(source).replace(/\.ts$/, '') }));
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
for (const copy of staleCopies(project)) {
|
|
167
|
+
log.warn(`${shown(copy)} is a copy of amxts's own API from an older version, not a plugin: delete it (npx amxts upgrade does)`);
|
|
168
|
+
}
|
|
169
|
+
|
|
165
170
|
for (const plugin of ownPlugins()) {
|
|
166
171
|
if (project.modules.some(pkg => pkg.short === plugin.name)) {
|
|
167
172
|
fail(`${shown(plugin.source)}: a module in amxts.config.ts is called ${plugin.name} too`, 'Rename the plugin.');
|
|
@@ -402,6 +407,16 @@ async function deployAndReload(built: string[], names: string): Promise<string>
|
|
|
402
407
|
if (before && byRcon) utimesSync(target, before.atime, before.mtime);
|
|
403
408
|
}
|
|
404
409
|
|
|
410
|
+
// A variable a plugin requires that the server's .env has not, or has of
|
|
411
|
+
// another kind: the module refuses the plugin unless the server's
|
|
412
|
+
// environment, which this cannot see, has it (scripts/server-env.ts).
|
|
413
|
+
const dotenvPath = join(serverDir, '.env');
|
|
414
|
+
const dotenv = parseDotenv(existsSync(dotenvPath) ? readFileSync(dotenvPath, 'utf8') : '');
|
|
415
|
+
for (const file of built) {
|
|
416
|
+
const missing = envProblems((aotSection(readFileSync(join(outDir, file)), ENV_SECTION) ?? '').split('\n').filter(Boolean), dotenv);
|
|
417
|
+
if (missing.length) log.warn(`${file.replace(/\.aot$/, '')} needs ${missing.join(', ')}, which ${shown(dotenvPath)} has not: the server refuses it unless its environment has it`);
|
|
418
|
+
}
|
|
419
|
+
|
|
405
420
|
// A plugin that exports natives has an include for Pawn plugins beside it;
|
|
406
421
|
// it goes where amxxpc on the server looks, when the server has one.
|
|
407
422
|
const pawnIncludes = join(serverDir, '..', 'amxmodx', 'scripting', 'include');
|
|
@@ -412,17 +427,6 @@ async function deployAndReload(built: string[], names: string): Promise<string>
|
|
|
412
427
|
// The module's own natives for Pawn: the fields plugins add to Player.
|
|
413
428
|
if (existsSync(pawnIncludes)) copyFileSync(join(CORE_DIR, 'runtime/host/amxts.inc'), join(pawnIncludes, 'amxts.inc'));
|
|
414
429
|
|
|
415
|
-
// A folder beside the plugins is a library - `~/lib/thing`, `~/myplugin/config`.
|
|
416
|
-
// It is compiled into the .aot already, so nothing here needs it; a .ts
|
|
417
|
-
// plugin written on the server does, and it was silently compiling against
|
|
418
|
-
// whatever copy happened to be there.
|
|
419
|
-
const ownFolder = resolve(sourceDir) !== resolve(CORE_PLUGINS) || resolve(project.dir) === resolve(CORE_DIR);
|
|
420
|
-
for (const entry of ownFolder ? readdirSync(sourceDir, { withFileTypes: true }) : []) {
|
|
421
|
-
if (entry.isDirectory()) {
|
|
422
|
-
cpSync(join(sourceDir, entry.name), join(serverDir, 'plugins', entry.name), { recursive: true });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
430
|
if (project.config) {
|
|
427
431
|
writeFileSync(listPath, serverList(existing, readFileSync(join(outDir, 'plugins.ini'), 'utf8')));
|
|
428
432
|
} else if (existing.length === 0) {
|
package/scripts/check.ts
CHANGED
|
@@ -38,7 +38,10 @@ try {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (!pkg && problems.length === 0) {
|
|
41
|
-
|
|
41
|
+
// A project: its plugins are checked by typecheck and build, not by this.
|
|
42
|
+
fail(existsSync(join(dir, 'amxts.config.ts'))
|
|
43
|
+
? 'amxts check checks a module package before it is published; in a project, `npx amxts typecheck` checks the plugins and `npx amxts build` builds them'
|
|
44
|
+
: 'amxts check: this folder is not a module package - package.json has no "amxts": { "module": ... }');
|
|
42
45
|
process.exit(1);
|
|
43
46
|
}
|
|
44
47
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The messages Counter-Strike's server sends its clients, as
|
|
2
2
|
// server.addMessageListener(name, ...) hears them - read by
|
|
3
|
-
// scripts/generate-
|
|
3
|
+
// scripts/generate-image.ts, which writes their events into as/events.ts.
|
|
4
4
|
//
|
|
5
5
|
// Every name the game registers is a message a plugin can listen to, by the
|
|
6
6
|
// name MESSAGE_NAMES gives it in the player's words: `death`, not `DeathMsg`.
|
|
@@ -129,10 +129,82 @@ export const MESSAGE_FIELDS: Record<string, MessageField[]> = {
|
|
|
129
129
|
f('slot', 6, 'number'),
|
|
130
130
|
f('position', 7, 'number'),
|
|
131
131
|
f('weapon', 8, 'weapon'),
|
|
132
|
+
f('flags', 9, 'number'),
|
|
132
133
|
],
|
|
133
134
|
WeapPickup: [f('weapon', 1, 'weapon')],
|
|
134
135
|
};
|
|
135
136
|
|
|
137
|
+
/** How a message's argument is written: `write_byte`, `write_short` and the rest; `string*` every text from there on. */
|
|
138
|
+
export type MessageWrite = 'byte' | 'char' | 'short' | 'long' | 'coord' | 'angle' | 'string' | 'string*';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The messages a plugin can send by name (player.send, server.send): each
|
|
142
|
+
* argument as the game writes it, in order - ReGameDLL's MESSAGE_BEGIN
|
|
143
|
+
* sites. A message is here only when its fields name every argument, so
|
|
144
|
+
* what an author gives is the whole message; `=n` is an argument the game
|
|
145
|
+
* always writes as `n` (ScoreInfo's class).
|
|
146
|
+
*/
|
|
147
|
+
export const MESSAGE_WRITES: Record<string, string> = {
|
|
148
|
+
ADStop: '',
|
|
149
|
+
AllowSpec: 'byte',
|
|
150
|
+
AmmoPickup: 'byte byte',
|
|
151
|
+
AmmoX: 'byte byte',
|
|
152
|
+
ArmorType: 'byte',
|
|
153
|
+
BarTime: 'short',
|
|
154
|
+
BarTime2: 'short short',
|
|
155
|
+
Battery: 'short',
|
|
156
|
+
BlinkAcct: 'byte',
|
|
157
|
+
BombDrop: 'coord coord coord byte',
|
|
158
|
+
BombPickup: '',
|
|
159
|
+
BotVoice: 'byte byte',
|
|
160
|
+
BuyClose: '',
|
|
161
|
+
Crosshair: 'byte',
|
|
162
|
+
CurWeapon: 'byte byte byte',
|
|
163
|
+
Damage: 'byte byte long coord coord coord',
|
|
164
|
+
DeathMsg: 'byte byte byte string',
|
|
165
|
+
Flashlight: 'byte byte',
|
|
166
|
+
FlashBat: 'byte',
|
|
167
|
+
Geiger: 'byte',
|
|
168
|
+
Health: 'byte',
|
|
169
|
+
HideWeapon: 'byte',
|
|
170
|
+
HostageK: 'byte',
|
|
171
|
+
InitHUD: '',
|
|
172
|
+
ItemPickup: 'string',
|
|
173
|
+
ItemStatus: 'byte',
|
|
174
|
+
Location: 'byte string',
|
|
175
|
+
Money: 'long byte',
|
|
176
|
+
MOTD: 'byte string',
|
|
177
|
+
NVGToggle: 'byte',
|
|
178
|
+
Radar: 'byte coord coord coord',
|
|
179
|
+
ResetHUD: '',
|
|
180
|
+
RoundTime: 'short',
|
|
181
|
+
SayText: 'byte string string*',
|
|
182
|
+
Scenario: 'byte string byte',
|
|
183
|
+
ScoreAttrib: 'byte byte',
|
|
184
|
+
ScoreInfo: 'byte short short short=0 short',
|
|
185
|
+
ScreenFade: 'short short short byte byte byte byte',
|
|
186
|
+
ScreenShake: 'short short short',
|
|
187
|
+
SendAudio: 'byte string short',
|
|
188
|
+
ServerName: 'string',
|
|
189
|
+
SetFOV: 'byte',
|
|
190
|
+
ShowTimer: '',
|
|
191
|
+
SpecHealth: 'byte',
|
|
192
|
+
SpecHealth2: 'byte byte',
|
|
193
|
+
Spectator: 'byte byte',
|
|
194
|
+
StatusIcon: 'byte string byte byte byte',
|
|
195
|
+
StatusText: 'byte string',
|
|
196
|
+
StatusValue: 'byte short',
|
|
197
|
+
TaskTime: 'short byte byte',
|
|
198
|
+
TeamInfo: 'byte string',
|
|
199
|
+
TeamScore: 'string short',
|
|
200
|
+
TextMsg: 'byte string string*',
|
|
201
|
+
Train: 'byte',
|
|
202
|
+
TutorClose: '',
|
|
203
|
+
ViewMode: '',
|
|
204
|
+
WeaponList: 'string byte byte byte byte byte byte byte byte',
|
|
205
|
+
WeapPickup: 'byte',
|
|
206
|
+
};
|
|
207
|
+
|
|
136
208
|
/** Every message Counter-Strike 1.6 registers, by the name get_user_msgid takes. */
|
|
137
209
|
export const CLIENT_MESSAGES = `
|
|
138
210
|
ADStop AllowSpec AmmoPickup AmmoX ArmorType BarTime BarTime2 Battery BlinkAcct BombDrop BombPickup
|
package/scripts/compile-cache.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// Compiles kept on disk, reused while what they read is the same.
|
|
2
2
|
//
|
|
3
3
|
// A compile is keyed by everything that is not a file it read - the
|
|
4
|
-
// compiler, the hood's scripts, the version the core builds as
|
|
5
|
-
// carries it in its ABI), the flags, the project and its config -
|
|
6
|
-
// stored with every file read it made (scripts/tracked-fs.ts): the entry,
|
|
7
|
-
// it imports, the facade and the generated API, the module packages
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// compiler, the hood's scripts, the line of the version the core builds as
|
|
5
|
+
// (a plugin carries it in its ABI), the flags, the project and its config -
|
|
6
|
+
// and stored with every file read it made (scripts/tracked-fs.ts): the entry,
|
|
7
|
+
// what it imports, the facade and the generated API, the module packages,
|
|
8
|
+
// the files the ABI's hash is made of. It is taken again only while each of
|
|
9
|
+
// those reads sees the same thing it saw, so a hit is what compiling again
|
|
10
|
+
// would give, byte for byte - but for the patch of the version in its ABI,
|
|
11
|
+
// which the module does not compare (scripts/build-identity.ts): a core's
|
|
12
|
+
// patch release with the same hood takes the plugins of the one before.
|
|
10
13
|
//
|
|
11
14
|
// One key holds one entry: a compile made again replaces the one before, so a
|
|
12
15
|
// cache does not grow with every edit. The testing library keeps its compiles
|
|
@@ -16,7 +19,7 @@ import type { Reads } from './tracked-fs';
|
|
|
16
19
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
17
20
|
import { dirname, join, resolve } from 'node:path';
|
|
18
21
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { coreVersion } from './build-identity';
|
|
22
|
+
import { coreVersion, releaseLine } from './build-identity';
|
|
20
23
|
import { listIncludes } from './includes';
|
|
21
24
|
import { currentProjectDir, loadProject } from './project';
|
|
22
25
|
import { hashOf, recordReads, unchanged } from './tracked-fs';
|
|
@@ -120,7 +123,7 @@ export function diskCache(dir: string | null, code: () => string, ownIncludes: s
|
|
|
120
123
|
let identity: string | null = null;
|
|
121
124
|
const fileOf = (parts: unknown[]) => {
|
|
122
125
|
identity ??= hashOf(`${FORMAT}\n${code()}`);
|
|
123
|
-
return join(dir!, `${hashOf(JSON.stringify([identity, coreVersion(), projectIdentity(ownIncludes), ...parts]))}.json`);
|
|
126
|
+
return join(dir!, `${hashOf(JSON.stringify([identity, releaseLine(coreVersion()), projectIdentity(ownIncludes), ...parts]))}.json`);
|
|
124
127
|
};
|
|
125
128
|
|
|
126
129
|
let pruned = false;
|
package/scripts/compile-pool.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import type { Plugin } from './compile';
|
|
8
8
|
import type { NativesBeside, PluginNative } from './plugin-natives';
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import { availableParallelism } from 'node:os';
|
|
10
|
+
import { availableParallelism, constants, setPriority } from 'node:os';
|
|
11
11
|
import { dirname, join } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { setNativesBeside } from './plugin-natives';
|
|
@@ -66,8 +66,9 @@ export interface WorkerJob {
|
|
|
66
66
|
export type WorkerResult = Compiled & { beside: NativesBeside };
|
|
67
67
|
|
|
68
68
|
/** A worker process: one job at a time, until it is closed. */
|
|
69
|
-
function worker() {
|
|
69
|
+
function worker(low: boolean) {
|
|
70
70
|
const child = spawn(process.execPath, [WORKER], { stdio: ['ignore', 'inherit', 'inherit', 'ipc'] });
|
|
71
|
+
if (low) setPriority(child.pid!, constants.priority.PRIORITY_BELOW_NORMAL);
|
|
71
72
|
let waiting: { resolve: (result: WorkerResult) => void; reject: (error: Error) => void } | null = null;
|
|
72
73
|
const answer = () => {
|
|
73
74
|
const now = waiting;
|
|
@@ -93,6 +94,8 @@ export interface CompileAll {
|
|
|
93
94
|
here: (plugin: Plugin, natives: PluginNative[]) => Promise<string | null>;
|
|
94
95
|
started?: (index: number) => void;
|
|
95
96
|
finished?: (index: number, compiled: Compiled) => void;
|
|
97
|
+
/** The workers below the machine's other work: the test server's build, which no one waits on at the keyboard. */
|
|
98
|
+
low?: boolean;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
@@ -102,7 +105,7 @@ export interface CompileAll {
|
|
|
102
105
|
*/
|
|
103
106
|
export async function compileAll(plugins: Plugin[], options: CompileAll): Promise<(Compiled | null)[]> {
|
|
104
107
|
const slots = Math.min(plugins.length, compileSlots());
|
|
105
|
-
const workers = slots > 1 ? Array.from({ length: slots }, worker) : [];
|
|
108
|
+
const workers = slots > 1 ? Array.from({ length: slots }, () => worker(options.low ?? false)) : [];
|
|
106
109
|
let failed = false;
|
|
107
110
|
const compile = async (plugin: Plugin, slot: number): Promise<Compiled> => {
|
|
108
111
|
if (workers.length === 0) {
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
// A compile process of scripts/compile-pool.ts: compiles the plugins it is
|
|
2
|
-
// sent, one at a time, through the build's plugin cache (scripts/plugin-cache.ts),
|
|
3
|
-
// and answers each with what went wrong and the plugin's natives. It ends when
|
|
4
|
-
// the build disconnects.
|
|
5
|
-
import type { WorkerJob, WorkerResult } from './compile-pool';
|
|
6
|
-
import type { PluginNative } from './plugin-natives';
|
|
7
|
-
import { pluginCache } from './plugin-cache';
|
|
8
|
-
import { nativesBeside } from './plugin-natives';
|
|
9
|
-
|
|
10
|
-
const caches = new Map<string, ReturnType<typeof pluginCache>>();
|
|
11
|
-
|
|
12
|
-
process.on('message', async ({ plugin, dir, includes }: WorkerJob) => {
|
|
13
|
-
const key = JSON.stringify([dir, includes]);
|
|
14
|
-
if (!caches.has(key)) caches.set(key, pluginCache(dir, includes));
|
|
15
|
-
const natives: PluginNative[] = [];
|
|
16
|
-
let problem: string | null;
|
|
17
|
-
try {
|
|
18
|
-
problem = await caches.get(key)!.compile(plugin, natives);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
problem = String((error as Error).stack ?? error);
|
|
21
|
-
}
|
|
22
|
-
process.send!({ problem, natives, beside: nativesBeside(natives) } satisfies WorkerResult);
|
|
23
|
-
});
|
|
1
|
+
// A compile process of scripts/compile-pool.ts: compiles the plugins it is
|
|
2
|
+
// sent, one at a time, through the build's plugin cache (scripts/plugin-cache.ts),
|
|
3
|
+
// and answers each with what went wrong and the plugin's natives. It ends when
|
|
4
|
+
// the build disconnects.
|
|
5
|
+
import type { WorkerJob, WorkerResult } from './compile-pool';
|
|
6
|
+
import type { PluginNative } from './plugin-natives';
|
|
7
|
+
import { pluginCache } from './plugin-cache';
|
|
8
|
+
import { nativesBeside } from './plugin-natives';
|
|
9
|
+
|
|
10
|
+
const caches = new Map<string, ReturnType<typeof pluginCache>>();
|
|
11
|
+
|
|
12
|
+
process.on('message', async ({ plugin, dir, includes }: WorkerJob) => {
|
|
13
|
+
const key = JSON.stringify([dir, includes]);
|
|
14
|
+
if (!caches.has(key)) caches.set(key, pluginCache(dir, includes));
|
|
15
|
+
const natives: PluginNative[] = [];
|
|
16
|
+
let problem: string | null;
|
|
17
|
+
try {
|
|
18
|
+
problem = await caches.get(key)!.compile(plugin, natives);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
problem = String((error as Error).stack ?? error);
|
|
21
|
+
}
|
|
22
|
+
process.send!({ problem, natives, beside: nativesBeside(natives) } satisfies WorkerResult);
|
|
23
|
+
});
|
package/scripts/compile.ts
CHANGED
|
@@ -6,24 +6,26 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
|
|
7
7
|
// Compiles one plugin: .ts in, .aot out.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// AssemblyScript's and lives in the readFile hook below.
|
|
9
|
+
// Every build compiles through this one function - `amxts build` and `dev`,
|
|
10
|
+
// the tests, the modules' prebuilt plugins - so that a plugin compiles the
|
|
11
|
+
// same way everywhere. In particular the `~/` alias, which is ours rather
|
|
12
|
+
// than AssemblyScript's and lives in the readFile hook below.
|
|
13
13
|
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { basename, dirname, relative, resolve } from 'node:path';
|
|
15
|
-
// The Binaryen asc itself runs on - the same copy
|
|
16
|
-
//
|
|
15
|
+
// The Binaryen asc itself runs on - the same copy - for the one pass asc
|
|
16
|
+
// does not run: Asyncify.
|
|
17
17
|
// @ts-ignore - shipped as JavaScript, with types beside it we do not need here
|
|
18
18
|
import * as assemblyscript from '../runtime/deps/assemblyscript/dist/assemblyscript.js';
|
|
19
19
|
// @ts-ignore - its types are beside it, under a path tsconfig does not map
|
|
20
20
|
import binaryen from '../runtime/deps/assemblyscript/node_modules/binaryen/index.js';
|
|
21
21
|
import { ascMain } from './asc';
|
|
22
|
-
import { ABI_SECTION,
|
|
22
|
+
import { ABI_SECTION, nativesFile, pluginAbi } from './build-identity';
|
|
23
|
+
import { LEAF_NATIVES } from './leaf-natives';
|
|
23
24
|
import { playerFieldsBuild } from './player-fields';
|
|
24
25
|
import { includeName, nativeContract, nativesTransform, pawnInclude } from './plugin-natives';
|
|
25
26
|
import { ascPath, sourcesFor } from './project';
|
|
26
27
|
import { targetTransforms } from './reapi-events';
|
|
28
|
+
import { ENV_SECTION, envBuild } from './server-env';
|
|
27
29
|
import { sharedModulesBuild } from './shared-modules';
|
|
28
30
|
import { displayName, MAP_SECTION, mapText, pluginMap, withSection } from './source-map';
|
|
29
31
|
import { HOST_SYSTEM, TARGET_ABI } from './system';
|
|
@@ -53,6 +55,9 @@ export interface Plugin {
|
|
|
53
55
|
/** Binaryen's optimisation level: a quick build's, and a full one's - asc's own --optimize. */
|
|
54
56
|
const OPTIMIZE = { quick: 1, full: 3 };
|
|
55
57
|
|
|
58
|
+
/** The size, in Binaryen's nodes, up to which a full build inlines any function (inlineSmall). */
|
|
59
|
+
const INLINE_SIZE = 30;
|
|
60
|
+
|
|
56
61
|
/** The hood's exports every plugin carries; see compilePlugin. */
|
|
57
62
|
export const HOOD_EXPORTS = '__amxts_exports.ts';
|
|
58
63
|
|
|
@@ -80,6 +85,9 @@ export const ASYNC_EXPORTS = BASE_EXPORTS + [
|
|
|
80
85
|
'',
|
|
81
86
|
].join('\n');
|
|
82
87
|
|
|
88
|
+
/** The names ASYNC_EXPORTS exports. */
|
|
89
|
+
const ASYNC_NAMES = [...ASYNC_EXPORTS.matchAll(/export function (\w+)/g)].map(match => match[1]);
|
|
90
|
+
|
|
83
91
|
/**
|
|
84
92
|
* A transform that moves the imports at the end of a file - the ones the
|
|
85
93
|
* build adds (scripts/auto-imports.ts), after the file's last line - to its
|
|
@@ -104,6 +112,37 @@ export const SUSPEND_IMPORT = 'env.co_suspend';
|
|
|
104
112
|
/** The import of a plugin that makes a promise: it is compiled again with ASYNC_EXPORTS. */
|
|
105
113
|
const WAKE_IMPORT = 'env.co_wake';
|
|
106
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The imports that use the plugin's memory they are given only while they
|
|
117
|
+
* run, and keep no address of it: an object whose address goes only to them
|
|
118
|
+
* can be made in its function's frame (makeStackObjects).
|
|
119
|
+
*/
|
|
120
|
+
const BORROWING_IMPORTS = new Set(['env.ent_vector']);
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The arguments of Pawn's natives that the module's thunk copies - text in
|
|
124
|
+
* or out, an array - and so keeps no address of, by the native's import:
|
|
125
|
+
* read off runtime/natives.txt. An object whose address goes only there, a field of the
|
|
126
|
+
* options a function reads, is no reason to keep it on the heap.
|
|
127
|
+
*/
|
|
128
|
+
const COPIED_ARGUMENTS: ReadonlyMap<string, number[]> = (() => {
|
|
129
|
+
const file = nativesFile();
|
|
130
|
+
if (!file) return new Map();
|
|
131
|
+
return new Map(readFileSync(file, 'utf8').split(/\r?\n/).flatMap((line) => {
|
|
132
|
+
const [name, , crossing] = line.split(' ');
|
|
133
|
+
if (!crossing || line.startsWith('#') || crossing === 'leaf') return [];
|
|
134
|
+
return [[`env.${name}`, crossing.split(',').flatMap((each, index) => (each === 'v' ? [] : [index]))]];
|
|
135
|
+
}));
|
|
136
|
+
})();
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The imports that never run the plugin's code - the abort, which does not
|
|
140
|
+
* return, and those marked `@leaf` - so a call to one runs no step of the
|
|
141
|
+
* collector: a function whose objects are all made in its frame and that
|
|
142
|
+
* calls nothing else keeps no shadow stack (makeStackObjects).
|
|
143
|
+
*/
|
|
144
|
+
const QUIET_IMPORTS = new Set(['env.abort', 'env.ent_vector', ...[...LEAF_NATIVES].map(name => `env.${name}`)]);
|
|
145
|
+
|
|
107
146
|
/**
|
|
108
147
|
* What wamrc keeps for a failed call's stack (scripts/source-map.ts): each
|
|
109
148
|
* function's frame with its index and the offset of the call it is in, or of
|
|
@@ -116,11 +155,11 @@ const WAKE_IMPORT = 'env.co_wake';
|
|
|
116
155
|
const STACK_FLAGS = ['--enable-dump-call-stack', '--call-stack-features=bounds-checks,ip,func-idx,trap-ip'];
|
|
117
156
|
|
|
118
157
|
/**
|
|
119
|
-
* The map goes into the .aot either way, and so
|
|
120
|
-
* compiled against, which the module checks
|
|
121
|
-
* (scripts/build-identity.ts).
|
|
158
|
+
* The map goes into the .aot either way, and so do the ABI the plugin is
|
|
159
|
+
* compiled against and the variables it requires, which the module checks
|
|
160
|
+
* before it loads it (scripts/build-identity.ts, scripts/server-env.ts).
|
|
122
161
|
*/
|
|
123
|
-
const SECTIONS = `--emit-custom-sections=${MAP_SECTION},${ABI_SECTION}`;
|
|
162
|
+
const SECTIONS = `--emit-custom-sections=${MAP_SECTION},${ABI_SECTION},${ENV_SECTION}`;
|
|
124
163
|
|
|
125
164
|
/**
|
|
126
165
|
* The CPU the machine code is for: a Pentium 4's, SSE2. LLVM's i386 left
|
|
@@ -234,18 +273,23 @@ export async function compileToWasm(
|
|
|
234
273
|
// A quick build leaves the optimising to one Binaryen pass afterwards.
|
|
235
274
|
const flags = [...(plugin.quick ? [] : ['--optimize']), ...(names ? ['--debug'] : []), '--sourceMap'];
|
|
236
275
|
const level = plugin.quick ? OPTIMIZE.quick : null;
|
|
237
|
-
|
|
276
|
+
// A quick build compiles every plugin with the scheduler's exports, once,
|
|
277
|
+
// and one that never waits loses them (finishing).
|
|
278
|
+
const hoodExports = plugin.quick ? ASYNC_EXPORTS : BASE_EXPORTS;
|
|
279
|
+
let made = await compileWasm(plugin, hoodExports, flags, level, natives);
|
|
238
280
|
|
|
239
281
|
// A plugin that makes a promise - an async function, fetch, sleep - has
|
|
240
|
-
// the host drive its jobs, so
|
|
241
|
-
// exports
|
|
242
|
-
|
|
282
|
+
// the host drive its jobs, so a full build compiles it again with the
|
|
283
|
+
// scheduler's exports: asc's optimiser has the whole program, and one
|
|
284
|
+
// compiled with them would come out otherwise.
|
|
285
|
+
if (hoodExports === BASE_EXPORTS && typeof made !== 'string' && importsOf(made.binary).has(WAKE_IMPORT)) {
|
|
243
286
|
if (natives) natives.length = 0;
|
|
244
287
|
made = await compileWasm(plugin, ASYNC_EXPORTS, flags, level, natives);
|
|
245
288
|
}
|
|
246
289
|
if (typeof made === 'string') return made;
|
|
247
290
|
mkdirSync(dirname(resolve(wasm)), { recursive: true });
|
|
248
|
-
|
|
291
|
+
const sections = withSection(withSection(made.binary, MAP_SECTION, mapText(made.map)), ABI_SECTION, pluginAbi(importsOf(made.binary)));
|
|
292
|
+
writeFileSync(wasm, made.env.length ? withSection(sections, ENV_SECTION, made.env.join('\n')) : sections);
|
|
249
293
|
return null;
|
|
250
294
|
}
|
|
251
295
|
|
|
@@ -265,10 +309,26 @@ export async function compileToWasm(
|
|
|
265
309
|
export function finishing(hoodExports: string, level: number | null, done: (first: number, names: string[]) => void = () => {}) {
|
|
266
310
|
return {
|
|
267
311
|
afterCompile(module: any) {
|
|
312
|
+
// Read before asc's optimiser, which folds the global away.
|
|
313
|
+
const dataEnd = (binaryen.getExpressionInfo(binaryen.getGlobalInfo(module.getGlobal(DATA_END)).init) as binaryen.ConstInfo).value as number;
|
|
268
314
|
const emit = module.emitBinary.bind(module);
|
|
269
315
|
module.emitBinary = (url?: string) => {
|
|
270
|
-
|
|
316
|
+
let imports = new Set(functionsOf(module).map(each => each.imported));
|
|
317
|
+
// A quick build's plugin that never waits, compiled with the
|
|
318
|
+
// scheduler's exports: they go, and what only they reached.
|
|
319
|
+
if (hoodExports === ASYNC_EXPORTS && level !== null && !imports.has(WAKE_IMPORT)) {
|
|
320
|
+
for (const name of ASYNC_NAMES) module.removeExport(name);
|
|
321
|
+
module.runPasses(['remove-unused-module-elements']);
|
|
322
|
+
imports = new Set(functionsOf(module).map(each => each.imported));
|
|
323
|
+
}
|
|
271
324
|
if (hoodExports === ASYNC_EXPORTS || !imports.has(WAKE_IMPORT)) {
|
|
325
|
+
exportCallGlobals(module);
|
|
326
|
+
if (level === null) {
|
|
327
|
+
inlineSmall(module);
|
|
328
|
+
// A number that only ever holds a whole number within i32 is kept in an i32.
|
|
329
|
+
assemblyscript.keepWholeNumbers(module.ptr);
|
|
330
|
+
makeStackObjects(module, dataEnd);
|
|
331
|
+
}
|
|
272
332
|
if (imports.has(SUSPEND_IMPORT)) asyncify(module, level ?? OPTIMIZE.full);
|
|
273
333
|
else if (level !== null) optimize(module, level);
|
|
274
334
|
}
|
|
@@ -292,6 +352,60 @@ function functionsOf(module: any): { name: string; imported: string | null }[] {
|
|
|
292
352
|
return ordered.map(info => ({ name: info.name, imported: info.module ? `${info.module}.${info.base}` : null }));
|
|
293
353
|
}
|
|
294
354
|
|
|
355
|
+
/**
|
|
356
|
+
* A full build's small functions inlined where they are called. A function
|
|
357
|
+
* that keeps a shadow-stack frame calls ~stack_check, its guard, and
|
|
358
|
+
* Binaryen does not inline a function that calls another unless it is
|
|
359
|
+
* tiny - so a small function with a frame stayed a call.
|
|
360
|
+
*/
|
|
361
|
+
function inlineSmall(module: any) {
|
|
362
|
+
const always = binaryen.getAlwaysInlineMaxSize();
|
|
363
|
+
binaryen.setOptimizeLevel(OPTIMIZE.full);
|
|
364
|
+
binaryen.setShrinkLevel(0);
|
|
365
|
+
binaryen.setAlwaysInlineMaxSize(INLINE_SIZE);
|
|
366
|
+
module.runPasses(['inlining-optimizing']);
|
|
367
|
+
binaryen.setAlwaysInlineMaxSize(always);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Where the plugin's static data ends, and its shadow stack's bottom. */
|
|
371
|
+
const DATA_END = '~lib/memory/__data_end';
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A full build's objects that never leave the function that makes them,
|
|
375
|
+
* made in its frame rather than on the heap - `player.origin.x` and the
|
|
376
|
+
* options literal of `player.showHud(text, { hold: 2 })` allocate nothing -
|
|
377
|
+
* by the compiler's pass over the inlined code, which knows which
|
|
378
|
+
* parameters each function keeps. A function an async function's coroutine
|
|
379
|
+
* may park in keeps its objects on the heap.
|
|
380
|
+
*/
|
|
381
|
+
function makeStackObjects(module: any, dataEnd: number) {
|
|
382
|
+
const functions = functionsOf(module);
|
|
383
|
+
const named = (imports: Set<string>) => functions.filter(each => each.imported && imports.has(each.imported)).map(each => each.name);
|
|
384
|
+
// An import's name with an argument's index, for one argument.
|
|
385
|
+
const copied = functions.flatMap(each => (COPIED_ARGUMENTS.get(each.imported ?? '') ?? []).map(index => `${each.name}#${index}`));
|
|
386
|
+
assemblyscript.makeStackObjects(module.ptr, [...named(BORROWING_IMPORTS), ...copied], named(new Set([SUSPEND_IMPORT])), dataEnd, named(QUIET_IMPORTS));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The globals a call of a function value sets, which the module sets too
|
|
391
|
+
* when it calls a listener itself rather than the plugin's walk of the
|
|
392
|
+
* listeners (the module's Handler.via): the closure's variables, how many
|
|
393
|
+
* arguments the call passes, the player an async function started there runs
|
|
394
|
+
* under. Exported under the names the module looks up; one the plugin has
|
|
395
|
+
* not got is not needed.
|
|
396
|
+
*/
|
|
397
|
+
const CALL_GLOBALS = [
|
|
398
|
+
['~lib/function/__env', '__env'],
|
|
399
|
+
['~argumentsLength', '__argumentsLength'],
|
|
400
|
+
['~lib/~/promise/__co_ambient_player', '__co_ambient_player'],
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
function exportCallGlobals(module: any) {
|
|
404
|
+
for (const [name, exported] of CALL_GLOBALS) {
|
|
405
|
+
if (module.getGlobal(name)) module.addGlobalExport(name, exported);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
295
409
|
/** Binaryen's optimisation at `level`: what a quick build runs in place of asc's. */
|
|
296
410
|
function optimize(module: any, level: number) {
|
|
297
411
|
binaryen.setOptimizeLevel(level);
|
|
@@ -303,6 +417,8 @@ function optimize(module: any, level: number) {
|
|
|
303
417
|
interface Compiled {
|
|
304
418
|
binary: Uint8Array;
|
|
305
419
|
map: PluginMap;
|
|
420
|
+
/** Its env() calls as the section amxts.env writes them, a line a name (scripts/server-env.ts). */
|
|
421
|
+
env: string[];
|
|
306
422
|
}
|
|
307
423
|
|
|
308
424
|
/**
|
|
@@ -339,6 +455,9 @@ async function compileWasm(
|
|
|
339
455
|
return String((problem as Error).message ?? problem);
|
|
340
456
|
}
|
|
341
457
|
|
|
458
|
+
// The settings it reads with env(), from every file it reaches (scripts/server-env.ts).
|
|
459
|
+
const env = envBuild();
|
|
460
|
+
|
|
342
461
|
let binary: Uint8Array | null = null;
|
|
343
462
|
let sourceMap = '';
|
|
344
463
|
let functions = { first: 0, names: [] as string[] };
|
|
@@ -361,7 +480,10 @@ async function compileWasm(
|
|
|
361
480
|
// `~/` is the plugins folder: ascPath answers the alias.
|
|
362
481
|
const path = ascPath(plugin.root, filename, baseDir);
|
|
363
482
|
const text = sources.read(path);
|
|
364
|
-
|
|
483
|
+
if (text === null) return null;
|
|
484
|
+
const file = relative('.', (sources.real(path) ?? path));
|
|
485
|
+
env.read(file, text);
|
|
486
|
+
return shared.read(path, playerFields.read(file, text));
|
|
365
487
|
},
|
|
366
488
|
|
|
367
489
|
// The binary and its source map, for compileToWasm to write as one.
|
|
@@ -389,6 +511,7 @@ async function compileWasm(
|
|
|
389
511
|
// An import of a module the config does not list says so first: asc's
|
|
390
512
|
// "not found" after it is only the consequence.
|
|
391
513
|
if (sources.problems.length) return [...sources.problems, stderr.toString()].join('\n').trim();
|
|
514
|
+
if (env.problems.length) return env.problems.join('\n');
|
|
392
515
|
|
|
393
516
|
// A transform's refusal - a native whose signature cannot cross - is an
|
|
394
517
|
// error with nothing written to stderr.
|
|
@@ -397,7 +520,7 @@ async function compileWasm(
|
|
|
397
520
|
// folder, where the module finds the sources to show a failed line. A
|
|
398
521
|
// build to ship names none - the machine's folders are not the server's.
|
|
399
522
|
const root = level === null ? '' : sources.project.dir;
|
|
400
|
-
return { binary, map: pluginMap(sourceMap, file => fileName(sources, plugin.root, file), functions.first, functions.names.map(displayName), root) };
|
|
523
|
+
return { binary, env: env.entries(), map: pluginMap(sourceMap, file => fileName(sources, plugin.root, file), functions.first, functions.names.map(displayName), root) };
|
|
401
524
|
}
|
|
402
525
|
|
|
403
526
|
/**
|
package/scripts/docs/as/amxts.ts
CHANGED
|
@@ -222,11 +222,14 @@ export default {
|
|
|
222
222
|
server.addCommand<KickArgs>("/kick <target> [reason]", ({ player, target, reason }) => {
|
|
223
223
|
target.kick(reason ?? \`Kicked by \${player.name}\`);
|
|
224
224
|
}, { access: "kick" });
|
|
225
|
-
server.addCommand("/hp", ({ player }) => print(
|
|
225
|
+
server.addCommand("/hp", ({ player }) => player.print(\`\${player.health} HP\`));
|
|
226
|
+
server.addCommand(["/cp", "cp"], checkpoint); // several names; a handler of the player
|
|
226
227
|
\`\`\`
|
|
227
228
|
|
|
228
229
|
A name with \`/\` is a chat command, one without a console command, and
|
|
229
|
-
\`"say <phrase>"\` a phrase written in chat.
|
|
230
|
+
\`"say <phrase>"\` a phrase written in chat. Several names - the usage
|
|
231
|
+
first, then other names - are one command. A command without
|
|
232
|
+
arguments takes a handler of the player, \`(player) => ...\`. A \`number\` argument is
|
|
230
233
|
parsed, a \`Player\` found by \`#userid\`, the whole name or a part of it,
|
|
231
234
|
a \`string\` taken as it is - the last one takes the rest of the line. A
|
|
232
235
|
word that is not what the command takes answers the player with the
|
|
@@ -248,11 +251,14 @@ export default {
|
|
|
248
251
|
server.addCommand<KickArgs>("/kick <target> [reason]", ({ player, target, reason }) => {
|
|
249
252
|
target.kick(reason ?? \`Kicked by \${player.name}\`);
|
|
250
253
|
}, { access: "kick" });
|
|
251
|
-
server.addCommand("/hp", ({ player }) => print(
|
|
254
|
+
server.addCommand("/hp", ({ player }) => player.print(\`\${player.health} HP\`));
|
|
255
|
+
server.addCommand(["/cp", "cp"], checkpoint); // несколько имён; обработчик игрока
|
|
252
256
|
\`\`\`
|
|
253
257
|
|
|
254
258
|
Имя со \`/\` — команда чата, без него — команда консоли, а
|
|
255
|
-
\`"say <фраза>"\` — фраза, написанная в чат.
|
|
259
|
+
\`"say <фраза>"\` — фраза, написанная в чат. Несколько имён — сначала
|
|
260
|
+
использование, затем другие имена — одна команда. Команда без
|
|
261
|
+
аргументов принимает обработчик игрока, \`(player) => ...\`. Аргумент \`number\`
|
|
256
262
|
разбирается как число, \`Player\` находится по \`#userid\`, имени целиком
|
|
257
263
|
или его части, \`string\` берётся как есть — последний забирает остаток
|
|
258
264
|
строки. На слово, которое команда не принимает, игрок получает её
|