@amxts/core 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +5 -5
  2. package/README.ru.md +5 -5
  3. package/as/amxts.d.ts +13 -3
  4. package/as/entities.ts +571 -501
  5. package/as/events.ts +3323 -557
  6. package/as/facade.ts +3079 -718
  7. package/as/fetch.ts +2 -2
  8. package/as/fs.ts +352 -59
  9. package/as/hlds.ts +121 -120
  10. package/as/hooks.ts +10585 -5177
  11. package/as/lib/check.ts +3 -2
  12. package/as/natives.ts +856 -1272
  13. package/as/promise.ts +37 -27
  14. package/as/vector.ts +94 -14
  15. package/includes/order.txt +17 -19
  16. package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
  17. package/package.json +9 -6
  18. package/runtime/deps/assemblyscript/dist/asc.js +136 -42
  19. package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
  20. package/runtime/licenses/README.md +9 -7
  21. package/runtime/licenses/ReGameDLL-LICENSE +21 -0
  22. package/runtime/licenses/ReHLDS-LICENSE +21 -0
  23. package/runtime/licenses/zlib-LICENSE +22 -0
  24. package/runtime/natives.txt +618 -523
  25. package/runtime/patches/README.md +87 -1
  26. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
  27. package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
  28. package/scripts/auto-imports.ts +1 -1
  29. package/scripts/build-identity.ts +102 -65
  30. package/scripts/build-wasm.ts +12 -12
  31. package/scripts/check.ts +4 -1
  32. package/scripts/client-messages.ts +73 -1
  33. package/scripts/compile-cache.ts +11 -8
  34. package/scripts/compile-pool.ts +6 -3
  35. package/scripts/compile-worker.ts +23 -23
  36. package/scripts/compile.ts +142 -19
  37. package/scripts/docs/as/amxts.ts +10 -4
  38. package/scripts/docs/as/facade.ts +1779 -275
  39. package/scripts/docs/as/fetch.ts +2 -2
  40. package/scripts/docs/as/fs.ts +190 -6
  41. package/scripts/docs/as/vector.ts +81 -0
  42. package/scripts/docs/entities.ts +12 -0
  43. package/scripts/docs/events.ts +49 -13
  44. package/scripts/docs/game.ts +6 -0
  45. package/scripts/docs/messages.ts +10 -1
  46. package/scripts/hlds-events.ts +11 -11
  47. package/scripts/includes.ts +3 -4
  48. package/scripts/leaf-natives.ts +84 -0
  49. package/scripts/player-fields.ts +1 -1
  50. package/scripts/plugin-natives.ts +1 -1
  51. package/scripts/prebuilt.ts +41 -10
  52. package/scripts/project.ts +2 -3
  53. package/scripts/reapi-events.ts +9 -9
  54. package/scripts/server-env.ts +145 -0
  55. package/scripts/shared-modules.ts +23 -7
  56. package/scripts/system.ts +1 -1
  57. package/scripts/tracked-fs.ts +11 -2
  58. package/scripts/typed-commands.ts +153 -15
  59. package/scripts/upgrade-calls.ts +225 -0
  60. package/scripts/upgrade-names.ts +1 -4
  61. package/scripts/upgrade.ts +26 -11
  62. package/src/cli-api.mjs +3 -2
  63. package/src/system.mjs +21 -11
  64. package/src/testing/compile.ts +31 -8
  65. package/src/testing/coroutines.ts +0 -10
  66. package/src/testing/memory.ts +89 -2
  67. package/src/testing/natives.ts +67 -30
  68. package/src/testing/server.ts +1094 -155
  69. package/src/testing/tables.ts +23 -6
  70. package/src/typecheck.mjs +110 -0
  71. package/runtime/licenses/Bun-LICENSE.md +0 -84
@@ -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, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, utimesSync, watch, writeFileSync } from 'node:fs';
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';
@@ -46,6 +46,7 @@ import { pluginCache } from './plugin-cache';
46
46
  import { includeName } from './plugin-natives';
47
47
  import { fromRegistry, prebuiltOf, prebuiltSurface } from './prebuilt';
48
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';
@@ -406,6 +407,16 @@ async function deployAndReload(built: string[], names: string): Promise<string>
406
407
  if (before && byRcon) utimesSync(target, before.atime, before.mtime);
407
408
  }
408
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
+
409
420
  // A plugin that exports natives has an include for Pawn plugins beside it;
