@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The natives that never run a plugin's code while they run - leaf natives -
|
|
3
|
+
* read from the sources of AMX Mod X 1.10 and its stock modules.
|
|
4
|
+
*
|
|
5
|
+
* A native is on the list when nothing it does, directly or through what it
|
|
6
|
+
* calls, can reach a plugin: it executes no forward and no callback
|
|
7
|
+
* (ExecuteForward, MF_ExecuteForward, callfunc, a comparator, a parser's
|
|
8
|
+
* reader), calls no game function (MDLL_*, the game's entity functions,
|
|
9
|
+
* ReGameDLL) and no engine function that runs the game's code or that a
|
|
10
|
+
* ReHLDS hookchain, Hamsandwich or ReAPI hooks - no cvar written, no command
|
|
11
|
+
* executed, no message sent, no entity made or removed, nothing precached.
|
|
12
|
+
* What it does is read and write its own memory and AMX Mod X's: its
|
|
13
|
+
* arguments, AMX Mod X's tables (players, tries, arrays, vaults, the
|
|
14
|
+
* dictionary, the plugin list), entity fields, cvar values, info buffers, a
|
|
15
|
+
* command's words, files.
|
|
16
|
+
*
|
|
17
|
+
* Its one way out is an error: LogError runs the error filter of the AMX it
|
|
18
|
+
* was called with, and the natives' image sets none.
|
|
19
|
+
*
|
|
20
|
+
* Every one reads and writes its arguments with get_amxaddr, get_amxstring
|
|
21
|
+
* and set_amxstring (no bounds check), and writes none it declares `const`.
|
|
22
|
+
*
|
|
23
|
+
* What a leaf native allows:
|
|
24
|
+
* - its import is marked `@leaf`: a plugin keeps no shadow-stack frame for
|
|
25
|
+
* a call to it, as no collection can run while it does;
|
|
26
|
+
* - a string it takes that is the plugin's static text - a literal, which
|
|
27
|
+
* never changes - crosses as cells made once a map and kept in the natives'
|
|
28
|
+
* image (the module's KeptText), not converted on every call;
|
|
29
|
+
* - the text it fills is copied back without asking where the plugin's
|
|
30
|
+
* memory is again, as the memory cannot grow while it runs.
|
|
31
|
+
*
|
|
32
|
+
* A native left off - one that may call back, such as ExecuteForward,
|
|
33
|
+
* callfunc_*, set_cvar_*, server_cmd or a Pawn plugin's native - goes the
|
|
34
|
+
* copy way. A wrong entry would let the collector free what a plugin still
|
|
35
|
+
* holds, so a native is added only after its source is read.
|
|
36
|
+
*/
|
|
37
|
+
/** The names in `text`, apart by white space. */
|
|
38
|
+
function names(text: string): string[] {
|
|
39
|
+
return text.split(/\s+/).filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const LEAF_NATIVES: ReadonlySet<string> = new Set([
|
|
43
|
+
// Text and numbers: string.cpp and amxmodx.cpp work on their arguments alone.
|
|
44
|
+
...names(`add argparse contain containi copy copyc equal equali float_to_str format_time get_char_bytes
|
|
45
|
+
get_time hash_file hash_string is_string_category mb_strtolower mb_strtotitle mb_strtoupper
|
|
46
|
+
mb_ucfirst md5 num_to_str num_to_word parse_loguser parse_time replace replace_string
|
|
47
|
+
replace_stringex setc split_string str_to_float str_to_num strcmp strfind strlen strncmp strtof
|
|
48
|
+
strtok strtok2 strtol`),
|
|
49
|
+
// Files, vaults and data packs: the C runtime and the engine's file system.
|
|
50
|
+
...names(`delete_file dir_exists fgets file_exists file_size fopen fputs GetFileTime LoadFileForMe mkdir
|
|
51
|
+
next_file open_dir read_dir read_file rename_file rmdir SetFilePermissions unlink write_file
|
|
52
|
+
get_vaultdata remove_vaultdata set_vaultdata vaultdata_exists nvault_lookup nvault_open
|
|
53
|
+
nvault_pset nvault_remove nvault_set nvault_touch ReadPackString WritePackString`),
|
|
54
|
+
// Cvars read, never written: writing one runs Cvar_DirectSet and its hooks.
|
|
55
|
+
...names(`cvar_exists get_cvar_flags get_cvar_float get_cvar_num get_cvar_pointer get_cvar_string
|
|
56
|
+
get_pcvar_string`),
|
|
57
|
+
// The server's and the players' state read: AMX Mod X's player table, info
|
|
58
|
+
// buffers, a command's words, the message and weapon tables. Not
|
|
59
|
+
// get_user_authid, nor get_players, which asks for it: the engine finds
|
|
60
|
+
// the id through ReHLDS's SV_GetIDString hookchain.
|
|
61
|
+
...names(`get_amxx_verstring get_flags get_localinfo get_mapname get_modname get_user_index get_user_info
|
|
62
|
+
get_user_ip get_user_msgid get_user_msgname get_user_name get_weaponid get_weaponname
|
|
63
|
+
get_xvar_id is_map_valid read_args read_argv read_flags read_logargv read_logdata xvar_exists`),
|
|
64
|
+
// Plugins, modules, libraries and the dictionary: AMX Mod X's own lists.
|
|
65
|
+
...names(`AddTranslation CreateLangKey find_plugin_byfile GetLangTransKey is_module_loaded
|
|
66
|
+
is_plugin_loaded lang_exists LibraryExists LookupLangKey module_exists register_dictionary`),
|
|
67
|
+
// Tries and arrays: AMX Mod X's memory. Not ArraySort or SortCustom*, which call a plugin back.
|
|
68
|
+
...names(`ArrayFindString ArrayGetString ArrayInsertStringAfter ArrayInsertStringBefore ArrayPushString
|
|
69
|
+
ArraySetString TrieDeleteKey TrieGetArray TrieGetCell TrieGetString TrieKeyExists TrieSetArray
|
|
70
|
+
TrieSetCell TrieSetString TrieSnapshotGetKey`),
|
|
71
|
+
// Entities and game data read: fields, private data, the engine's search
|
|
72
|
+
// of entities by a field. Not lookup_sequence, which may load a model
|
|
73
|
+
// through a hookchain, nor cs_find_ent_*, which call the game.
|
|
74
|
+
...names(`copy_infokey_buffer cs_get_item_id cs_get_translated_item_alias cs_get_user_model eng_get_string
|
|
75
|
+
entity_get_string find_ent_by_class find_ent_by_model find_ent_by_owner find_ent_by_target
|
|
76
|
+
find_ent_by_tname find_ent_data_info find_gamerules_info find_sphere_class GameConfGetAddress
|
|
77
|
+
GameConfGetClassOffset GameConfGetKeyValue GameConfGetOffset get_ent_data_string
|
|
78
|
+
get_gamerules_string get_global_string get_info_keybuffer get_keyvalue get_pdata_string
|
|
79
|
+
has_map_ent_class`),
|
|
80
|
+
// GeoIP: lookups in the databases in memory.
|
|
81
|
+
...names(`geoip_city geoip_code2 geoip_code2_ex geoip_code3 geoip_code3_ex geoip_continent_code
|
|
82
|
+
geoip_continent_name geoip_country geoip_country_ex geoip_latitude geoip_longitude
|
|
83
|
+
geoip_region_code geoip_region_name geoip_timezone`),
|
|
84
|
+
]);
|
package/scripts/player-fields.ts
CHANGED
|
@@ -721,7 +721,7 @@ export function playerFieldsSource(fields: PlayerField[]): string {
|
|
|
721
721
|
'\t\tconst ids = text.split(",");',
|
|
722
722
|
'\t\tfor (let i = 0; i < ids.length; i++) {',
|
|
723
723
|
'\t\t\tconst id = I32.parseInt(unchecked(ids[i]));',
|
|
724
|
-
'\t\t\tif (id > 0) list.push(
|
|
724
|
+
'\t\t\tif (id > 0) list.push(__playerOf(id));',
|
|
725
725
|
'\t\t}',
|
|
726
726
|
'\t\treturn list;',
|
|
727
727
|
'\t}',
|
|
@@ -988,7 +988,7 @@ function crossForwards(sources: any[], contract: Contract | null, problems: stri
|
|
|
988
988
|
} else if (type === 'Vector' && param.type !== 'Float') {
|
|
989
989
|
problems.push(`${where} - argument ${i + 1} is ${pawn} in the include: a Vector is three Floats, Float:${param.name}[3]`);
|
|
990
990
|
} else if (!param.type || param.type === 'any') {
|
|
991
|
-
//
|
|
991
|
+
// An untagged array crosses as text (generate-image.ts, kindOf).
|
|
992
992
|
problems.push(`${where} - argument ${i + 1} is ${pawn} in the include, without a tag: Pawn passes it as text, and a ${type} would arrive empty; tag it, Float:${param.name}[]`);
|
|
993
993
|
} else {
|
|
994
994
|
crossing += param.type === 'Float' ? 'F' : '_';
|
package/scripts/prebuilt.ts
CHANGED
|
@@ -22,9 +22,14 @@
|
|
|
22
22
|
// - the core: the compile's scripts, the patched AssemblyScript and its
|
|
23
23
|
// Binaryen, the API in as/, the natives table wamrc reads and the WAMR
|
|
24
24
|
// patch - which also decides whether the server's module loads the .aot.
|
|
25
|
-
// A package from the registry is its
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// A package from the registry is its ABI (scripts/build-identity.ts): the
|
|
26
|
+
// .aot is taken under any core of its line on the same ground that has
|
|
27
|
+
// every import it uses with the same shape, as the server's module of that
|
|
28
|
+
// line loads it - a core's patch release needs no module released again,
|
|
29
|
+
// only one that uses what a later patch added. The .aot keeps the facade
|
|
30
|
+
// of the core it was compiled with, as any plugin built before the patch
|
|
31
|
+
// does. A folder on this machine (a checkout) is its content too, so the
|
|
32
|
+
// version, the ABI and a hash are all kept;
|
|
28
33
|
// - every module package the compile reads - the module, and the ones it
|
|
29
34
|
// imports (menu-core reads config-core for its proxy) - the same way;
|
|
30
35
|
// - the options amxts.config.ts gives them: setup is compiled with them;
|
|
@@ -47,12 +52,13 @@ import { createHash } from 'node:crypto';
|
|
|
47
52
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
48
53
|
import { join, relative, resolve, sep } from 'node:path';
|
|
49
54
|
import ts from 'typescript';
|
|
50
|
-
import { coreVersion } from './build-identity';
|
|
55
|
+
import { ABI_SECTION, abiIdentity, abiLine, coreVersion, importName, importShapes, releaseLine } from './build-identity';
|
|
51
56
|
import { compileToMachineCode, compileToWasm } from './compile';
|
|
52
57
|
import { codeFiles } from './compile-cache';
|
|
53
58
|
import { includeForward, nativesBeside } from './plugin-natives';
|
|
54
59
|
import { CORE_DIR, CORE_PLUGINS, loadProject, optionsOf, shared, sourcesFor } from './project';
|
|
55
60
|
import { moduleSurface } from './shared-modules';
|
|
61
|
+
import { readSection } from './source-map';
|
|
56
62
|
import { SYSTEM_NAME, SYSTEMS, wamrcPath } from './system';
|
|
57
63
|
import { c, log, since } from './ui';
|
|
58
64
|
|
|
@@ -60,7 +66,7 @@ import { c, log, since } from './ui';
|
|
|
60
66
|
export const PREBUILT_DIR = 'prebuilt';
|
|
61
67
|
const MANIFEST = 'manifest.json';
|
|
62
68
|
/** Goes up when the manifest changes shape: a build reads only its own. */
|
|
63
|
-
const FORMAT =
|
|
69
|
+
const FORMAT = 2;
|
|
64
70
|
const CORE_NAME = '@amxts/core';
|
|
65
71
|
|
|
66
72
|
/** A package as a prebuilt module was compiled with it. */
|
|
@@ -68,6 +74,8 @@ interface Built {
|
|
|
68
74
|
version: string;
|
|
69
75
|
/** Its content: the code files, TypeScript without comments. */
|
|
70
76
|
hash: string;
|
|
77
|
+
/** The core's: the ABI the .aot carries. */
|
|
78
|
+
abi?: string;
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export interface PrebuiltManifest {
|
|
@@ -152,9 +160,9 @@ function coreHash(): string {
|
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
/** The core or a module package the project has, by name, as a prebuilt module is checked against it. */
|
|
155
|
-
function installed(name: string, sources: Sources): { version: string; dir: string; hash: () => string } | null {
|
|
163
|
+
function installed(name: string, sources: Sources): { version: string; dir: string; hash: () => string; abi?: string } | null {
|
|
156
164
|
if (name === CORE_NAME) {
|
|
157
|
-
return { version: coreVersion(), dir: CORE_DIR, hash: coreHash };
|
|
165
|
+
return { version: coreVersion(), dir: CORE_DIR, hash: coreHash, abi: abiIdentity() };
|
|
158
166
|
}
|
|
159
167
|
const pkg = sources.project.packages.find(each => each.name === name);
|
|
160
168
|
return pkg ? { version: pkg.version, dir: pkg.dir, hash: () => packageHash(pkg) } : null;
|
|
@@ -223,7 +231,8 @@ function mismatch(manifest: PrebuiltManifest, pkg: ModulePackage, sources: Sourc
|
|
|
223
231
|
for (const [name, built] of Object.entries(manifest.from)) {
|
|
224
232
|
const have = installed(name, sources);
|
|
225
233
|
if (!have) return `${named} was built with ${name}, which the project does not have`;
|
|
226
|
-
|
|
234
|
+
const other = have.abi ? otherAbi(built.abi ?? '', have.abi) : have.version !== built.version && `${name} ${built.version}, the project has ${have.version}`;
|
|
235
|
+
if (other) return `${named} was built for ${other}`;
|
|
227
236
|
if (!fromRegistry(have.dir) && have.hash() !== built.hash) {
|
|
228
237
|
return `${named} was built from another ${name} ${built.version} than the one in ${posix(relative(sources.project.dir, have.dir)) || '.'}`;
|
|
229
238
|
}
|
|
@@ -235,11 +244,31 @@ function mismatch(manifest: PrebuiltManifest, pkg: ModulePackage, sources: Sourc
|
|
|
235
244
|
}
|
|
236
245
|
}
|
|
237
246
|
const own = resolve(sources.project.pluginsDir) !== resolve(CORE_PLUGINS);
|
|
238
|
-
const shadow = own ? manifest.places.find(place =>
|
|
247
|
+
const shadow = own ? manifest.places.find(place => sources.ownFile(place)) : undefined;
|
|
239
248
|
if (shadow) return `${posix(relative(sources.project.dir, join(sources.project.pluginsDir, shadow)))} takes the place of a file ${named} was built from`;
|
|
240
249
|
return null;
|
|
241
250
|
}
|
|
242
251
|
|
|
252
|
+
/**
|
|
253
|
+
* How the ABI a module was built for (the identity and its imports, as its
|
|
254
|
+
* .aot carries them) differs from the project's - `amxts 0.2, the project
|
|
255
|
+
* has 0.3`, both whole within a line, an import of a later patch or of
|
|
256
|
+
* another shape - or false when the server's module would load it: a module
|
|
257
|
+
* built by one patch serves every patch of its line that has its imports.
|
|
258
|
+
*/
|
|
259
|
+
function otherAbi(built: string, have: string): string | false {
|
|
260
|
+
const [identity, ...imports] = built.split('\n');
|
|
261
|
+
if (abiLine(identity) !== abiLine(have)) {
|
|
262
|
+
const line = (abi: string) => releaseLine(abi.slice(0, abi.indexOf('+')));
|
|
263
|
+
return line(identity) === line(have) ? `amxts ${identity}, the project has ${have}` : `amxts ${line(identity)}, the project has ${line(have)}`;
|
|
264
|
+
}
|
|
265
|
+
const shapes = importShapes();
|
|
266
|
+
const misfit = imports.find(shape => shapes.get(importName(shape)) !== shape);
|
|
267
|
+
if (!misfit) return false;
|
|
268
|
+
const name = importName(misfit);
|
|
269
|
+
return shapes.has(name) ? `another shape of ${name}` : `amxts ${identity.slice(0, identity.indexOf('+'))} or later (it uses ${name}), the project has ${coreVersion()}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
243
272
|
/** The `new Forward<...>("name")` calls of a package whose arguments are not all text: their names. */
|
|
244
273
|
function typedForwards(pkg: ModulePackage): string[] {
|
|
245
274
|
const names: string[] = [];
|
|
@@ -261,6 +290,7 @@ async function prebuild(pkg: ModulePackage, sources: Sources): Promise<string> {
|
|
|
261
290
|
rmSync(dir, { recursive: true, force: true });
|
|
262
291
|
mkdirSync(dir, { recursive: true });
|
|
263
292
|
const owner = sources.ownerSource(pkg);
|
|
293
|
+
const core = installed(CORE_NAME, sources)!;
|
|
264
294
|
const plugin = { source: owner, output: '', root: CORE_PLUGINS, wamrc: wamrcPath(), signatures: join(CORE_DIR, 'runtime/natives.txt'), quick: false };
|
|
265
295
|
const natives: PluginNative[] = [];
|
|
266
296
|
const wasm = join(dir, `${pkg.short}.wasm`);
|
|
@@ -277,18 +307,19 @@ async function prebuild(pkg: ModulePackage, sources: Sources): Promise<string> {
|
|
|
277
307
|
const bytes = readFileSync(output);
|
|
278
308
|
systems[system] = { file: `${PREBUILT_DIR}/${system}/${pkg.short}.aot`, size: bytes.length, sha256: sha256(bytes) };
|
|
279
309
|
}
|
|
310
|
+
// The ABI the .aot carries: the identity, and the imports it uses.
|
|
311
|
+
const abi = readSection(new Uint8Array(readFileSync(wasm)), ABI_SECTION) ?? undefined;
|
|
280
312
|
rmSync(wasm, { force: true });
|
|
281
313
|
|
|
282
314
|
// What the compile read: its places in the tree, and the packages they are in.
|
|
283
315
|
const reached = sources.reach(join(CORE_PLUGINS, 'facade.ts'), sources.reach(join(CORE_PLUGINS, sources.entry(owner))));
|
|
284
316
|
const places = [...reached].map(place => posix(relative(CORE_PLUGINS, place))).sort();
|
|
285
317
|
const used = sources.project.modules.filter(each => each === pkg || [...reached].some(place => sources.packageOf(sources.real(place) ?? place) === each));
|
|
286
|
-
const core = installed(CORE_NAME, sources)!;
|
|
287
318
|
const manifest: PrebuiltManifest = {
|
|
288
319
|
format: FORMAT,
|
|
289
320
|
module: pkg.name,
|
|
290
321
|
version: pkg.version,
|
|
291
|
-
from: Object.fromEntries([[CORE_NAME, { version: core.version, hash: core.hash() }], ...used.map(each => [each.name, { version: each.version, hash: packageHash(each) }])]),
|
|
322
|
+
from: Object.fromEntries([[CORE_NAME, { version: core.version, hash: core.hash(), abi }], ...used.map(each => [each.name, { version: each.version, hash: packageHash(each) }])]),
|
|
292
323
|
options: Object.fromEntries(used.map(each => [each.name, optionsOf(sources.project, each.definition)])),
|
|
293
324
|
forwards: Object.fromEntries(used.flatMap(typedForwards).map(name => [name, declarationOf(name)])),
|
|
294
325
|
places,
|
package/scripts/project.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { createRequire } from 'node:module';
|
|
|
53
53
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
55
|
import ts from 'typescript';
|
|
56
|
+
import { serverFolder } from '../src/system.mjs';
|
|
56
57
|
import { coreImports, importedName, importTable, withAutoImports } from './auto-imports';
|
|
57
58
|
import { includeDirs } from './includes';
|
|
58
59
|
import { existsSync, readdirSync, readFileSync, statSync } from './tracked-fs';
|
|
@@ -667,9 +668,56 @@ export function coreEntryOf(place: string): string {
|
|
|
667
668
|
export function projectPlugins(project: Project): string[] {
|
|
668
669
|
const core = resolve(project.pluginsDir) === resolve(CORE_PLUGINS);
|
|
669
670
|
if (core && resolve(project.dir) !== resolve(CORE_DIR)) return [];
|
|
671
|
+
const stale = new Set(staleCopies(project));
|
|
670
672
|
return readdirSync(project.pluginsDir)
|
|
671
673
|
.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts') && !(core && NOT_PLUGINS.has(f)))
|
|
672
|
-
.map(f => join(project.pluginsDir, f))
|
|
674
|
+
.map(f => join(project.pluginsDir, f))
|
|
675
|
+
.filter(f => !stale.has(f));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** A server's addons/amxts: the module's files, written over on every start. */
|
|
679
|
+
export const SERVER_FOLDER = /[\\/]addons[\\/]amxts$/i;
|
|
680
|
+
|
|
681
|
+
/** The server's addons/amxts that AMXTS_SERVER names, or '' when it names none. */
|
|
682
|
+
export function serverDir(): string {
|
|
683
|
+
try {
|
|
684
|
+
const dir = serverFolder(process.env.AMXTS_SERVER ?? '');
|
|
685
|
+
return dir && resolve(dir);
|
|
686
|
+
} catch {
|
|
687
|
+
return '';
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Copies of the core's own API at the top of the project's plugins folder,
|
|
693
|
+
* left there by an older amxts: a file named as one of the core's as/ (or
|
|
694
|
+
* the editor's imports.d.ts) whose first line is the core's - a generator's
|
|
695
|
+
* header or that file's own first line. An author's file of the same name
|
|
696
|
+
* starts otherwise and stays a plugin. A server's addons/amxts/plugins of
|
|
697
|
+
* an older amxts holds them too: its module wrote them there.
|
|
698
|
+
*/
|
|
699
|
+
export function staleCopies(project: Project): string[] {
|
|
700
|
+
const dir = resolve(project.pluginsDir);
|
|
701
|
+
const parent = dirname(dir);
|
|
702
|
+
if (!existsSync(dir) || dir === resolve(CORE_PLUGINS) || SERVER_FOLDER.test(parent) || parent === serverDir()) return [];
|
|
703
|
+
coreFirstLines ??= new Map([...readdirSync(CORE_PLUGINS).filter(f => f.endsWith('.ts')).map(f => [f, firstLine(join(CORE_PLUGINS, f))] as const), ['imports.d.ts', '// GENERATED by ']]);
|
|
704
|
+
const core = coreFirstLines;
|
|
705
|
+
return readdirSync(dir)
|
|
706
|
+
.filter((f) => {
|
|
707
|
+
const header = core.get(f);
|
|
708
|
+
if (header === undefined) return false;
|
|
709
|
+
const line = firstLine(join(dir, f));
|
|
710
|
+
return line.startsWith('// GENERATED by ') || line === header;
|
|
711
|
+
})
|
|
712
|
+
.sort()
|
|
713
|
+
.map(f => join(dir, f));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** The first line of each file at the top of the core's as/, read once. */
|
|
717
|
+
let coreFirstLines: Map<string, string> | undefined;
|
|
718
|
+
|
|
719
|
+
function firstLine(path: string): string {
|
|
720
|
+
return readFileSync(path, 'utf8').split('\n', 1)[0].trimEnd();
|
|
673
721
|
}
|
|
674
722
|
|
|
675
723
|
/** The modules that run on the server - each in its owner plugin: every one but a library. */
|
|
@@ -747,7 +795,12 @@ export class Sources {
|
|
|
747
795
|
/** A plugin that imports a module package the config does not list. */
|
|
748
796
|
readonly problems: string[] = [];
|
|
749
797
|
|
|
750
|
-
|
|
798
|
+
/** Copies of the core's API an older amxts left in the plugins folder: the core's files are under them. */
|
|
799
|
+
private readonly stale: Set<string>;
|
|
800
|
+
|
|
801
|
+
constructor(readonly root: string, readonly project: Project) {
|
|
802
|
+
this.stale = new Set(staleCopies(project));
|
|
803
|
+
}
|
|
751
804
|
|
|
752
805
|
/**
|
|
753
806
|
* Whether a file is a plugin's code, which auto-imports reach: the
|
|
@@ -792,14 +845,20 @@ export class Sources {
|
|
|
792
845
|
return this.project.modules.find(pkg => path === pkg.module || path === pkg.natives || ofPackage(pkg, path)) ?? null;
|
|
793
846
|
}
|
|
794
847
|
|
|
848
|
+
/** The project's own file at this place under its plugins folder, or null: an old copy of the core's API is not one. */
|
|
849
|
+
ownFile(place: string): string | null {
|
|
850
|
+
const own = join(this.project.pluginsDir, place);
|
|
851
|
+
return existsSync(own) && !this.stale.has(own) ? own : null;
|
|
852
|
+
}
|
|
853
|
+
|
|
795
854
|
/** The file on disk at this place in the tree, or null. */
|
|
796
855
|
real(path: string): string | null {
|
|
797
856
|
const rel = this.rel(path);
|
|
798
857
|
if (!inside(this.root, resolve(path))) return existsSync(path) ? resolve(path) : null;
|
|
799
858
|
|
|
800
859
|
if (this.project.pluginsDir !== resolve(this.root)) {
|
|
801
|
-
const own =
|
|
802
|
-
if (
|
|
860
|
+
const own = this.ownFile(rel);
|
|
861
|
+
if (own) return own;
|
|
803
862
|
}
|
|
804
863
|
const core = resolve(this.root, rel);
|
|
805
864
|
if (existsSync(core)) return core;
|
package/scripts/reapi-events.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// The game events
|
|
2
|
-
// functions - and the build's refusal of a listener for one nothing else
|
|
1
|
+
// The game events only ReGameDLL's and ReHLDS's hookchains deliver - their
|
|
2
|
+
// own functions - and the build's refusal of a listener for one nothing else
|
|
3
3
|
// hears, in a project for plain HLDS.
|
|
4
4
|
//
|
|
5
|
-
// A server without
|
|
6
|
-
// (as/hooks.ts picks it once
|
|
7
|
-
// another stock hook (scripts/hlds-events.ts); a listener for one of
|
|
8
|
-
// is only a line in its console. A project whose amxts.config.ts
|
|
9
|
-
// `target: "hlds"` has said there is
|
|
10
|
-
// listener instead, with the file and the line.
|
|
5
|
+
// A server without them hears every other game event through the entity's
|
|
6
|
+
// function in its class (as/hooks.ts picks it once), and many of these
|
|
7
|
+
// through another stock hook (scripts/hlds-events.ts); a listener for one of
|
|
8
|
+
// the rest is only a line in its console. A project whose amxts.config.ts
|
|
9
|
+
// says `target: "hlds"` has said there is neither, so the build stops at
|
|
10
|
+
// such a listener instead, with the file and the line.
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
// @ts-ignore - shipped as JavaScript, with types beside it we do not need here
|
|
13
13
|
import * as asc from '../runtime/deps/assemblyscript/dist/assemblyscript.js';
|
|
@@ -76,7 +76,7 @@ export function reapiListeners(sources: any[], events = unheardEvents()): string
|
|
|
76
76
|
const event = gameEventOf(node);
|
|
77
77
|
if (event === undefined || !events.has(event)) return;
|
|
78
78
|
const line = source.text.slice(0, node.range.start).split('\n').length;
|
|
79
|
-
found.push(`${source.normalizedPath}:${line}: "${event}"
|
|
79
|
+
found.push(`${source.normalizedPath}:${line}: "${event}" is ReGameDLL's or ReHLDS's own, and amxts.config.ts's target is "hlds" - nothing on plain HLDS hears it. Listen for another event, or set target: "rehlds"`);
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
return found;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// env(): the settings a plugin reads of the server's environment and of its
|
|
2
|
+
// addons/amxts/.env, and the ones a plugin cannot start without.
|
|
3
|
+
//
|
|
4
|
+
// const token = env("KZ_MAP_TOKEN"); // required text: the plugin does not start without it
|
|
5
|
+
// const limit = env("KZ_MAX_RECORDS", 100); // a number, 100 when the server has none
|
|
6
|
+
// const debug = env("KZ_DEBUG", false); // an on/off switch
|
|
7
|
+
//
|
|
8
|
+
// Each call's name, and the kind and value of its default, go into the
|
|
9
|
+
// plugin's .aot as the section amxts.env, a line each: `KZ_MAP_TOKEN` for a
|
|
10
|
+
// required one, `KZ_MAX_RECORDS number 100`, `KZ_DEBUG boolean false`,
|
|
11
|
+
// `KZ_MIRROR string https://...`. The module refuses the plugin while a
|
|
12
|
+
// required one is nowhere or one is there and not of its kind
|
|
13
|
+
// (runtime/src/module.cpp, MissingEnv; envProblems here is the same), and
|
|
14
|
+
// `amxts dev` warns of one the server's .env lacks. The name and the default
|
|
15
|
+
// are written out in the call: the build reads them there. The module reads
|
|
16
|
+
// .env as parseDotenv does.
|
|
17
|
+
import ts from 'typescript';
|
|
18
|
+
|
|
19
|
+
/** The custom section of a plugin that lists the variables it reads: wamrc copies it into the .aot. */
|
|
20
|
+
export const ENV_SECTION = 'amxts.env';
|
|
21
|
+
|
|
22
|
+
/** What a plugin imports the facade's env() from, once the build has rewritten its imports. */
|
|
23
|
+
const FACADE = new Set(['~/facade', '@amxts/core']);
|
|
24
|
+
|
|
25
|
+
/** A number as env() takes one: decimal, a fraction and an exponent allowed. */
|
|
26
|
+
const NUMBER = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
27
|
+
|
|
28
|
+
/** The words of an on/off switch, in any case. */
|
|
29
|
+
const SWITCH = new Set(['1', '0', 'true', 'false', 'yes', 'no', 'on', 'off']);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A .env file's variables: `KEY=value` lines, `#` comments, a value in
|
|
33
|
+
* quotes kept as it is - `#` and spaces too - and one without them cut at a
|
|
34
|
+
* ` #` comment and trimmed. A line without `=` is no variable.
|
|
35
|
+
*/
|
|
36
|
+
export function parseDotenv(text: string): Map<string, string> {
|
|
37
|
+
const variables = new Map<string, string>();
|
|
38
|
+
for (const raw of text.split('\n')) {
|
|
39
|
+
const line = raw.trim();
|
|
40
|
+
const at = line.indexOf('=');
|
|
41
|
+
if (line.startsWith('#') || at <= 0) continue;
|
|
42
|
+
const value = line.slice(at + 1).trim();
|
|
43
|
+
const quote = value[0];
|
|
44
|
+
const end = quote === '"' || quote === '\'' ? value.indexOf(quote, 1) : -1;
|
|
45
|
+
variables.set(line.slice(0, at).trim(), end > 0 ? value.slice(1, end) : value.replace(/\s+#.*$/, ''));
|
|
46
|
+
}
|
|
47
|
+
return variables;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One call of the facade's env(): its section line - null when its name or default is not written out - and its line in the file. */
|
|
51
|
+
export interface EnvCall {
|
|
52
|
+
entry: string | null;
|
|
53
|
+
line: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A default as the section writes it - `number 100`, `boolean false`, `string text` - or null when it is not written out. */
|
|
57
|
+
function defaultOf(node: ts.Expression): string | null {
|
|
58
|
+
if (ts.isStringLiteralLike(node)) return `string ${node.text}`;
|
|
59
|
+
if (ts.isNumericLiteral(node)) return `number ${node.text}`;
|
|
60
|
+
if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) return `number -${node.operand.text}`;
|
|
61
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return `boolean ${node.getText()}`;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The calls of the facade's env() in a file, by the name it imports env() under. */
|
|
66
|
+
export function envCallsOf(text: string): EnvCall[] {
|
|
67
|
+
if (!text.includes('env')) return [];
|
|
68
|
+
const source = ts.createSourceFile('file.ts', text, ts.ScriptTarget.Latest, true);
|
|
69
|
+
const local = source.statements.flatMap((statement) => {
|
|
70
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !FACADE.has(statement.moduleSpecifier.text)) return [];
|
|
71
|
+
const bindings = statement.importClause?.namedBindings;
|
|
72
|
+
return bindings && ts.isNamedImports(bindings) ? bindings.elements.filter(each => (each.propertyName ?? each.name).text === 'env').map(each => each.name.text) : [];
|
|
73
|
+
})[0];
|
|
74
|
+
if (!local) return [];
|
|
75
|
+
|
|
76
|
+
const calls: EnvCall[] = [];
|
|
77
|
+
const visit = (node: ts.Node) => {
|
|
78
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === local) {
|
|
79
|
+
const [name, fallback] = node.arguments;
|
|
80
|
+
const kind = fallback ? defaultOf(fallback) : '';
|
|
81
|
+
calls.push({
|
|
82
|
+
entry: name && ts.isStringLiteralLike(name) && kind !== null ? `${name.text}${kind ? ` ${kind}` : ''}` : null,
|
|
83
|
+
line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(node, visit);
|
|
87
|
+
};
|
|
88
|
+
visit(source);
|
|
89
|
+
return calls;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One compile's section lines: `read` goes into the compile's readFile with
|
|
94
|
+
* every file the plugin reaches, `entries` gives them after, one a name, and
|
|
95
|
+
* `problems` each call whose name or default is not written out; a new one
|
|
96
|
+
* for every compile.
|
|
97
|
+
*/
|
|
98
|
+
export function envBuild() {
|
|
99
|
+
const entries = new Map<string, string>();
|
|
100
|
+
const problems: string[] = [];
|
|
101
|
+
return {
|
|
102
|
+
read(file: string, text: string): void {
|
|
103
|
+
for (const call of envCallsOf(text)) {
|
|
104
|
+
if (call.entry === null) {
|
|
105
|
+
problems.push(`${file.replace(/\\/g, '/')}:${call.line}: env() takes its name and its default written out - env("MYPLUGIN_TOKEN"), env("MYPLUGIN_LIMIT", 100), env("MYPLUGIN_DEBUG", false), env("MYPLUGIN_URL", "https://...") - the build reads what a plugin needs from the call`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const name = call.entry.split(' ', 1)[0];
|
|
109
|
+
// Required wherever one call has no default.
|
|
110
|
+
if (!entries.has(name) || call.entry === name) entries.set(name, call.entry);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
entries: () => [...entries.keys()].sort().map(name => entries.get(name)!),
|
|
114
|
+
problems,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* What the section's lines need that `variables` has not, as the module's
|
|
120
|
+
* line names it: `KZ_MAP_TOKEN` for a required one missing,
|
|
121
|
+
* `KZ_MAX_RECORDS as a number` for one there and of another kind.
|
|
122
|
+
*/
|
|
123
|
+
export function envProblems(entries: string[], variables: Map<string, string>): string[] {
|
|
124
|
+
return entries.flatMap((entry) => {
|
|
125
|
+
const [name, kind] = entry.split(' ');
|
|
126
|
+
const value = variables.get(name);
|
|
127
|
+
if (value === undefined) return kind ? [] : [name];
|
|
128
|
+
if (kind === 'number' && !NUMBER.test(value)) return [`${name} as a number`];
|
|
129
|
+
if (kind === 'boolean' && !SWITCH.has(value.toLowerCase())) return [`${name} as a boolean`];
|
|
130
|
+
return [];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The section of a plugin's .aot by its name, as the module reads it (AotSection); null when it has none. */
|
|
135
|
+
export function aotSection(aot: Uint8Array, wanted: string): string | null {
|
|
136
|
+
const view = new DataView(aot.buffer, aot.byteOffset, aot.byteLength);
|
|
137
|
+
const name = new TextEncoder().encode(`${wanted}\0`);
|
|
138
|
+
for (let at = 8; at + 8 <= aot.length; at = (at + 8 + view.getUint32(at + 4, true) + 3) & ~3) {
|
|
139
|
+
const length = view.getUint32(at + 4, true);
|
|
140
|
+
const body = at + 8;
|
|
141
|
+
if (view.getUint32(at, true) !== 100 || length < 6 + name.length || view.getUint32(body, true) !== 0 || view.getUint16(body + 4, true) !== name.length) continue;
|
|
142
|
+
if (name.every((byte, i) => aot[body + 6 + i] === byte)) return new TextDecoder().decode(aot.subarray(body + 6 + name.length, body + length));
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
// What crosses, and how:
|
|
19
19
|
//
|
|
20
20
|
// - number, boolean, string (and a union of string literals, which is a
|
|
21
|
-
// string), arrays of them, `T | null
|
|
21
|
+
// string), arrays of them, `T | null`, a `Record<string, T>` - its keys
|
|
22
|
+
// and values in order; a rest parameter as its array;
|
|
22
23
|
// - Player, by its id;
|
|
23
24
|
// - an object the module hands out and takes back - it is returned by one
|
|
24
25
|
// exported function and taken by another: a config, a section - or hands
|
|
@@ -100,6 +101,7 @@ type Wire
|
|
|
100
101
|
| { kind: 'player' }
|
|
101
102
|
| { kind: 'null'; of: Wire }
|
|
102
103
|
| { kind: 'array'; of: Wire }
|
|
104
|
+
| { kind: 'dict'; of: Wire }
|
|
103
105
|
| { kind: 'fn'; params: Wire[]; result: Wire | null; sig: number }
|
|
104
106
|
| { kind: 'record'; cls: ClassInfo }
|
|
105
107
|
| { kind: 'handle'; cls: ClassInfo };
|
|
@@ -132,6 +134,8 @@ interface ExportedFunction {
|
|
|
132
134
|
paramNames: string[];
|
|
133
135
|
wires: Wire[];
|
|
134
136
|
result: Wire | null;
|
|
137
|
+
/** Its last parameter is a rest parameter, `...names: string[]`: the owner spreads the array into it. */
|
|
138
|
+
rest?: boolean;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
export interface ModuleAnalysis {
|
|
@@ -434,6 +438,11 @@ function describe(program: any, name: string): ModuleAnalysis {
|
|
|
434
438
|
const of = toWire(cls.typeArguments[0], context);
|
|
435
439
|
return of ? { kind: 'array', of } : null;
|
|
436
440
|
}
|
|
441
|
+
// A Record of text keys, `{ KZ_CP: checkpoint }`: its keys and values, in order.
|
|
442
|
+
if (program.resolver.isRecordClass(cls) && String(cls.typeArguments[0]) === '~lib/string/String') {
|
|
443
|
+
const of = toWire(cls.typeArguments[1], context);
|
|
444
|
+
return of ? { kind: 'dict', of } : null;
|
|
445
|
+
}
|
|
437
446
|
if (cls.internalName === '~lib/~/facade/Player') return { kind: 'player' };
|
|
438
447
|
const info = classInfo(cls, context);
|
|
439
448
|
if (!info) return null;
|
|
@@ -459,13 +468,12 @@ function describe(program: any, name: string): ModuleAnalysis {
|
|
|
459
468
|
const params: string[] = [];
|
|
460
469
|
const paramNames: string[] = [];
|
|
461
470
|
const wires: Wire[] = [];
|
|
471
|
+
let rest = false;
|
|
462
472
|
declaration.signature.parameters.forEach((param: any, i: number) => {
|
|
463
473
|
const paramName = param.name.text;
|
|
464
474
|
const pcontext = where(`${context} - parameter "${paramName}"`);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
475
|
+
// A rest parameter crosses as the array it is.
|
|
476
|
+
if (param.parameterKind === as.ParameterKind.Rest) rest = true;
|
|
469
477
|
if (param.initializer && !plainDefault(param.initializer)) {
|
|
470
478
|
problems.push(`${pcontext}: its default is computed - the proxy evaluates it in the other plugin, so it has to be a literal`);
|
|
471
479
|
return;
|
|
@@ -484,7 +492,7 @@ function describe(program: any, name: string): ModuleAnalysis {
|
|
|
484
492
|
}
|
|
485
493
|
const result = returned === 'void' ? null : toWire(returns, where(`${context} - its result`));
|
|
486
494
|
if (returned !== 'void' && !result) return null;
|
|
487
|
-
return { name: fnName, params, paramNames, wires, result };
|
|
495
|
+
return { name: fnName, params, paramNames, wires, result, rest };
|
|
488
496
|
}
|
|
489
497
|
|
|
490
498
|
for (const { name: fnName, element, instance } of exported) {
|
|
@@ -573,6 +581,7 @@ function wireKey(wire: Wire): string {
|
|
|
573
581
|
case 'player': return 'player';
|
|
574
582
|
case 'null': return `${wireKey(wire.of)}?`;
|
|
575
583
|
case 'array': return `${wireKey(wire.of)}[]`;
|
|
584
|
+
case 'dict': return `{${wireKey(wire.of)}}`;
|
|
576
585
|
case 'fn': return `(${wire.params.map(wireKey).join(',')})=>${wire.result ? wireKey(wire.result) : 'void'}`;
|
|
577
586
|
case 'record': return `rec ${wire.cls.internal}`;
|
|
578
587
|
case 'handle': return `handle ${wire.cls.internal}`;
|
|
@@ -609,6 +618,7 @@ class Codec {
|
|
|
609
618
|
case 'player': return '__Player';
|
|
610
619
|
case 'null': return wire.of.kind === 'fn' ? `(${this.type(wire.of)}) | null` : `${this.type(wire.of)} | null`;
|
|
611
620
|
case 'array': return `Array<${this.type(wire.of)}>`;
|
|
621
|
+
case 'dict': return `Record<string, ${this.type(wire.of)}>`;
|
|
612
622
|
case 'fn': return `(${wire.params.map((p, i) => `a${i}: ${this.type(p)}`).join(', ')}) => ${wire.result ? this.type(wire.result) : 'void'}`;
|
|
613
623
|
case 'record':
|
|
614
624
|
case 'handle': return this.className(wire.cls);
|
|
@@ -677,6 +687,12 @@ class Codec {
|
|
|
677
687
|
lines.push(`function __r${n}(r: __Reader): ${T} {\n\tconst count = r.i32();\n\tconst list = new Array<${this.type(wire.of)}>();\n\tfor (let i: i32 = 0; i < count; i++) list.push(__r${inner}(r));\n\treturn list;\n}`);
|
|
678
688
|
break;
|
|
679
689
|
}
|
|
690
|
+
case 'dict': {
|
|
691
|
+
const inner = this.id(wire.of);
|
|
692
|
+
lines.push(`function __w${n}(w: __Writer, v: ${T}): void {\n\tconst keys = Object.keys(v);\n\tw.i32(keys.length);\n\tfor (let i: i32 = 0; i < keys.length; i++) {\n\t\tw.str(keys[i]);\n\t\t__w${inner}(w, v[keys[i]]);\n\t}\n}`);
|
|
693
|
+
lines.push(`function __r${n}(r: __Reader): ${T} {\n\tconst count = r.i32();\n\tconst record: ${T} = {};\n\tfor (let i: i32 = 0; i < count; i++) {\n\t\tconst key = r.str();\n\t\trecord[key] = __r${inner}(r);\n\t}\n\treturn record;\n}`);
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
680
696
|
case 'fn':
|
|
681
697
|
lines.push(`function __w${n}(w: __Writer, v: ${T}): void { w.i32(__sendFunction(changetype<usize>(v), ${wire.sig}, __invoke)); }`);
|
|
682
698
|
lines.push(`function __r${n}(r: __Reader): ${T} { return changetype<${T}>(__receiveFunction(r, changetype<usize>(__stub${wire.sig}))); }`);
|
|
@@ -875,7 +891,7 @@ export function serveSource(analysis: ModuleAnalysis): string {
|
|
|
875
891
|
|
|
876
892
|
/** A call of `fn` on `target` - the module, or a handle's object read first. */
|
|
877
893
|
const callCase = (op: number, fn: ExportedFunction, target: string, before: string[]) => {
|
|
878
|
-
const args = fn.paramNames.map((_, i) =>
|
|
894
|
+
const args = fn.paramNames.map((_, i) => `${fn.rest && i === fn.paramNames.length - 1 ? '...' : ''}a${i}`).join(', ');
|
|
879
895
|
const lines = [
|
|
880
896
|
...before,
|
|
881
897
|
...fn.wires.map((wire, i) => `\t\t\tconst a${i} = ${codec.read(wire, 'r')};`),
|
package/scripts/system.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
7
7
|
import { executable, MODULE_FILE } from '../src/system.mjs';
|
|
8
8
|
|
|
9
9
|
export type { ServerSystem, System } from '../src/system.mjs';
|
|
10
|
-
export { describeSystem, detectServerSystem, executable, HOST_SYSTEM, manifestName, MODULE_FILE, moduleVersion, parseSystem, serverFiles, serverFolder, serverImage, serverSystem, SYSTEM_NAME, SYSTEMS, TARGET_ABI, WAMRC_PACKAGE, wamrcPath } from '../src/system.mjs';
|
|
10
|
+
export { describeSystem, detectServerSystem, executable, HOST_SYSTEM, manifestName, MODULE_FILE, moduleAbiOf, moduleVersion, parseSystem, serverFiles, serverFolder, serverImage, serverSystem, SYSTEM_NAME, SYSTEMS, TARGET_ABI, WAMRC_PACKAGE, wamrcPath } from '../src/system.mjs';
|
|
11
11
|
|
|
12
12
|
const CORE = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
13
13
|
|