@amxts/core 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +5 -5
  2. package/README.ru.md +5 -5
  3. package/as/amxts.d.ts +13 -3
  4. package/as/entities.ts +571 -501
  5. package/as/events.ts +3323 -557
  6. package/as/facade.ts +3079 -718
  7. package/as/fetch.ts +2 -2
  8. package/as/fs.ts +352 -59
  9. package/as/hlds.ts +121 -120
  10. package/as/hooks.ts +10585 -5177
  11. package/as/lib/check.ts +3 -2
  12. package/as/natives.ts +856 -1272
  13. package/as/promise.ts +37 -27
  14. package/as/vector.ts +94 -14
  15. package/includes/order.txt +17 -19
  16. package/node_modules/assemblyscript/std/assembly/index.d.ts +1 -1
  17. package/package.json +9 -6
  18. package/runtime/deps/assemblyscript/dist/asc.js +136 -42
  19. package/runtime/deps/assemblyscript/dist/assemblyscript.js +141 -141
  20. package/runtime/licenses/README.md +9 -7
  21. package/runtime/licenses/ReGameDLL-LICENSE +21 -0
  22. package/runtime/licenses/ReHLDS-LICENSE +21 -0
  23. package/runtime/licenses/zlib-LICENSE +22 -0
  24. package/runtime/natives.txt +618 -523
  25. package/runtime/patches/README.md +87 -1
  26. package/runtime/patches/assemblyscript-0.28.20-amxts.patch +4838 -416
  27. package/runtime/patches/wamr-2.4.5-amxts.patch +417 -5
  28. package/scripts/auto-imports.ts +1 -1
  29. package/scripts/build-identity.ts +102 -65
  30. package/scripts/build-wasm.ts +17 -13
  31. package/scripts/check.ts +4 -1
  32. package/scripts/client-messages.ts +73 -1
  33. package/scripts/compile-cache.ts +11 -8
  34. package/scripts/compile-pool.ts +6 -3
  35. package/scripts/compile-worker.ts +23 -23
  36. package/scripts/compile.ts +142 -19
  37. package/scripts/docs/as/amxts.ts +10 -4
  38. package/scripts/docs/as/facade.ts +1779 -275
  39. package/scripts/docs/as/fetch.ts +2 -2
  40. package/scripts/docs/as/fs.ts +190 -6
  41. package/scripts/docs/as/vector.ts +81 -0
  42. package/scripts/docs/entities.ts +12 -0
  43. package/scripts/docs/events.ts +49 -13
  44. package/scripts/docs/game.ts +6 -0
  45. package/scripts/docs/messages.ts +10 -1
  46. package/scripts/hlds-events.ts +11 -11
  47. package/scripts/includes.ts +3 -4
  48. package/scripts/leaf-natives.ts +84 -0
  49. package/scripts/player-fields.ts +1 -1
  50. package/scripts/plugin-natives.ts +1 -1
  51. package/scripts/prebuilt.ts +42 -11
  52. package/scripts/project.ts +63 -4
  53. package/scripts/reapi-events.ts +9 -9
  54. package/scripts/server-env.ts +145 -0
  55. package/scripts/shared-modules.ts +23 -7
  56. package/scripts/system.ts +1 -1
  57. package/scripts/tracked-fs.ts +11 -2
  58. package/scripts/typed-commands.ts +153 -15
  59. package/scripts/upgrade-calls.ts +225 -0
  60. package/scripts/upgrade-names.ts +1 -4
  61. package/scripts/upgrade.ts +47 -34
  62. package/src/cli-api.mjs +3 -2
  63. package/src/system.mjs +21 -11
  64. package/src/testing/compile.ts +31 -8
  65. package/src/testing/coroutines.ts +0 -10
  66. package/src/testing/memory.ts +89 -2
  67. package/src/testing/natives.ts +67 -30
  68. package/src/testing/server.ts +1094 -155
  69. package/src/testing/tables.ts +23 -6
  70. package/src/typecheck.mjs +110 -0
  71. package/runtime/licenses/Bun-LICENSE.md +0 -84
@@ -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, hashOf([...names].sort().join('\n')));
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 hashOf(fs.readdirSync(path).sort().join('\n'));
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
- return [
183
- `class ${type}${shape ? ` implements ${shape}` : ''} {`,
184
- ...(player ? ['\tplayer!: __AmxtsPlayer;'] : []),
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
- '\t\thandler(args);',
196
- `\t}${player ? ', options' : ''});`,
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
- const usage = usageOf(usageNode, !!node.typeArguments?.length, file);
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
- functions.push(commandCode(functions.length, method, argumentsOf(node, usage, shapes, at, method === 'addCommand'), shape));
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
+ }
@@ -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-host.ts's EVENT_NAMES).
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',