410
421
  // it goes where amxxpc on the server looks, when the server has one.
411
422
  const pawnIncludes = join(serverDir, '..', 'amxmodx', 'scripting', 'include');
@@ -416,17 +427,6 @@ async function deployAndReload(built: string[], names: string): Promise<string>
416
427
  // The module's own natives for Pawn: the fields plugins add to Player.
417
428
  if (existsSync(pawnIncludes)) copyFileSync(join(CORE_DIR, 'runtime/host/amxts.inc'), join(pawnIncludes, 'amxts.inc'));
418
429
 
419
- // A folder beside the plugins is a library - `~/lib/thing`, `~/myplugin/config`.
420
- // It is compiled into the .aot already, so nothing here needs it; a .ts
421
- // plugin written on the server does, and it was silently compiling against
422
- // whatever copy happened to be there.
423
- const ownFolder = resolve(sourceDir) !== resolve(CORE_PLUGINS) || resolve(project.dir) === resolve(CORE_DIR);
424
- for (const entry of ownFolder ? readdirSync(sourceDir, { withFileTypes: true }) : []) {
425
- if (entry.isDirectory()) {
426
- cpSync(join(sourceDir, entry.name), join(serverDir, 'plugins', entry.name), { recursive: true });
427
- }
428
- }
429
-
430
430
  if (project.config) {
431
431
  writeFileSync(listPath, serverList(existing, readFileSync(join(outDir, 'plugins.ini'), 'utf8')));
432
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
- fail('amxts check: this folder is not a module package - package.json has no "amxts": { "module": ... }');
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-host.ts, which writes their events into as/events.ts.
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
@@ -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 (a plugin
5
- // carries it in its ABI), the flags, the project and its config - and
6
- // stored with every file read it made (scripts/tracked-fs.ts): the entry, what
7
- // it imports, the facade and the generated API, the module packages. It is
8
- // taken again only while each of those reads sees the same thing it saw, so a
9
- // hit is what compiling again would give, byte for byte.
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;
@@ -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
+ });
@@ -6,24 +6,26 @@ import { spawnSync } from 'node:child_process';
6
6
 
7
7
  // Compiles one plugin: .ts in, .aot out.
8
8
  //
9
- // Both halves of the project use this one function - `bun run plugins` here,
10
- // and amxts-compile.exe on a server - so that a plugin compiles the same way
11
- // in both places. In particular the `~/` alias, which is ours rather than
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, so the compiler a server
16
- // gets carries one - for the one pass asc does not run: Asyncify.
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, abiIdentity } from './build-identity';
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 does the ABI the plugin is
120
- * compiled against, which the module checks before it loads it
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
- let made = await compileWasm(plugin, BASE_EXPORTS, flags, level, natives);
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 it is compiled again with the scheduler's
241
- // exports.
242
- if (typeof made !== 'string' && importsOf(made.binary).has(WAKE_IMPORT)) {
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
- writeFileSync(wasm, withSection(withSection(made.binary, MAP_SECTION, mapText(made.map)), ABI_SECTION, abiIdentity()));
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
- const imports = new Set(functionsOf(module).map(each => each.imported));
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
- return text === null ? null : shared.read(path, playerFields.read(relative('.', (sources.real(path) ?? path)), text));
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
  /**
@@ -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(player, \`\${player.health} HP\`));
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. A \`number\` argument is
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(player, \`\${player.health} HP\`));
254
+ server.addCommand("/hp", ({ player }) => player.print(\`\${player.health} HP\`));
255
+ server.addCommand(["/cp", "cp"], checkpoint); // несколько имён; обработчик игрока
252
256
  \`\`\`
253
257
 
254
258
  Имя со \`/\` — команда чата, без него — команда консоли, а
255
- \`"say <фраза>"\` — фраза, написанная в чат. Аргумент \`number\`
259
+ \`"say <фраза>"\` — фраза, написанная в чат. Несколько имён — сначала
260
+ использование, затем другие имена — одна команда. Команда без
261
+ аргументов принимает обработчик игрока, \`(player) => ...\`. Аргумент \`number\`
256
262
  разбирается как число, \`Player\` находится по \`#userid\`, имени целиком
257
263
  или его части, \`string\` берётся как есть — последний забирает остаток
258
264
  строки. На слово, которое команда не принимает, игрок получает её