@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/upgrade.ts
CHANGED
|
@@ -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
|
|
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
|
|
802
|
-
*
|
|
803
|
-
*
|
|
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
|
|
808
|
-
//
|
|
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
|
-
/**
|
|
814
|
-
|
|
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
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
if (
|
|
836
|
-
|
|
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
|
|
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: [
|
|
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
|
|
44
|
-
*
|
|
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
|
|
47
|
+
* @returns {{ asset: string, path: string, tool: boolean }[]} the module first
|
|
48
48
|
*/
|
|
49
49
|
export function serverFiles(system) {
|
|
50
|
-
|
|
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'))
|
|
91
|
+
return MODULE_ABI.exec(readFileSync(file, 'latin1'));
|
|
82
92
|
} catch {
|
|
83
93
|
return null;
|
|
84
94
|
}
|
package/src/testing/compile.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
package/src/testing/memory.ts
CHANGED
|
@@ -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.
|
|
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
|
}
|