@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.
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 +17 -13
  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 +42 -11
  52. package/scripts/project.ts +63 -4
  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 +47 -34
  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
@@ -29,14 +29,16 @@ import type { Kind } from './upgrade-names';
29
29
  // functions take one object, the menu's context, and an item is one object
30
30
  // with its title (scripts/upgrade-menus.ts). What is rewritten no longer
31
31
  // matches, so a second run changes nothing.
32
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
32
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
33
33
  import { basename, dirname, join, relative, resolve } from 'node:path';
34
34
  import { fileURLToPath } from 'node:url';
35
35
  import ts from 'typescript';
36
36
  import { MESSAGE_NAMES } from './client-messages';
37
- import { CORE_ENTRIES, CORE_PLUGINS, loadProject } from './project';
38
- import { apiFiles, serverFolder } from './system';
37
+ import { CORE_ENTRIES, CORE_PLUGINS, loadProject, SERVER_FOLDER, serverDir, staleCopies } from './project';
38
+ import { apiFiles } from './system';
39
+ import { parseUsage } from './typed-commands';
39
40
  import { c, log } from './ui';
41
+ import { upgradeCalls } from './upgrade-calls';
40
42
  import { upgradeMenus } from './upgrade-menus';
41
43
  import { COMMON, EVENTS, FLAG_NAMES, GAME_EVENT_FIELDS, HIDDEN, HIDDEN_EVENTS, JOIN_OPTIONS, RENAMED, SERVER_EVENT_CLASSES, SERVER_EVENT_FIELDS, SERVER_EVENTS, SERVER_EVENTS_BY_HAND, SERVER_FIELD_CLASSES, SERVER_GAME_EVENTS } from './upgrade-names';
42
44
 
