@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/tracked-fs.ts
CHANGED
|
@@ -73,10 +73,19 @@ export function readdirSync(path: PathLike): string[] {
|
|
|
73
73
|
note('readdir', path, 'missing');
|
|
74
74
|
throw error;
|
|
75
75
|
}
|
|
76
|
-
note('readdir', path,
|
|
76
|
+
note('readdir', path, listingOf(names));
|
|
77
77
|
return names;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* What a folder's listing is noted as: its names but the hidden ones - a
|
|
82
|
+
* project's node_modules gains `.cache` with the first build's own cache,
|
|
83
|
+
* which no compile depends on.
|
|
84
|
+
*/
|
|
85
|
+
function listingOf(names: string[]): string {
|
|
86
|
+
return hashOf(names.filter(name => !name.startsWith('.')).sort().join('\n'));
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
/** `run`, with every file read it makes - and anything else read meanwhile - noted. */
|
|
81
90
|
export async function recordReads<T>(run: () => Promise<T>): Promise<{ reads: Reads; result: PromiseSettledResult<T> }> {
|
|
82
91
|
const reads: Reads = {};
|
|
@@ -97,7 +106,7 @@ function seenNow(op: string, path: string): string {
|
|
|
97
106
|
case 'read': return hashOf(fs.readFileSync(path));
|
|
98
107
|
case 'exists': return String(fs.existsSync(path));
|
|
99
108
|
case 'stat': return kindOf(fs.statSync(path));
|
|
100
|
-
case 'readdir': return
|
|
109
|
+
case 'readdir': return listingOf(fs.readdirSync(path));
|
|
101
110
|
default: return 'unknown';
|
|
102
111
|
}
|
|
103
112
|
} catch {
|
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
// - the call's `server.addCommand<KickArgs>` becomes that function's name,
|
|
33
33
|
// padded, so the rest of the line keeps its columns.
|
|
34
34
|
//
|
|
35
|
+
// Several names, `server.addCommand(["/cp", "cp"], ...)`, are one command:
|
|
36
|
+
// the usage first, then names without arguments. A player's command without
|
|
37
|
+
// arguments takes a handler of the player too, `(player) => ...`: its
|
|
38
|
+
// argument's type is the class and `__AmxtsPlayer`, which the compiler hands
|
|
39
|
+
// the class's `player` (`Context & Player`).
|
|
40
|
+
//
|
|
41
|
+
// A player's command added at the top level of the file with a function
|
|
42
|
+
// declared there by name, `server.addCommand("/hp", onHp)`, gets no closure:
|
|
43
|
+
// its parser is a function of its own that calls the handler by its name, and
|
|
44
|
+
// the module calls another for the command typed in the console - in the
|
|
45
|
+
// facade's place, the words taken from __commandWords.
|
|
46
|
+
//
|
|
35
47
|
// The interface is read from the source text - a syntax tree, no type checker
|
|
36
48
|
// - in this file or imported, as the typed configs read theirs.
|
|
37
49
|
import type { ImportReader } from './typed-configs';
|
|
@@ -57,6 +69,14 @@ function usageOf(node: ts.Expression, typed: boolean, file: ts.SourceFile): Usag
|
|
|
57
69
|
return { name: '', args: [] };
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
/** A command's other name, after its usage: a name in place, without arguments. */
|
|
73
|
+
function aliasOf(node: ts.Expression, file: ts.SourceFile): string {
|
|
74
|
+
if (!ts.isStringLiteralLike(node)) throw new ShapeError(node, file, 'a command\'s other names are written in place: server.addCommand(["/cp", "cp"], ...)');
|
|
75
|
+
const usage = usageOrFail(parseUsage(node.text), node, file);
|
|
76
|
+
if (usage.args.length > 0) throw new ShapeError(node, file, `"${node.text}" - the arguments are written once, in the first name: ["/kick <target>", "/k"]`);
|
|
77
|
+
return usage.name;
|
|
78
|
+
}
|
|
79
|
+
|
|
60
80
|
function usageOrFail(usage: Usage | string, node: ts.Node, file: ts.SourceFile): Usage {
|
|
61
81
|
if (typeof usage === 'string') throw new ShapeError(node, file, usage);
|
|
62
82
|
return usage;
|
|
@@ -165,13 +185,30 @@ function readOf(arg: Argument, at: number, last: boolean, tabs: string): string[
|
|
|
165
185
|
}
|
|
166
186
|
}
|
|
167
187
|
|
|
188
|
+
/**
|
|
189
|
+
* A handler declared by name that the parser calls directly: its name, and the
|
|
190
|
+
* type its parameter is declared with - the interface, which the generated
|
|
191
|
+
* class implements - or null when it takes none.
|
|
192
|
+
*/
|
|
193
|
+
interface Direct {
|
|
194
|
+
name: string;
|
|
195
|
+
takes: string | null;
|
|
196
|
+
}
|
|
197
|
+
|
|
168
198
|
/**
|
|
169
199
|
* The generated code for one call: the class of its handler's argument - an
|
|
170
200
|
* implementation of `shape`, the interface the call names - and the function
|
|
171
|
-
* that registers it
|
|
201
|
+
* that registers it; with `direct`, the parser and the console's function
|
|
202
|
+
* besides, which call the handler by its name.
|
|
172
203
|
*/
|
|
173
|
-
function commandCode(index: number, method: 'addCommand' | 'addServerCommand', args: Argument[], shape: string | undefined): string {
|
|
204
|
+
function commandCode(index: number, method: 'addCommand' | 'addServerCommand', args: Argument[], shape: string | undefined, kept: boolean, direct: Direct | null, names: boolean): string {
|
|
174
205
|
const player = method === 'addCommand';
|
|
206
|
+
// A command without arguments takes a handler of the player too.
|
|
207
|
+
const takes = `__AmxtsCommandArgs${index}${player && !shape && args.length === 0 ? ' & __AmxtsPlayer' : ''}`;
|
|
208
|
+
const usage = names ? 'usage: string[]' : 'usage: string';
|
|
209
|
+
const register = (run: string, consoleIndex: string) => names
|
|
210
|
+
? `\t__amxtsServer.__addCommand(usage[0], ${run}, options, ${consoleIndex}, usage.slice(1));`
|
|
211
|
+
: `\t__amxtsServer.__addCommand(usage, ${run}, options${consoleIndex === '0' ? '' : `, ${consoleIndex}`});`;
|
|
175
212
|
const type = `__AmxtsCommandArgs${index}`;
|
|
176
213
|
const required = args.filter(arg => !arg.optional).length;
|
|
177
214
|
const lastIsText = args.length > 0 && args[args.length - 1].kind.kind === 'text';
|
|
@@ -179,25 +216,120 @@ function commandCode(index: number, method: 'addCommand' | 'addServerCommand', a
|
|
|
179
216
|
const lines = readOf(arg, at, at === args.length - 1, arg.optional ? '\t\t\t' : '\t\t');
|
|
180
217
|
return arg.optional ? [`\t\tif (words.count > ${at}) {`, ...lines, '\t\t}'] : lines;
|
|
181
218
|
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
...args.map(arg => `\t${arg.name}${arg.optional ? '?' : '!'}: ${typeOf(arg)};`),
|
|
186
|
-
'}',
|
|
187
|
-
`function __amxtsCommand${index}(usage: string, handler: (args: ${type}) => void${player ? ', options: __AmxtsCommandOptions = {}' : ''}): void {`,
|
|
188
|
-
`\t__amxtsServer.__${method}(usage, (words: __AmxtsCommandWords): void => {`,
|
|
189
|
-
`\t\tconst args = new ${type}();`,
|
|
219
|
+
// The words read into the handler's argument, inside the closure; one tab less in a function of its own.
|
|
220
|
+
const parse = [
|
|
221
|
+
`\t\tconst args = ${kept ? `__amxtsArgs${index}` : `new ${type}()`};`,
|
|
190
222
|
...(player ? ['\t\targs.player = words.player!;'] : []),
|
|
191
223
|
...(required > 0 ? [`\t\tif (!words.need(${required})) return;`] : []),
|
|
192
224
|
...reads,
|
|
193
225
|
...(lastIsText ? [] : [`\t\tif (!words.done(${args.length})) return;`]),
|
|
194
226
|
'\t\tif (words.failed) return;',
|
|
195
|
-
|
|
196
|
-
|
|
227
|
+
];
|
|
228
|
+
const run = ['(words: __AmxtsCommandWords): void => {', ...parse, '\t\thandler(args);', '\t}'].join('\n');
|
|
229
|
+
const closure = [
|
|
230
|
+
`function __amxtsCommand${index}(${usage}, handler: (args: ${takes}) => void${player ? ', options: __AmxtsCommandOptions = {}' : ''}): void {`,
|
|
231
|
+
player ? register(run, '0') : `\t__amxtsServer.__${method}(usage, ${run});`,
|
|
232
|
+
'}',
|
|
233
|
+
];
|
|
234
|
+
return [
|
|
235
|
+
`class ${type}${shape ? ` implements ${shape}` : ''} {`,
|
|
236
|
+
...(player ? ['\tplayer!: __AmxtsPlayer;'] : []),
|
|
237
|
+
...args.map(arg => `\t${arg.name}${arg.optional ? '?' : '!'}: ${typeOf(arg)};`),
|
|
197
238
|
'}',
|
|
239
|
+
...(kept ? [`const __amxtsArgs${index} = new ${type}();`] : []),
|
|
240
|
+
...(direct ? directCode(index, direct, args.length > 0, parse, usage, register) : closure),
|
|
198
241
|
].join('\n');
|
|
199
242
|
}
|
|
200
243
|
|
|
244
|
+
/**
|
|
245
|
+
* A command's functions for a handler the parser calls by its name: the
|
|
246
|
+
* parser, the one the module calls for the command typed in the console - a
|
|
247
|
+
* command of no words for a handler of none reads only a word too many - and
|
|
248
|
+
* the one that registers them.
|
|
249
|
+
*/
|
|
250
|
+
function directCode(index: number, direct: Direct, words: boolean, parse: string[], usage: string, register: (run: string, consoleIndex: string) => string): string[] {
|
|
251
|
+
const run = `__amxtsCommandRun${index}`;
|
|
252
|
+
const entry = direct.takes == null && !words
|
|
253
|
+
? [
|
|
254
|
+
'\tif (argc > 1) {',
|
|
255
|
+
'\t\t__amxtsCommandUsage(tag, id);',
|
|
256
|
+
'\t\treturn;',
|
|
257
|
+
'\t}',
|
|
258
|
+
'\tconst ambient = __co_ambient_player;',
|
|
259
|
+
'\t__co_ambient_player = id;',
|
|
260
|
+
`\t${direct.name}();`,
|
|
261
|
+
'\t__co_ambient_player = ambient;',
|
|
262
|
+
]
|
|
263
|
+
: [
|
|
264
|
+
'\tconst words = __amxtsCommandWords(tag, id, argc);',
|
|
265
|
+
`\t${run}(words);`,
|
|
266
|
+
'\t__amxtsCommandDone(words);',
|
|
267
|
+
];
|
|
268
|
+
// A handler of the player is given the player, one of the arguments the object.
|
|
269
|
+
const given = direct.takes == null ? '' : direct.takes === 'Player' ? 'args.player' : 'args';
|
|
270
|
+
const handler = direct.takes === 'Player' ? '__AmxtsPlayer' : direct.takes ?? `__AmxtsCommandArgs${index}`;
|
|
271
|
+
return [
|
|
272
|
+
`function ${run}(words: __AmxtsCommandWords): void {`,
|
|
273
|
+
...parse.map(line => line.slice(1)),
|
|
274
|
+
`\t${direct.name}(${given});`,
|
|
275
|
+
'}',
|
|
276
|
+
`function __amxtsCommandConsole${index}(tag: i32, id: i32, access: i32, unused: i32, argc: i32): void {`,
|
|
277
|
+
...entry,
|
|
278
|
+
'}',
|
|
279
|
+
`function __amxtsCommand${index}(${usage}, handler: (args: ${handler}) => void, options: __AmxtsCommandOptions = {}): void {`,
|
|
280
|
+
register(run, `__amxtsCommandConsole${index}.index`),
|
|
281
|
+
'}',
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The handler the parser can call by its name: a function declared by name at
|
|
287
|
+
* the top level of the file, not async, not generic, given to a player's
|
|
288
|
+
* command added at the top level too - where no other declaration can take
|
|
289
|
+
* its name.
|
|
290
|
+
*/
|
|
291
|
+
function directOf(call: ts.CallExpression, method: string, file: ts.SourceFile): Direct | null {
|
|
292
|
+
const handler = call.arguments[1];
|
|
293
|
+
if (method !== 'addCommand' || !handler || !ts.isIdentifier(handler)) return null;
|
|
294
|
+
if (!ts.isExpressionStatement(call.parent) || call.parent.parent !== file) return null;
|
|
295
|
+
const fn = file.statements.find((each): each is ts.FunctionDeclaration => ts.isFunctionDeclaration(each) && each.name?.text === handler.text);
|
|
296
|
+
if (!fn || fn.typeParameters || fn.modifiers?.some(each => each.kind === ts.SyntaxKind.AsyncKeyword)) return null;
|
|
297
|
+
const parameter = fn.parameters[0];
|
|
298
|
+
if (parameter && !parameter.type) return null;
|
|
299
|
+
return { name: handler.text, takes: parameter ? parameter.type!.getText(file) : null };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Whether a handler can keep nothing of the object its arguments come in: it
|
|
304
|
+
* takes none, or takes them apart as it starts - `({ player, target }) =>`,
|
|
305
|
+
* or a function of this file declared so. Then every call of the command
|
|
306
|
+
* hands it the same object, filled anew: a command allocates nothing. One
|
|
307
|
+
* with an optional argument gets a new one, which leaves an argument not
|
|
308
|
+
* typed unset.
|
|
309
|
+
*/
|
|
310
|
+
function keepsNothing(handler: ts.Expression | undefined, file: ts.SourceFile): boolean {
|
|
311
|
+
const named = handler && ts.isIdentifier(handler) ? handler.text : null;
|
|
312
|
+
const fn = handler && (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler))
|
|
313
|
+
? handler
|
|
314
|
+
: file.statements.find((each): each is ts.FunctionDeclaration => ts.isFunctionDeclaration(each) && named != null && each.name?.text === named);
|
|
315
|
+
if (!fn) return false;
|
|
316
|
+
const first = fn.parameters[0];
|
|
317
|
+
return !first || takesPlayer(fn, file) || (ts.isObjectBindingPattern(first.name) && first.name.elements.every(each => !each.dotDotDotToken));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Whether a handler takes the player rather than the command's object, as
|
|
322
|
+
* the compiler decides it (`Context & Player`): its parameter is a name,
|
|
323
|
+
* written `Player` or not typed, and it never reads the name's `.player`.
|
|
324
|
+
*/
|
|
325
|
+
function takesPlayer(fn: ts.SignatureDeclaration, file: ts.SourceFile): boolean {
|
|
326
|
+
const first = fn.parameters[0];
|
|
327
|
+
if (!first || !ts.isIdentifier(first.name)) return false;
|
|
328
|
+
if (first.type) return first.type.getText(file) === 'Player';
|
|
329
|
+
const body = 'body' in fn && fn.body ? fn.body.getText(file) : '';
|
|
330
|
+
return !new RegExp(`(?<![\\w$])${first.name.text}\\??\\.player(?![\\w$])`).test(body);
|
|
331
|
+
}
|
|
332
|
+
|
|
201
333
|
export interface TypedCommands {
|
|
202
334
|
text: string;
|
|
203
335
|
problems: string[];
|
|
@@ -236,10 +368,16 @@ export function typedCommands(path: string, display: string, text: string, impor
|
|
|
236
368
|
try {
|
|
237
369
|
const usageNode = node.arguments[0];
|
|
238
370
|
if (!usageNode) throw new ShapeError(node, file, 'the usage is missing: server.addCommand("/hp", ({ player }) => ...)');
|
|
239
|
-
|
|
371
|
+
// ["/cp", "cp"]: the usage, then the command's other names.
|
|
372
|
+
const names = ts.isArrayLiteralExpression(usageNode) ? usageNode.elements : null;
|
|
373
|
+
if (names && (method !== 'addCommand' || names.length === 0)) throw new ShapeError(usageNode, file, method === 'addCommand' ? 'the names are missing: server.addCommand(["/cp", "cp"], ...)' : 'a server command has one name');
|
|
374
|
+
const usage = usageOf(names ? names[0] : usageNode, !!node.typeArguments?.length, file);
|
|
375
|
+
names?.slice(1).forEach(each => aliasOf(each, file));
|
|
240
376
|
const name = `__amxtsCommand${functions.length}`;
|
|
241
377
|
const shape = node.typeArguments?.[0]?.getText(file);
|
|
242
|
-
|
|
378
|
+
const args = argumentsOf(node, usage, shapes, at, method === 'addCommand');
|
|
379
|
+
const kept = !args.some(arg => arg.optional) && keepsNothing(node.arguments[1], file);
|
|
380
|
+
functions.push(commandCode(functions.length, method, args, shape, kept, directOf(node, method, file), !!names));
|
|
243
381
|
const start = node.expression.getStart(file);
|
|
244
382
|
const end = node.arguments.pos - 1;
|
|
245
383
|
edits.push({ start, end, with: name.padEnd(end - start) });
|
|
@@ -258,6 +396,6 @@ export function typedCommands(path: string, display: string, text: string, impor
|
|
|
258
396
|
for (const edit of edits.sort((a, b) => b.start - a.start)) out = out.slice(0, edit.start) + edit.with + out.slice(edit.end);
|
|
259
397
|
|
|
260
398
|
const code = ['// GENERATED by scripts/typed-commands.ts: the commands this file adds, each word read as its type says.', ...functions].join('\n');
|
|
261
|
-
const facade = `import { server as __amxtsServer, Player as __AmxtsPlayer, CommandOptions as __AmxtsCommandOptions, __CommandWords as __AmxtsCommandWords } from "${FACADE}";`;
|
|
399
|
+
const facade = `import { server as __amxtsServer, Player as __AmxtsPlayer, CommandOptions as __AmxtsCommandOptions, __CommandWords as __AmxtsCommandWords, __commandWords as __amxtsCommandWords, __commandDone as __amxtsCommandDone, __commandUsage as __amxtsCommandUsage } from "${FACADE}";`;
|
|
262
400
|
return { text: withGenerated(path, out, code, facade), problems };
|
|
263
401
|
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { Change, Edit, Left } from './upgrade';
|
|
2
|
+
// The calls `amxts upgrade` brings up to date (scripts/upgrade.ts): the ones
|
|
3
|
+
// whose shape changed, not their name.
|
|
4
|
+
//
|
|
5
|
+
// - `print(player, text)` is `player.print(text)`, `print(0, text)` is
|
|
6
|
+
// `server.print(text)`, and `print({ id, variant }, text)` is the same with
|
|
7
|
+
// the variant last - `0` for everyone was Pawn's, and the object said what
|
|
8
|
+
// the variant says. Once no call of the free print is left, its import goes. A player's id is read off his Player:
|
|
9
|
+
// `print(player.id, ...)` is `player.print(...)`; another number is listed.
|
|
10
|
+
// - `removeAllItems(true)` is `removeAllItems({ suit: true })`: a bare
|
|
11
|
+
// boolean nobody reads.
|
|
12
|
+
// - `Storage.get` gives `undefined` for a key that is not there, as `Map.get`
|
|
13
|
+
// does: a comparison of its value with `null` compares with `undefined`,
|
|
14
|
+
// and a `| null` it was annotated with is `| undefined`.
|
|
15
|
+
// - `give` takes any text: the cast to `ItemName` goes, and its import when
|
|
16
|
+
// nothing else names the type.
|
|
17
|
+
// - A test's `server.vault(name)` is `server.storage(name)`: a Storage is not nVault.
|
|
18
|
+
// - An entity's `viewModel` and `weaponModel` are the model's file, text: a
|
|
19
|
+
// `0` written or compared is `""`, another number is listed.
|
|
20
|
+
// - `cmd()` and `cmdWide()` take no `info`: the fourth argument goes.
|
|
21
|
+
// - Listed: a listener of the server events `pause`, `unpause` and `modules`,
|
|
22
|
+
// which are gone; reapi's `DisableHookChain` and `EnableHookChain`, where a
|
|
23
|
+
// `hook()` handle goes to `unhook`; `cells("text")` given to a call, where
|
|
24
|
+
// a raw native takes a const string as it is.
|
|
25
|
+
import ts from 'typescript';
|
|
26
|
+
import { applyEdits } from './upgrade';
|
|
27
|
+
|
|
28
|
+
/** The words a file must have for a pass to look at it. */
|
|
29
|
+
const CALL_WORDS = /\bprint\s*\(|\bremoveAllItems\s*\(|\bStorage\b|\bgive\s*\(|\.vault\s*\(|\.(?:view|weapon)Model\b|\bcmd(?:Wide)?\s*\(|HookChain\s*\(|\bcells\s*\(|["'](?:pause|unpause|modules)["']/;
|
|
30
|
+
|
|
31
|
+
/** The server events AMX Mod X raises for a Pawn plugin about itself alone. */
|
|
32
|
+
const GONE_EVENTS = new Set(['pause', 'unpause', 'modules']);
|
|
33
|
+
|
|
34
|
+
/** The fields that held a number of the engine's strings and hold the model's file. */
|
|
35
|
+
const MODELS = new Set(['viewModel', 'weaponModel']);
|
|
36
|
+
|
|
37
|
+
/** What `x.print(` takes as it is; anything else is wrapped in parens. */
|
|
38
|
+
const PLAIN = new Set([
|
|
39
|
+
ts.SyntaxKind.Identifier,
|
|
40
|
+
ts.SyntaxKind.PropertyAccessExpression,
|
|
41
|
+
ts.SyntaxKind.ElementAccessExpression,
|
|
42
|
+
ts.SyntaxKind.CallExpression,
|
|
43
|
+
ts.SyntaxKind.NonNullExpression,
|
|
44
|
+
ts.SyntaxKind.ParenthesizedExpression,
|
|
45
|
+
ts.SyntaxKind.ThisKeyword,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const NULL_COMPARISONS = new Set([
|
|
49
|
+
ts.SyntaxKind.EqualsEqualsToken,
|
|
50
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
51
|
+
ts.SyntaxKind.ExclamationEqualsToken,
|
|
52
|
+
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** A file's calls brought to their new shapes; what upgrade cannot rewrite safely is listed. */
|
|
56
|
+
export function upgradeCalls(file: string, text: string): { text: string; changes: Change[]; left: Left[] } {
|
|
57
|
+
if (!CALL_WORDS.test(text)) return { text, changes: [], left: [] };
|
|
58
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
59
|
+
const edits: Edit[] = [];
|
|
60
|
+
const left: Left[] = [];
|
|
61
|
+
const lineOf = (node: ts.Node) => source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
62
|
+
const textOf = (node: ts.Node) => node.getText(source);
|
|
63
|
+
const replace = (node: ts.Node, to: string) => edits.push({ start: node.getStart(source), end: node.getEnd(), with: to, from: textOf(node) });
|
|
64
|
+
// The names the rewrites took out of the code - the free print, the casts
|
|
65
|
+
// give no longer needs - and how many times each.
|
|
66
|
+
const gone = new Map<string, number>();
|
|
67
|
+
const went = (name: string) => gone.set(name, (gone.get(name) ?? 0) + 1);
|
|
68
|
+
|
|
69
|
+
// The names a Storage is held by, and the names that hold what its get gave.
|
|
70
|
+
const storages = new Set<string>();
|
|
71
|
+
const stored = new Set<string>();
|
|
72
|
+
const isStorageGet = (node: ts.Node): boolean => {
|
|
73
|
+
const call = ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node) ? node.expression : node;
|
|
74
|
+
return ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'get'
|
|
75
|
+
&& ts.isIdentifier(call.expression.expression) && storages.has(call.expression.expression.text);
|
|
76
|
+
};
|
|
77
|
+
const collect = (node: ts.Node) => {
|
|
78
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
79
|
+
const init = node.initializer;
|
|
80
|
+
if (ts.isNewExpression(init) && ts.isIdentifier(init.expression) && init.expression.text === 'Storage') storages.add(node.name.text);
|
|
81
|
+
}
|
|
82
|
+
ts.forEachChild(node, collect);
|
|
83
|
+
};
|
|
84
|
+
collect(source);
|
|
85
|
+
const collectStored = (node: ts.Node) => {
|
|
86
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isStorageGet(node.initializer)) {
|
|
87
|
+
stored.add(node.name.text);
|
|
88
|
+
if (node.type && /\|\s*null\b/.test(textOf(node.type))) replace(node.type, textOf(node.type).replace(/\|\s*null\b/, '| undefined'));
|
|
89
|
+
}
|
|
90
|
+
ts.forEachChild(node, collectStored);
|
|
91
|
+
};
|
|
92
|
+
collectStored(source);
|
|
93
|
+
|
|
94
|
+
const visit = (node: ts.Node) => {
|
|
95
|
+
ts.forEachChild(node, visit);
|
|
96
|
+
|
|
97
|
+
// `player.viewModel = 0`, `weapon.weaponModel == 0`
|
|
98
|
+
if (ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && MODELS.has(node.left.name.text) && ts.isNumericLiteral(node.right)) {
|
|
99
|
+
if (node.right.text === '0') replace(node.right, '""');
|
|
100
|
+
else left.push({ file, line: lineOf(node), why: `${node.left.name.text} is the model's file now, text: "models/v_knife.mdl"` });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// `x == null`, where x is a Storage's get or a name that holds one.
|
|
105
|
+
if (ts.isBinaryExpression(node) && NULL_COMPARISONS.has(node.operatorToken.kind)) {
|
|
106
|
+
const sides = [node.left, node.right];
|
|
107
|
+
const nullSide = sides.find(side => side.kind === ts.SyntaxKind.NullKeyword);
|
|
108
|
+
const other = sides.find(side => side !== nullSide);
|
|
109
|
+
if (nullSide && other && (isStorageGet(other) || (ts.isIdentifier(other) && stored.has(other.text)))) replace(nullSide, 'undefined');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!ts.isCallExpression(node)) return;
|
|
113
|
+
const callee = node.expression;
|
|
114
|
+
const args = node.arguments;
|
|
115
|
+
|
|
116
|
+
// cmd(name, handler, flag, info) and cmdWide - no info.
|
|
117
|
+
if (ts.isIdentifier(callee) && (callee.text === 'cmd' || callee.text === 'cmdWide') && args.length === 4) {
|
|
118
|
+
edits.push({ start: args[2].getEnd(), end: args[3].getEnd(), with: '', from: text.slice(args[2].getEnd(), args[3].getEnd()) });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (ts.isIdentifier(callee) && (callee.text === 'DisableHookChain' || callee.text === 'EnableHookChain')) {
|
|
122
|
+
left.push({ file, line: lineOf(node), why: 'a hook() handle is the module\'s: take the hook off with unhook(handle), and hook() again to put it back' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (ts.isIdentifier(callee) && callee.text === 'cells' && args.length === 1 && ts.isStringLiteral(args[0]) && ts.isCallExpression(node.parent)) {
|
|
126
|
+
left.push({ file, line: lineOf(node), why: `a raw native takes a const string as it is: ${textOf(args[0])}, not ${textOf(node)}` });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (ts.isPropertyAccessExpression(callee) && /^(?:add|remove)EventListener$/.test(callee.name.text) && args[0] && ts.isStringLiteral(args[0]) && GONE_EVENTS.has(args[0].text)) {
|
|
130
|
+
left.push({ file, line: lineOf(node), why: `the server event "${args[0].text}" is gone: AMX Mod X raises it for a Pawn plugin about itself alone - remove the listener` });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// print(player, ...), print(0, ...) and print({ id, variant }, text)
|
|
135
|
+
// Only the callee and the first argument are rewritten, so a change
|
|
136
|
+
// inside the text (a Storage's get) is kept.
|
|
137
|
+
if (ts.isIdentifier(callee) && callee.text === 'print' && args.length >= 2) {
|
|
138
|
+
let to = args[0];
|
|
139
|
+
let place = '';
|
|
140
|
+
const head = (to: string) => edits.push({ start: callee.getStart(source), end: args[1].getStart(source), with: to, from: text.slice(callee.getStart(source), args[1].getStart(source)) });
|
|
141
|
+
if (ts.isObjectLiteralExpression(to)) {
|
|
142
|
+
const { properties } = to;
|
|
143
|
+
const field = (name: string) => properties.find((p): p is ts.PropertyAssignment => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name);
|
|
144
|
+
const id = field('id');
|
|
145
|
+
const variant = field('variant');
|
|
146
|
+
if (!id || args.length > 2 || properties.some(p => p !== id && p !== variant)) {
|
|
147
|
+
left.push({ file, line: lineOf(node), why: 'print takes a player now, and the place as its second argument: player.print(text, "center"); everyone is server.print(text, "center")' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (variant) place = `, ${textOf(variant.initializer)}`;
|
|
151
|
+
to = id.initializer;
|
|
152
|
+
}
|
|
153
|
+
// A player's id names the player it is read off.
|
|
154
|
+
if (ts.isPropertyAccessExpression(to) && to.name.text === 'id') to = to.expression;
|
|
155
|
+
if (ts.isNumericLiteral(to) && to.text !== '0') {
|
|
156
|
+
left.push({ file, line: lineOf(node), why: 'print takes a player now: `player.print(text)`' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (ts.isNumericLiteral(to)) head('server.print(');
|
|
161
|
+
else head(`${PLAIN.has(to.kind) ? textOf(to) : `(${textOf(to)})`}.print(`);
|
|
162
|
+
went('print');
|
|
163
|
+
if (place) edits.push({ start: args[1].getEnd(), end: args[1].getEnd(), with: place, from: '' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!ts.isPropertyAccessExpression(callee)) return;
|
|
167
|
+
|
|
168
|
+
// removeAllItems(true) - the suit too.
|
|
169
|
+
if (callee.name.text === 'removeAllItems' && args.length === 1) {
|
|
170
|
+
const suit = args[0];
|
|
171
|
+
if (suit.kind === ts.SyntaxKind.FalseKeyword) replace(node, `${textOf(callee)}()`);
|
|
172
|
+
else if (!ts.isObjectLiteralExpression(suit)) replace(suit, suit.kind === ts.SyntaxKind.TrueKeyword ? '{ suit: true }' : `{ suit: ${textOf(suit)} }`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// A test's server.vault(name)
|
|
177
|
+
if (callee.name.text === 'vault' && ts.isIdentifier(callee.expression) && callee.expression.text === 'server') {
|
|
178
|
+
replace(callee.name, 'storage');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// give(<ItemName>name), give(name as ItemName)
|
|
183
|
+
if (callee.name.text === 'give' && args.length >= 1) {
|
|
184
|
+
const item = args[0];
|
|
185
|
+
const cast = ts.isTypeAssertionExpression(item) || ts.isAsExpression(item) ? item : undefined;
|
|
186
|
+
if (cast && /^(?:ItemName|WeaponName)$/.test(textOf(cast.type))) {
|
|
187
|
+
replace(item, textOf(cast.expression));
|
|
188
|
+
went(textOf(cast.type));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
visit(source);
|
|
193
|
+
for (const [name, count] of gone) dropImport(name, count);
|
|
194
|
+
return { ...applyEdits(file, text, source, edits), left };
|
|
195
|
+
|
|
196
|
+
/** Where the line at `at` ends, its line break included. */
|
|
197
|
+
function lineEnd(at: number) {
|
|
198
|
+
return text[at] === '\r' ? at + 2 : text[at] === '\n' ? at + 1 : at;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The core's import of a name the file used only where the rewrites took it out. */
|
|
202
|
+
function dropImport(name: string, count: number) {
|
|
203
|
+
let named = 0;
|
|
204
|
+
const countNames = (node: ts.Node) => {
|
|
205
|
+
if (ts.isIdentifier(node) && node.text === name && !ts.isImportSpecifier(node.parent)) named++;
|
|
206
|
+
ts.forEachChild(node, countNames);
|
|
207
|
+
};
|
|
208
|
+
countNames(source);
|
|
209
|
+
if (named > count) return;
|
|
210
|
+
for (const statement of source.statements) {
|
|
211
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || !/^(?:@amxts\/core|~\/)/.test(statement.moduleSpecifier.text)) continue;
|
|
212
|
+
const bindings = statement.importClause?.namedBindings;
|
|
213
|
+
if (!bindings || !ts.isNamedImports(bindings)) continue;
|
|
214
|
+
const at = bindings.elements.findIndex(element => element.name.text === name);
|
|
215
|
+
if (at < 0) continue;
|
|
216
|
+
const { elements } = bindings;
|
|
217
|
+
const alone = elements.length === 1 && !statement.importClause!.name;
|
|
218
|
+
const start = alone ? statement.getStart(source) : at > 0 ? elements[at - 1].getEnd() : elements[at].getStart(source);
|
|
219
|
+
let end = alone ? lineEnd(statement.getEnd()) : at > 0 ? elements[at].getEnd() : elements[at + 1].getStart(source);
|
|
220
|
+
// A blank line after it goes too where it would be the first, or a second one.
|
|
221
|
+
if (alone && text[end] && lineEnd(end) > end && text.slice(end, lineEnd(end)).trim() === '' && /^\s*$|\n[\t\r ]*\n[\t ]*$/.test(text.slice(0, start))) end = lineEnd(end);
|
|
222
|
+
edits.push({ start, end, with: '', from: text.slice(start, end).trim() });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
package/scripts/upgrade-names.ts
CHANGED
|
@@ -233,7 +233,7 @@ export const COMMON = new Set(['menu', 'weapon', 'memory', 'conditions', 'activi
|
|
|
233
233
|
/**
|
|
234
234
|
* Server events by their old names - the Pawn forward's and the short one
|
|
235
235
|
* before it - each by the name in the author's words
|
|
236
|
-
* (scripts/generate-
|
|
236
|
+
* (scripts/generate-image.ts's EVENT_NAMES).
|
|
237
237
|
*/
|
|
238
238
|
export const SERVER_EVENTS: Record<string, string> = {
|
|
239
239
|
plugin_init: 'init',
|
|
@@ -241,10 +241,7 @@ export const SERVER_EVENTS: Record<string, string> = {
|
|
|
241
241
|
cfg: 'pluginsLoaded',
|
|
242
242
|
plugin_cfg: 'pluginsLoaded',
|
|
243
243
|
plugin_end: 'end',
|
|
244
|
-
plugin_pause: 'pause',
|
|
245
|
-
plugin_unpause: 'unpause',
|
|
246
244
|
plugin_log: 'log',
|
|
247
|
-
plugin_modules: 'modules',
|
|
248
245
|
changelevel: 'changeLevel',
|
|
249
246
|
server_changelevel: 'changeLevel',
|
|
250
247
|
server_frame: 'frame',
|