@@ -188,10 +190,13 @@ export function upgradeHandlers(file: string, text: string): { text: string; cha
188
190
  const handler = node.arguments[1];
189
191
  if (!handler) return;
190
192
  const player = method === 'addCommand';
193
+ // A command without arguments takes a handler of the player as it is.
194
+ const ofPlayer = player && takesNoArguments(node);
191
195
 
192
196
  if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
193
197
  const params = handler.parameters;
194
198
  if (params.length === 0 || !ts.isIdentifier(params[0].name)) return;
199
+ if (ofPlayer && params.length === 1) return;
195
200
  if (!player || readsWords(handler)) {
196
201
  left.push({ file, line: lineOf(handler), why: BY_HAND });
197
202
  return;
@@ -210,7 +215,7 @@ export function upgradeHandlers(file: string, text: string): { text: string; cha
210
215
  left.push({ file, line: lineOf(handler), why: `${handler.getText(source)} is declared elsewhere: if it takes the player, pass ({ player }) => ${handler.getText(source)}(player)` });
211
216
  return;
212
217
  }
213
- if (fn.parameters.length === 0) return;
218
+ if (fn.parameters.length === 0 || (ofPlayer && fn.parameters.length === 1)) return;
214
219
  if (!player || fn.parameters.length > 1 || !ts.isIdentifier(fn.parameters[0].name)) {
215
220
  left.push({ file, line: lineOf(handler), why: BY_HAND });
216
221
  return;
@@ -222,6 +227,16 @@ export function upgradeHandlers(file: string, text: string): { text: string; cha
222
227
  return { ...applyEdits(file, text, source, edits), left };
223
228
  }
224
229
 
230
+ /** Whether a command's call adds one without arguments: no type argument, and none in its usage - the first name of an array of them. */
231
+ function takesNoArguments(call: ts.CallExpression): boolean {
232
+ if (call.typeArguments?.length) return false;
233
+ const usage = call.arguments[0];
234
+ const first = usage && ts.isArrayLiteralExpression(usage) ? usage.elements[0] : usage;
235
+ if (!first || !ts.isStringLiteralLike(first)) return !first || !ts.isTemplateExpression(first);
236
+ const parsed = parseUsage(first.text);
237
+ return typeof parsed !== 'string' && parsed.args.length === 0;
238
+ }
239
+
225
240
  /** A piece of a file's text to replace: where, with what, and what it was. */
226
241
  export interface Edit {
227
242
  start: number;
@@ -775,9 +790,6 @@ export function upgradeFlags(file: string, text: string): { text: string; change
775
790
  /** Folders that are not the project's code: what is installed, built or generated. */
776
791
  const SKIP = new Set(['node_modules', 'dist', '.amxts', '.git']);
777
792
 
778
- /** A server's addons/amxts: the module's files, written over on every start. */
779
- const SERVER_FOLDER = /[\\/]addons[\\/]amxts$/i;
780
-
781
793
  /** The project's TypeScript files: its plugins, its tests and fixtures, a module's sources. */
782
794
  function codeFiles(dir: string, skip: Set<string>): string[] {
783
795
  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
@@ -787,37 +799,32 @@ function codeFiles(dir: string, skip: Set<string>): string[] {
787
799
  });
788
800
  }
789
801
 
790
- /** The server's addons/amxts that AMXTS_SERVER names, or '' when it names none. */
791
- function serverDir(): string {
792
- try {
793
- const dir = serverFolder(process.env.AMXTS_SERVER ?? '');
794
- return dir && resolve(dir);
795
- } catch {
796
- return '';
797
- }
798
- }
799
-
800
802
  /**
801
- * The project's code to upgrade. The module's own files - the API a server
802
- * writes beside its plugins, at the core's version it runs - are not, in the
803
- * server's folder or a copy of one; a file of that name in the plugins
804
- * folder is the author's.
803
+ * The project's code to upgrade. The module's own files - the API an older
804
+ * server wrote beside its plugins - are not, in the server's folder or a copy
805
+ * of one; a file of that name in the plugins folder is the author's.
805
806
  */
806
807
  function projectFiles(dir: string, pluginsDir: string, outDir: string): string[] {
807
- // The API at the top of as/, and the editor's globals: what the module
808
- // writes beside the plugins (scripts/server-files.ts).
808
+ // The API at the top of as/, and the editor's globals: what an older
809
+ // module wrote beside the plugins.
809
810
  const theirs = new Set([...apiFiles(), 'imports.d.ts']);
810
811
  return codeFiles(dir, new Set([outDir, serverDir()])).filter(file => !theirs.has(basename(file)) || dirname(file) === pluginsDir);
811
812
  }
812
813
 
813
- /** Rewrites the project in `dir`: every change, in the order of the files, and what is left to do by hand. `write: false` only lists them. */
814
- export function upgradeProject(dir: string, { write = true } = {}): { changes: Change[]; left: Left[] } {
814
+ /**
815
+ * Rewrites the project in `dir`: every change, in the order of the files, and
816
+ * what is left to do by hand. Then removes the copies of the core's API an
817
+ * older amxts left in the plugins folder - after the rewrite, so nothing
818
+ * still imports them by `~/`. `write: false` only lists them.
819
+ */
820
+ export function upgradeProject(dir: string, { write = true } = {}): { changes: Change[]; left: Left[]; removed: string[] } {
815
821
  const project = loadProject(dir);
816
822
  const ownFolder = resolve(project.pluginsDir) !== resolve(CORE_PLUGINS);
817
- const renames = renamesFor(project.modules, place => ownFolder && existsSync(join(project.pluginsDir, place)));
823
+ const stale = staleCopies(project);
824
+ const renames = renamesFor(project.modules, place => ownFolder && existsSync(join(project.pluginsDir, place)) && !stale.includes(join(project.pluginsDir, place)));
818
825
  const changes: Change[] = [];
819
826
  const left: Left[] = [];
820
- for (const file of projectFiles(project.dir, resolve(project.pluginsDir), project.outDir)) {
827
+ for (const file of projectFiles(project.dir, resolve(project.pluginsDir), project.outDir).filter(file => !stale.includes(file))) {
821
828
  const text = readFileSync(file, 'utf8');
822
829
  const name = relative(project.dir, file).replace(/\\/g, '/');
823
830
  const http = dropHttpImports(name, text);
@@ -830,12 +837,16 @@ export function upgradeProject(dir: string, { write = true } = {}): { changes: C
830
837
  const events = upgradeEvents(name, names.text);
831
838
  const messages = upgradeMessages(name, events.text);
832
839
  const flags = upgradeFlags(name, messages.text);
833
- left.push(...http.left, ...handlers.left, ...menus.left, ...names.left, ...events.left, ...messages.left, ...flags.left);
834
- if (flags.text === text) continue;
835
- if (write) writeFileSync(file, flags.text);
836
- changes.push(...http.changes, ...imports.changes, ...handlers.changes, ...menus.changes, ...names.changes, ...events.changes, ...messages.changes, ...flags.changes);
840
+ const calls = upgradeCalls(name, flags.text);
841
+ left.push(...http.left, ...handlers.left, ...menus.left, ...names.left, ...events.left, ...messages.left, ...flags.left, ...calls.left);
842
+ if (calls.text === text) continue;
843
+ if (write) writeFileSync(file, calls.text);
844
+ changes.push(...http.changes, ...imports.changes, ...handlers.changes, ...menus.changes, ...names.changes, ...events.changes, ...messages.changes, ...flags.changes, ...calls.changes);
845
+ }
846
+ if (write) {
847
+ for (const file of stale) rmSync(file);
837
848
  }
838
- return { changes, left };
849
+ return { changes, left, removed: stale.map(file => basename(file)) };
839
850
  }
840
851
 
841
852
  // Run as a task (scripts/run.ts names it in argv) or by itself.
@@ -844,14 +855,16 @@ if (import.meta.main || resolve(process.argv[1] ?? '') === fileURLToPath(import.
844
855
  // amxts command as JSON, which prints what is left to do in its summary.
845
856
  const args = process.argv.slice(2);
846
857
  const report = args.includes('--report') ? args[args.indexOf('--report') + 1] : undefined;
847
- const { changes, left } = upgradeProject(process.cwd(), { write: !args.includes('--dry-run') });
858
+ const write = !args.includes('--dry-run');
859
+ const { changes, left, removed } = upgradeProject(process.cwd(), { write });
860
+ if (removed.length) log.success(`${write ? 'removed' : 'would remove'} amxts's old API copies from plugins/: ${removed.join(', ')}`);
848
861
  for (const change of changes) console.log(` ${c.dim(`${change.file}:${change.line}`)} ${change.from} ${c.dim('→')} ${change.to}`);
849
862
  if (report) {
850
863
  writeFileSync(report, JSON.stringify({ changes, left }));
851
864
  } else {
852
865
  const files = new Set(changes.map(change => change.file)).size;
853
866
  if (changes.length) log.success(`upgraded ${changes.length} place(s) in ${files} file(s)`);
854
- else if (!left.length) log.success('nothing to upgrade: the code already uses this core\'s API');
867
+ else if (!left.length && !removed.length) log.success('nothing to upgrade: the code already uses this core\'s API');
855
868
  for (const each of left) log.warn(`${each.file}:${each.line} ${each.why}`);
856
869
  }
857
870
  }
package/src/cli-api.mjs CHANGED
@@ -84,7 +84,8 @@ const SCRIPTS = {
84
84
  * - `upgrade` rewrites the project's code to this core's API, listing each change;
85
85
  * `--dry-run` writes nothing, `--report <file>` writes `{ changes, left }` there as JSON
86
86
  * and leaves what is left to do by hand to the caller to print;
87
- * - `typecheck` runs TypeScript over the project, after `prepare`.
87
+ * - `typecheck` runs TypeScript over the project, after `prepare`, and says
88
+ * what TypeScript allows and the build does not (src/typecheck.mjs).
88
89
  *
89
90
  * A failure is printed by the task itself and ends it with a non-zero code.
90
91
  *
@@ -94,7 +95,7 @@ const SCRIPTS = {
94
95
  */
95
96
  export function task(name, args = []) {
96
97
  if (name === 'typecheck') {
97
- return { runtime: 'node', args: [require.resolve('typescript/bin/tsc'), '--noEmit', '-p', '.amxts/tsconfig.json', ...args] };
98
+ return { runtime: 'node', args: [join(coreDir, 'src', 'typecheck.mjs'), ...args] };
98
99
  }
99
100
  const script = SCRIPTS[name];
100
101
  if (!script) throw new Error(`@amxts/core ${version} has no task "${name}"`);
package/src/system.mjs CHANGED
@@ -40,19 +40,14 @@ export const MODULE_FILE = { windows: 'amxts_amxx.dll', linux: 'amxts_amxx_i386.
40
40
 
41
41
  /**
42
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.
43
+ * attached under, and where it goes under the game folder (cstrike). A server
44
+ * runs the module alone - it loads plugins built elsewhere. `tool` is a file
45
+ * a server has only when its kit put it there; no such file is left.
46
46
  * @param {System} system
47
- * @returns {{ asset: string, path: string, tool: boolean }[]} the module first, then the tools
47
+ * @returns {{ asset: string, path: string, tool: boolean }[]} the module first
48
48
  */
49
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
- ];
50
+ return [{ asset: MODULE_FILE[system], path: `addons/amxmodx/modules/${MODULE_FILE[system]}`, tool: false }];
56
51
  }
57
52
 
58
53
  /** A system's release manifest, attached beside its files: the version, and each file's size and sha256. */
@@ -77,8 +72,23 @@ const MODULE_ABI = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\+abi\.[0-9a-f]{8}/;
77
72
  * @returns {string | null} the version: `0.2.0`
78
73
  */
79
74
  export function moduleVersion(file) {
75
+ return moduleAbiMatch(file)?.[1] ?? null;
76
+ }
77
+
78
+ /**
79
+ * The ABI a module file loads plugins of, read as moduleVersion reads its
80
+ * release. Null when there is no file or no such string.
81
+ * @param {string} file
82
+ * @returns {string | null} the ABI: `0.2.0+abi.1a2b3c4d`
83
+ */
84
+ export function moduleAbiOf(file) {
85
+ return moduleAbiMatch(file)?.[0] ?? null;
86
+ }
87
+
88
+ /** @param {string} file */
89
+ function moduleAbiMatch(file) {
80
90
  try {
81
- return MODULE_ABI.exec(readFileSync(file, 'latin1'))?.[1] ?? null;
91
+ return MODULE_ABI.exec(readFileSync(file, 'latin1'));
82
92
  } catch {
83
93
  return null;
84
94
  }
@@ -11,6 +11,7 @@ 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
13
  import { targetTransforms } from '../../scripts/reapi-events';
14
+ import { envBuild } from '../../scripts/server-env';
14
15
  import { sharedModulesBuild } from '../../scripts/shared-modules';
15
16
  import { existsSync, hashOf } from '../../scripts/tracked-fs';
16
17
  import { cached } from './compile-cache';
@@ -26,6 +27,8 @@ export interface Compiled {
26
27
  binary: Uint8Array;
27
28
  /** Whether it carries the coroutine scheduler's exports (as/promise.ts). */
28
29
  async: boolean;
30
+ /** Its env() calls as the section amxts.env writes them, a line a name (scripts/server-env.ts). */
31
+ env: string[];
29
32
  }
30
33
 
31
34
  const compiled = new Map<string, Promise<Compiled>>();
@@ -77,6 +80,19 @@ function moduleOwner(source: string): string | null {
77
80
  /** A compile as the disk cache keeps it: no WebAssembly.Module, and an error as its text. */
78
81
  type Built = (Omit<Compiled, 'module'> & { beside: NativesBeside }) | { error: string };
79
82
 
83
+ /**
84
+ * `AMXTS_GC_STRESS=1` in the environment: every plugin and snippet a test
85
+ * compiles runs a step of the collector on every allocation
86
+ * (`ASC_GC_STRESS`), so a value a function holds without a shadow-stack
87
+ * slot is freed at once and its test fails. Part of the cache's key.
88
+ */
89
+ const GC_STRESS = process.env.AMXTS_GC_STRESS ? ['--use', 'ASC_GC_STRESS=1'] : [];
90
+
91
+ /** `args` with GC_STRESS, unless they ask for it themselves. */
92
+ export function stressed(args: string[]): string[] {
93
+ return args.includes('ASC_GC_STRESS=1') ? args : [...args, ...GC_STRESS];
94
+ }
95
+
80
96
  /** A failure worth keeping on disk is the compiler's; a file that could not be opened is not. */
81
97
  const PASSING = /\b(?:EBUSY|EPERM|EACCES|EMFILE|ENFILE|EAGAIN)\b/;
82
98
 
@@ -89,7 +105,7 @@ function lasting(built: Built): boolean {
89
105
  * when an earlier run compiled it from the same files.
90
106
  */
91
107
  async function build(path: string): Promise<Compiled> {
92
- const built = await cached<Built>(['plugin', process.cwd(), path, PLUGINS_ROOT], async () => {
108
+ const built = await cached<Built>(['plugin', process.cwd(), path, PLUGINS_ROOT, GC_STRESS], async () => {
93
109
  try {
94
110
  const fresh = await buildFresh(path);
95
111
  return { ...fresh, beside: nativesBeside(fresh.natives) };
@@ -127,11 +143,12 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
127
143
  let binary: Uint8Array | undefined;
128
144
  const natives: PluginNative[] = [];
129
145
  const playerFields = playerFieldsBuild();
146
+ const env = envBuild();
130
147
  // scripts/compile.ts's shared modules: a proxy for one another plugin owns.
131
148
  const shared = await sharedModulesBuild(PLUGINS_ROOT, entry);
132
149
 
133
150
  const { error, stderr } = await ascMain(
134
- [entry, HOOD_EXPORTS, '--outFile', 'plugin.wasm', '--optimize', '--exportTable', '--exportStart', '_start'],
151
+ stressed([entry, HOOD_EXPORTS, '--outFile', 'plugin.wasm', '--optimize', '--exportTable', '--exportStart', '_start']),
135
152
  {
136
153
  // scripts/compile.ts's readFile: the `~/` alias, the hood's exports and the fields plugins add to Player.
137
154
  readFile(filename: string, baseDir: string): string | null {
@@ -139,7 +156,10 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
139
156
 
140
157
  const file = ascPath(PLUGINS_ROOT, filename, baseDir);
141
158
  const text = sources.read(file);
142
- return text === null ? null : shared.read(file, playerFields.read(relative('.', (sources.real(file) ?? file)), text));
159
+ if (text === null) return null;
160
+ const shown = relative('.', (sources.real(file) ?? file));
161
+ env.read(shown, text);
162
+ return shared.read(file, playerFields.read(shown, text));
143
163
  },
144
164
  // Only the binary is wanted; nothing is written to disk.
145
165
  writeFile(_: string, contents: string | Uint8Array) {
@@ -151,12 +171,13 @@ async function buildWith(path: string, hoodExports: string): Promise<Omit<Compil
151
171
  );
152
172
 
153
173
  if (sources.problems.length) throw new Error(sources.problems.join('\n'));
174
+ if (env.problems.length) throw new Error(env.problems.join('\n'));
154
175
 
155
176
  if (error || !binary) {
156
177
  throw new Error(`${relative(process.cwd(), path)} does not compile:\n${stderr.toString() || String(error?.message ?? error)}`);
157
178
  }
158
179
 
159
- return { natives, binary, async: false };
180
+ return { natives, binary, async: false, env: env.entries() };
160
181
  }
161
182
 
162
183
  /**
@@ -188,19 +209,21 @@ export interface SourcesCompiled {
188
209
  /**
189
210
  * asc - the patched one, as the build runs it - on `sources` alone, a test's
190
211
  * snippet of the compiler at work, with the disk cache in front. Nothing is
191
- * read from disk, so the sources and `args` are the whole key.
212
+ * read from disk, so the sources and `args` are the whole key. `finished`
213
+ * adds what a full build does after asc's optimiser (finishing).
192
214
  */
193
- export async function compileSources(args: string[], sources: Record<string, string>): Promise<SourcesCompiled> {
194
- return cached<SourcesCompiled>(['sources', args, sources], async () => {
215
+ export async function compileSources(args: string[], sources: Record<string, string>, finished = false): Promise<SourcesCompiled> {
216
+ return cached<SourcesCompiled>(['sources', stressed(args), sources, finished], async () => {
195
217
  let binary: Uint8Array | null = null;
196
218
  const text: Record<string, string> = {};
197
- const { error, stderr } = await ascMain(args, {
219
+ const { error, stderr } = await ascMain(stressed(args), {
198
220
  readFile: (name: string) => sources[name] ?? null,
199
221
  writeFile(name: string, contents: string | Uint8Array) {
200
222
  if (typeof contents === 'string') text[name] = contents;
201
223
  else binary = contents;
202
224
  },
203
225
  listFiles: () => [],
226
+ transforms: finished ? [finishing(BASE_EXPORTS, null)] : [],
204
227
  });
205
228
  return error ? { error: stderr.toString(), binary: null, text } : { error: null, binary, text };
206
229
  });
@@ -24,15 +24,6 @@ interface Coroutine {
24
24
  drop: boolean;
25
25
  }
26
26
 
27
- // co_id(): a task id no other plugin on the server has, as module.cpp's is -
28
- // one counter for every plugin.
29
- let nextId = 0x60000000;
30
-
31
- /** The next of those ids. @internal */
32
- export function nextTaskId(): number {
33
- return nextId++;
34
- }
35
-
36
27
  export class Coroutines {
37
28
  /** What a trap inside a coroutine said: it drops that one coroutine, as on the server. */
38
29
  readonly traps: string[] = [];
@@ -81,7 +72,6 @@ export class Coroutines {
81
72
  this.exports.asyncify_start_unwind(coroutine.buffer);
82
73
  },
83
74
  co_wake: () => { this.wake = true; },
84
- co_id: nextTaskId,
85
75
  };
86
76
  }
87
77
 
@@ -22,6 +22,22 @@ export function bitsFloat(cell: number): number {
22
22
  return floatView[0];
23
23
  }
24
24
 
25
+ /** How many of the bytes are whole UTF-8 characters: a last one cut short is left out. */
26
+ function wholeUtf8(bytes: Uint8Array): number {
27
+ let lead = bytes.length;
28
+ while (lead > 0 && (bytes[lead - 1] & 0xC0) === 0x80) lead--;
29
+ if (lead === 0 || bytes[lead - 1] < 0xC0) return bytes.length;
30
+ const size = bytes[lead - 1] >= 0xF0 ? 4 : bytes[lead - 1] >= 0xE0 ? 3 : 2;
31
+ return bytes.length - (lead - 1) < size ? lead - 1 : bytes.length;
32
+ }
33
+
34
+ // The AMX heap the module passes text through (Frame::inText, outText):
35
+ // cells of the fake's own at addresses past any plugin's memory, which a
36
+ // native reads and writes as it does the plugin's.
37
+ const HEAP = 0x7F000000;
38
+ // The longest text that crosses, in bytes and its terminator (MAX_CROSSING_CELLS).
39
+ const CROSSING_CELLS = 16384;
40
+
25
41
  /** How many of the bytes fit in `max` without cutting a UTF-8 character in two. */
26
42
  export function utf8Fit(bytes: Uint8Array, max: number): number {
27
43
  let length = Math.min(bytes.length, max);
@@ -31,6 +47,8 @@ export function utf8Fit(bytes: Uint8Array, max: number): number {
31
47
 
32
48
  export class Memory {
33
49
  private dataView?: DataView;
50
+ private heap = new Int32Array(1 << 18);
51
+ private heapTop = 0;
34
52
 
35
53
  constructor(private exports: any) {}
36
54
 
@@ -42,11 +60,70 @@ export class Memory {
42
60
  }
43
61
 
44
62
  cell(pointer: number): number {
45
- return this.view.getInt32(pointer, true);
63
+ return pointer >= HEAP ? this.heap[(pointer - HEAP) >> 2] : this.view.getInt32(pointer, true);
64
+ }
65
+
66
+ byte(pointer: number): number {
67
+ return this.view.getUint8(pointer);
46
68
  }
47
69
 
48
70
  setCell(pointer: number, value: number): void {
49
- this.view.setInt32(pointer, value | 0, true);
71
+ if (pointer >= HEAP) this.heap[(pointer - HEAP) >> 2] = value | 0;
72
+ else this.view.setInt32(pointer, value | 0, true);
73
+ }
74
+
75
+ /**
76
+ * A native's call as the module's thunk makes it, by the native's marks in
77
+ * runtime/natives.txt: an `s` argument is the plugin's string, handed to
78
+ * the native as a Pawn string on the heap; a `t` one is a buffer the
79
+ * native fills on the heap, back in the plugin as UTF-8 bytes up to its end.
80
+ */
81
+ across(marks: readonly string[], args: number[], call: (cells: number[]) => number): number {
82
+ const top = this.heapTop;
83
+ try {
84
+ const cells = args.map((arg, i) => marks[i] === 's' ? this.heapText(this.string(arg)) : marks[i] === 't' ? this.heapCells(args[i + 1] + 1) : arg);
85
+ const result = call(cells);
86
+ marks.forEach((mark, i) => {
87
+ if (mark !== 't') return;
88
+ const bytes = this.bytes(cells[i], args[i + 1]);
89
+ const kept = bytes.subarray(0, wholeUtf8(bytes));
90
+ this.setRaw(args[i], Uint8Array.of(...kept, 0));
91
+ this.setTextHead(args[i], kept);
92
+ });
93
+ return result;
94
+ } finally {
95
+ this.heapTop = top;
96
+ }
97
+ }
98
+
99
+ /** Frame::backText's head before a text a native filled: its count, the top bit for one not ASCII, and its hash. */
100
+ private setTextHead(pointer: number, bytes: Uint8Array): void {
101
+ // As the module's: four bytes a word, little-endian, while all four are there, then a byte at a time.
102
+ let hash = 0;
103
+ let bits = 0;
104
+ let i = 0;
105
+ for (; i + 4 <= bytes.length; i += 4) hash = (Math.imul(hash, 31) + (bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24)) >>> 0;
106
+ for (; i < bytes.length; i++) hash = (Math.imul(hash, 31) + bytes[i]) >>> 0;
107
+ for (const byte of bytes) bits |= byte;
108
+ this.view.setUint32(pointer - 8, (bytes.length | (bits >= 0x80 ? 0x80000000 : 0)) >>> 0, true);
109
+ this.view.setUint32(pointer - 4, hash, true);
110
+ }
111
+
112
+ /** `count` empty cells of the heap: their address. */
113
+ private heapCells(count: number): number {
114
+ const at = this.heapTop;
115
+ this.heapTop += Math.min(Math.max(count, 1), CROSSING_CELLS);
116
+ this.heap.fill(0, at, this.heapTop);
117
+ return HEAP + at * 4;
118
+ }
119
+
120
+ /** Text as a Pawn string on the heap, cut at the longest that crosses, between letters. */
121
+ private heapText(text: string): number {
122
+ const bytes = encoder.encode(text);
123
+ const length = utf8Fit(bytes, CROSSING_CELLS - 1);
124
+ const at = this.heapCells(length + 1);
125
+ this.heap.set(bytes.subarray(0, length), (at - HEAP) >> 2);
126
+ return at;
50
127
  }
51
128
 
52
129
  float(pointer: number): number {
@@ -61,6 +138,16 @@ export class Memory {
61
138
  return [this.float(pointer), this.float(pointer + 4), this.float(pointer + 8)];
62
139
  }
63
140
 
141
+ /** A plugin's number (f64) read from its own memory. */
142
+ number(pointer: number): number {
143
+ return this.view.getFloat64(pointer, true);
144
+ }
145
+
146
+ /** A plugin's number (f64) in its own memory. */
147
+ setNumber(pointer: number, value: number): void {
148
+ this.view.setFloat64(pointer, value, true);
149
+ }
150
+
64
151
  setVector(pointer: number, value: number[]): void {
65
152
  for (let i = 0; i < 3; i++) this.setFloat(pointer + i * 4, value[i] ?? 0);
66
153
  }