@brew-lang/sdk 0.0.0-stage → 0.3.0-alpha

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.
@@ -0,0 +1,2025 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.jsdocTagText = jsdocTagText;
37
+ exports.transpile = transpile;
38
+ /**
39
+ * Brew Compiler — TypeScript to Skript
40
+ *
41
+ * Parses TypeScript with the official TypeScript parser by using the
42
+ * `ts.createSourceFile` API function, and then emits Skript code by walking
43
+ * the AST.
44
+ *
45
+ * The transpiler keeps a registry of declared types (interfaces / classes /
46
+ * functions) and a stack of lexical scopes, so that every identifier is
47
+ * resolved to a unique Skript variable key and a compile-time type. A type
48
+ * dispatcher then picks the right Skript representation per type:
49
+ *
50
+ * - types deriving from `NBTCompound` -> SkBee `nbt compound` values,
51
+ * indexed with `tag "name" of {_x}` syntax
52
+ * - types deriving from `JavaObject` -> skript-Reflect syntax,
53
+ * i.e. `{_player}.getLocation()`
54
+ * - everything else deriving from
55
+ * `object` (including `object` and object literals) -> Skript list
56
+ * variables, indexed with `{_x}::key`
57
+ */
58
+ const fs = __importStar(require("fs"));
59
+ const path = __importStar(require("path"));
60
+ const ts = __importStar(require("typescript"));
61
+ // ---------------------------------------------------------------------------
62
+ // Type mapping: TS types -> Skript types
63
+ // ---------------------------------------------------------------------------
64
+ const TYPE_MAP = {
65
+ // TS primitives and `object` keep their TS (lowercase) spelling
66
+ number: "number",
67
+ string: "text",
68
+ boolean: "boolean",
69
+ // `object` values cross function boundaries as an NBT compound: a single
70
+ // value (keyed lists lose their indices across calls, SkriptLang#811)
71
+ // carried by local variables only — global variables would not be usable
72
+ // inside async sections.
73
+ object: "nbt compound",
74
+ any: "object",
75
+ // `Function` holds a lambda (section) value; Skript's catch-all type
76
+ Function: "object",
77
+ // Skript domain types are PascalCase in the TS API...
78
+ Player: "player",
79
+ World: "world",
80
+ ItemType: "item type",
81
+ Location: "location",
82
+ Vector: "vector",
83
+ Timespan: "timespan",
84
+ // ...with lowercase aliases still accepted
85
+ player: "player",
86
+ world: "world",
87
+ itemtype: "item type",
88
+ location: "location",
89
+ vector: "vector",
90
+ timespan: "timespan",
91
+ };
92
+ /** Base types that decide a type's Skript representation. */
93
+ const NBT_BASES = new Set(["nbtcompound", "compound"]);
94
+ // "object": mirrored classes extend the java.lang.Object interface from
95
+ // the @brew-types language packs
96
+ const JAVA_BASES = new Set(["javaobject", "java", "object"]);
97
+ /** Last dotted segment of a type name: org.bukkit.entity.Player -> Player
98
+ * (the registry keys declared types by their simple class name, so qualified
99
+ * Java-path references resolve to them). */
100
+ function lastSegment(typeName) {
101
+ const i = typeName.lastIndexOf(".");
102
+ return i === -1 ? typeName : typeName.slice(i + 1);
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // Type registry: interfaces, classes and function signatures in the file
106
+ // ---------------------------------------------------------------------------
107
+ class TypeRegistry {
108
+ types = new Map();
109
+ /** Function name -> signature (param type texts + return type text). */
110
+ signatures = new Map();
111
+ /**
112
+ * `@ffi <pattern>` over an ambient `declare const X: T;`: the pattern
113
+ * substitutes for every use of X, like a macro. Declares without a
114
+ * pattern stay type-level.
115
+ */
116
+ macros = new Map();
117
+ /**
118
+ * `@ffi <pattern>` over an interface property: the pattern emits the
119
+ * property's native syntax instead of the default access. %this% is the
120
+ * receiver, %key% the property name, and %value% (setter form) the
121
+ * assigned value; %1%/%2%/%3% are aliases.
122
+ */
123
+ memberPatterns = new Map();
124
+ /**
125
+ * `@macro` functions: TypeScript macros, C/C++ #define-style — never
126
+ * emitted as functions; every call site expands the macro's body inline
127
+ * with its arguments bound to locals.
128
+ */
129
+ macroDecls = new Map();
130
+ /** Java class locations of the types used as Java objects in the script. */
131
+ usedClasslocs = new Set();
132
+ /** Collect declarations from a source file (case is preserved). */
133
+ collect(sf) {
134
+ for (const stmt of sf.statements) {
135
+ if (ts.isInterfaceDeclaration(stmt) || ts.isClassDeclaration(stmt)) {
136
+ const name = stmt.name?.text;
137
+ if (!name)
138
+ continue;
139
+ const locTag = ts.getJSDocTags(stmt).find((t) => t.tagName.text === "classloc");
140
+ const classloc = locTag && typeof locTag.comment === "string"
141
+ ? locTag.comment.trim() : undefined;
142
+ const info = {
143
+ bases: (stmt.heritageClauses ?? []).flatMap((c) => c.types.map((t) => t.expression.getText())),
144
+ members: new Map(),
145
+ hasMethods: false,
146
+ classloc,
147
+ };
148
+ for (const m of stmt.members) {
149
+ if (ts.isPropertySignature(m) || ts.isPropertyDeclaration(m)) {
150
+ if (ts.isIdentifier(m.name)) {
151
+ info.members.set(m.name.text, m.type?.getText() ?? "any");
152
+ // an @ffi pattern over the property: native get/set syntax
153
+ const ptag = jsdocTagText(ts.getJSDocTags(m), ["ffi", "skript"]);
154
+ if (ptag) {
155
+ if (!this.memberPatterns.has(name))
156
+ this.memberPatterns.set(name, new Map());
157
+ this.memberPatterns.get(name).set(m.name.text, ptag);
158
+ }
159
+ }
160
+ }
161
+ else if (ts.isMethodSignature(m) || ts.isMethodDeclaration(m) ||
162
+ ts.isConstructSignatureDeclaration(m) || ts.isIndexSignatureDeclaration(m) ||
163
+ ts.isGetAccessorDeclaration(m) || ts.isSetAccessorDeclaration(m)) {
164
+ info.hasMethods = true;
165
+ if (ts.isGetAccessorDeclaration(m) && ts.isIdentifier(m.name)) {
166
+ info.members.set(m.name.text, m.type?.getText() ?? "any");
167
+ }
168
+ else if (ts.isSetAccessorDeclaration(m) && ts.isIdentifier(m.name)) {
169
+ info.members.set(m.name.text, m.parameters[0].type?.getText() ?? "any");
170
+ }
171
+ else if ((ts.isMethodSignature(m) || ts.isMethodDeclaration(m)) && ts.isIdentifier(m.name)) {
172
+ // class methods (declare class members, incl. static ones) type
173
+ // their accesses too, e.g. the result of a static call
174
+ info.members.set(m.name.text, m.type?.getText() ?? "any");
175
+ }
176
+ }
177
+ }
178
+ this.types.set(name, info);
179
+ }
180
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
181
+ // `/** @ffi <pattern> */` maps an ambient stub onto a native
182
+ // Skript syntax instead of a function call: %1%, %2%, ... in the
183
+ // pattern are the call arguments, e.g.
184
+ // /** @ffi broadcast %1% */ declare function broadcast(msg: string)
185
+ // turns `broadcast(m)` into the effect `broadcast {_m}`.
186
+ const pattern = jsdocTagText(ts.getJSDocTags(stmt), ["ffi", "skript"]);
187
+ // a `@macro` function is never emitted: its body expands inline at
188
+ // every call site. `@inline` generates the same macro from the
189
+ // function, but the function itself IS emitted — so value-position
190
+ // calls of statement-bodied inlines fall back to a real call.
191
+ if (stmt.body &&
192
+ (jsdocTagText(ts.getJSDocTags(stmt), ["macro"]) !== undefined ||
193
+ jsdocTagText(ts.getJSDocTags(stmt), ["inline"]) !== undefined)) {
194
+ this.macroDecls.set(stmt.name.text, stmt);
195
+ }
196
+ this.signatures.set(stmt.name.text, {
197
+ params: stmt.parameters.map((p) => p.type?.getText() ?? ""),
198
+ ret: stmt.type?.getText() ?? "",
199
+ pattern,
200
+ });
201
+ }
202
+ // Ambient `declare const X: T;` with an @ffi tag: a const macro whose
203
+ // pattern substitutes for every use of X.
204
+ if (ts.isVariableStatement(stmt)) {
205
+ for (const d of stmt.declarationList.declarations) {
206
+ if (!ts.isIdentifier(d.name))
207
+ continue;
208
+ const ambient = (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0;
209
+ if (!ambient)
210
+ continue;
211
+ const tag = jsdocTagText(ts.getJSDocTags(stmt), ["ffi", "skript"]);
212
+ if (tag) {
213
+ this.macros.set(d.name.text, {
214
+ pattern: tag,
215
+ type: d.type?.getText() ?? "",
216
+ });
217
+ }
218
+ }
219
+ }
220
+ // Type aliases: resolve to the aliased text.
221
+ if (ts.isTypeAliasDeclaration(stmt)) {
222
+ this.types.set(stmt.name.text, {
223
+ bases: [],
224
+ members: new Map(),
225
+ hasMethods: false,
226
+ alias: stmt.type.getText(),
227
+ });
228
+ }
229
+ }
230
+ }
231
+ /**
232
+ * Resolve relative imports and register the declarations of the imported
233
+ * modules (interfaces, type aliases, and declare-function signatures,
234
+ * including their @ffi patterns). This is how the TS API modules in
235
+ * src/api are consumed: scripts `import { broadcast } from ".../api"` and
236
+ * the transpiler picks up the mappings. Imported declarations override
237
+ * same-named local ones. Imported files are only PARSED, never type-checked.
238
+ */
239
+ collectImports(sf, importBase, visited = new Set()) {
240
+ for (const stmt of sf.statements) {
241
+ // follow both imports and index-style re-exports (`export * from "./x"`)
242
+ if (!ts.isImportDeclaration(stmt) &&
243
+ !(ts.isExportDeclaration(stmt) && stmt.moduleSpecifier))
244
+ continue;
245
+ const spec = stmt.moduleSpecifier.text;
246
+ if (!spec.startsWith(".")) {
247
+ // @brew-types/* language packs resolve through node_modules (walking
248
+ // up from the importing file, like node); other bare specifiers are
249
+ // left to the editor/runtime and not followed
250
+ if (spec.startsWith("@brew-types/")) {
251
+ const pkgFile = TypeRegistry.resolveBrewSpec(spec, importBase);
252
+ if (pkgFile && !visited.has(pkgFile)) {
253
+ visited.add(pkgFile);
254
+ const sub = ts.createSourceFile(pkgFile, fs.readFileSync(pkgFile, "utf8"), ts.ScriptTarget.Latest, true);
255
+ this.collect(sub);
256
+ this.collectImports(sub, path.dirname(pkgFile), visited);
257
+ }
258
+ }
259
+ continue;
260
+ }
261
+ const target = spec.replace(/\.js$/, "");
262
+ const candidates = [
263
+ path.join(importBase, target + ".ts"),
264
+ path.join(importBase, target, "index.ts"),
265
+ ];
266
+ const file = candidates.find((c) => fs.existsSync(c));
267
+ if (!file || visited.has(file))
268
+ continue;
269
+ visited.add(file);
270
+ const sub = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
271
+ this.collect(sub);
272
+ this.collectImports(sub, path.dirname(file), visited);
273
+ }
274
+ }
275
+ /** Resolve an @brew-types/... specifier to a file inside a node_modules
276
+ * dir at or above fromDir (the file itself or its index, .ts or .d.ts). */
277
+ static resolveBrewSpec(spec, fromDir) {
278
+ let dir = path.resolve(fromDir);
279
+ for (;;) {
280
+ const base = path.join(dir, "node_modules", spec);
281
+ for (const cand of [
282
+ base + ".ts", base + ".d.ts",
283
+ path.join(base, "index.ts"), path.join(base, "index.d.ts"),
284
+ ]) {
285
+ if (fs.existsSync(cand))
286
+ return cand;
287
+ }
288
+ const parent = path.dirname(dir);
289
+ if (parent === dir)
290
+ return null;
291
+ dir = parent;
292
+ }
293
+ }
294
+ /** Resolve a type name to its Skript representation category. */
295
+ category(typeText, depth = 0) {
296
+ // qualified names (org.bukkit.entity.Player) bind the declared class
297
+ let raw = lastSegment(typeText.trim());
298
+ if (!raw || raw === "void" || raw === "undefined" || raw === "null")
299
+ return "plain";
300
+ // async functions are annotated Promise<T>; the returned value is T
301
+ const promise = /^Promise<(.+)>$/.exec(raw);
302
+ if (promise)
303
+ return this.category(promise[1], depth + 1);
304
+ raw = lastSegment(raw); // strip any qualifier the unwrap revealed
305
+ if (raw.endsWith("[]"))
306
+ return "plain"; // Skript plural/list values
307
+ const lower = raw.toLowerCase();
308
+ if (["number", "string", "boolean", "text", "bigint", "symbol"].includes(lower))
309
+ return "plain";
310
+ if (raw === "object" || lower === "any")
311
+ return "list";
312
+ const info = this.types.get(raw);
313
+ if (info) {
314
+ if (info.alias !== undefined)
315
+ return this.category(info.alias, depth + 1);
316
+ if (NBT_BASES.has(lower))
317
+ return "nbt";
318
+ if (JAVA_BASES.has(lower))
319
+ return "java";
320
+ if (info.hasMethods && depth < 16)
321
+ return "java";
322
+ // Declared data types default to list representation unless a base
323
+ // says otherwise.
324
+ for (const base of info.bases) {
325
+ if (NBT_BASES.has(base.toLowerCase()))
326
+ return "nbt";
327
+ if (JAVA_BASES.has(base.toLowerCase()))
328
+ return "java";
329
+ if (this.types.has(base)) {
330
+ const cat = this.category(base, depth + 1);
331
+ if (cat !== "list" || this.types.get(base).hasMethods)
332
+ return cat;
333
+ }
334
+ }
335
+ return "list";
336
+ }
337
+ if (NBT_BASES.has(lower))
338
+ return "nbt";
339
+ if (JAVA_BASES.has(lower))
340
+ return "java";
341
+ // `Function`-typed values hold sections: opaque single values
342
+ if (lower === "function")
343
+ return "plain";
344
+ // Unknown types (e.g. native Skript types like `player`) index like lists.
345
+ return "list";
346
+ }
347
+ /** Type text of a named member of a declared type, if known. */
348
+ memberType(typeText, member) {
349
+ const info = this.types.get(lastSegment(typeText.trim()));
350
+ if (!info || !info.members.has(member))
351
+ return null;
352
+ return info.members.get(member);
353
+ }
354
+ /**
355
+ * Mark a type as used as a Java object, collecting its @classloc for the
356
+ * skript-Reflect `import:` section.
357
+ */
358
+ markJava(typeName) {
359
+ const info = this.types.get(lastSegment(typeName.trim()));
360
+ if (info?.classloc)
361
+ this.usedClasslocs.add(info.classloc);
362
+ }
363
+ }
364
+ class Scope {
365
+ vars = new Map();
366
+ }
367
+ class ScopeStack {
368
+ obfuscate;
369
+ scopes = [new Scope()];
370
+ usedKeys = new Set();
371
+ counter = 0;
372
+ /** When true, locals get short generated keys ({_o1}, {_o2}, ...) instead
373
+ * of their source names. Safe: Skript locals are call-scoped, so keys
374
+ * can be reused across functions. */
375
+ constructor(obfuscate = false) {
376
+ this.obfuscate = obfuscate;
377
+ }
378
+ push() { this.scopes.push(new Scope()); }
379
+ pop() { this.scopes.pop(); }
380
+ /** Next short local key for obfuscated builds. */
381
+ nextKey() {
382
+ return `_o${(++this.counter).toString(36)}`;
383
+ }
384
+ /** Bind a name in the innermost scope under a unique Skript key. */
385
+ declare(name, typeText, category, isList = false) {
386
+ const key = this.obfuscate ? this.nextKey() : this.uniqueKey(`_${name}`);
387
+ return this.bind(name, key, typeText, category, isList);
388
+ }
389
+ uniqueKey(base) {
390
+ let key = base;
391
+ let n = 2;
392
+ while (this.usedKeys.has(key))
393
+ key = `${base}_${n++}`;
394
+ return key;
395
+ }
396
+ /** Bind a name in the innermost scope under the given key. */
397
+ bind(name, key, typeText, category, isList = false) {
398
+ this.usedKeys.add(key);
399
+ const info = { name, key, typeText, category, isList };
400
+ this.scopes[this.scopes.length - 1].vars.set(name, info);
401
+ return info;
402
+ }
403
+ /** Bind a name that keeps an externally chosen key (function parameters). */
404
+ declareWithKey(name, key, typeText, category, isList = false) {
405
+ return this.bind(name, key, typeText, category, isList);
406
+ }
407
+ /**
408
+ * Bind an undeclared name as a globalThis property: like JavaScript,
409
+ * assigning to a name no `let`/`const` declared creates a global —
410
+ * {globalThis::<name>} in Skript. Global names stay readable across
411
+ * triggers, so they are never obfuscated or suffixed.
412
+ */
413
+ declareGlobal(name, typeText, category, isList = false) {
414
+ const info = this.bind(name, `globalThis::${name}`, typeText, category, isList);
415
+ info.global = true;
416
+ return info;
417
+ }
418
+ /** Look a name up from the innermost scope outwards. */
419
+ lookup(name) {
420
+ for (let i = this.scopes.length - 1; i >= 0; i--) {
421
+ const v = this.scopes[i].vars.get(name);
422
+ if (v)
423
+ return v;
424
+ }
425
+ return undefined;
426
+ }
427
+ }
428
+ /**
429
+ * Runtime helper emitted (once) into scripts that read NBT tags. SkBee's
430
+ * typed getters return null when the requested type doesn't match the tag's
431
+ * actual type (NBTApi.getTag), so this reads the tag's runtime type first
432
+ * and uses the matching getter. */
433
+ /**
434
+ * Deterministic per-file id (FNV-1a of the source, base36): the same file
435
+ * always obfuscates to the same names, and two different files never share
436
+ * function names — like ES6 module-scoped symbols.
437
+ */
438
+ function fileKey(source) {
439
+ let h = 0x811c9dc5;
440
+ for (let i = 0; i < source.length; i++) {
441
+ h ^= source.charCodeAt(i);
442
+ h = Math.imul(h, 0x01000193);
443
+ }
444
+ return (h >>> 0).toString(36);
445
+ }
446
+ const TS2SK_TAG_HELPER = [
447
+ "function brew_tag(c: nbt compound, k: text) :: object:",
448
+ "\tset {_t} to tag type of tag \"%{_k}%\" of {_c}",
449
+ "\tif \"%{_t}%\" is \"byte tag\":",
450
+ "\t\treturn byte tag \"%{_k}%\" of {_c}",
451
+ "\telse if \"%{_t}%\" is \"short tag\":",
452
+ "\t\treturn short tag \"%{_k}%\" of {_c}",
453
+ "\telse if \"%{_t}%\" is \"int tag\":",
454
+ "\t\treturn int tag \"%{_k}%\" of {_c}",
455
+ "\telse if \"%{_t}%\" is \"long tag\":",
456
+ "\t\treturn long tag \"%{_k}%\" of {_c}",
457
+ "\telse if \"%{_t}%\" is \"float tag\":",
458
+ "\t\treturn float tag \"%{_k}%\" of {_c}",
459
+ "\telse if \"%{_t}%\" is \"double tag\":",
460
+ "\t\treturn double tag \"%{_k}%\" of {_c}",
461
+ "\telse if \"%{_t}%\" is \"boolean tag\":",
462
+ "\t\treturn boolean tag \"%{_k}%\" of {_c}",
463
+ "\telse if \"%{_t}%\" is \"string tag\":",
464
+ "\t\treturn string tag \"%{_k}%\" of {_c}",
465
+ "\telse if \"%{_t}%\" is \"compound tag\":",
466
+ "\t\treturn compound tag \"%{_k}%\" of {_c}",
467
+ "\tstop",
468
+ "",
469
+ ];
470
+ /**
471
+ * The comment of the first matching JSDoc tag. `@ffi` is the native-syntax
472
+ * annotation (the original name `@skript` still works); `@macro` marks a
473
+ * TypeScript macro (a C/C++ #define-style inline expansion).
474
+ */
475
+ function jsdocTagText(tags, names) {
476
+ for (const t of tags) {
477
+ if (names.includes(t.tagName.text)) {
478
+ // a bare tag (no comment, e.g. `/** @macro */`) is present with ""
479
+ return typeof t.comment === "string" ? t.comment.trim() : "";
480
+ }
481
+ }
482
+ return undefined;
483
+ }
484
+ function skriptType(node, reg) {
485
+ if (!node)
486
+ return "object";
487
+ if (ts.isUnionTypeNode(node)) {
488
+ // `T | undefined` -> keep base type (Skript params are effectively optional)
489
+ const nonNull = node.types.find((t) => t.getText() !== "undefined" && t.getText() !== "null");
490
+ if (nonNull)
491
+ return skriptType(nonNull, reg);
492
+ }
493
+ const raw = node.getText();
494
+ // Arrays map to Skript's plural list types: player[] -> players
495
+ if (raw.endsWith("[]")) {
496
+ const elem = raw.slice(0, -2).toLowerCase();
497
+ if (elem === "object" || elem === "any")
498
+ return "objects";
499
+ return `${skriptTypeNodeText(raw.slice(0, -2), reg)}s`;
500
+ }
501
+ return skriptTypeNodeText(raw, reg);
502
+ }
503
+ function skriptTypeNodeText(raw, reg) {
504
+ // async functions are annotated Promise<T>; the function itself returns T
505
+ raw = raw.replace(/^Promise<(.+)>$/, "$1");
506
+ const mapped = TYPE_MAP[raw] ?? TYPE_MAP[raw.toLowerCase()] ?? raw;
507
+ const cat = reg.category(raw);
508
+ if (cat === "nbt")
509
+ return "nbt compound";
510
+ if (cat === "java") {
511
+ reg.markJava(raw);
512
+ return "javaobject";
513
+ }
514
+ return mapped;
515
+ }
516
+ // ---------------------------------------------------------------------------
517
+ // Transpiler: expression/statement emission with the type dispatcher
518
+ // ---------------------------------------------------------------------------
519
+ class Transpiler {
520
+ lines = [];
521
+ indent = 0;
522
+ /** Statements that must be emitted before the next emitted line. */
523
+ /** Statements that must be emitted before the next emitted line. The
524
+ * indentation is baked in at push time: pending lines can outlive the
525
+ * statement that created them (e.g. at the end of a trigger body) and must
526
+ * not be re-indented at whatever depth they finally flush. */
527
+ pending = [];
528
+ push(line) {
529
+ this.pending.push("\t".repeat(this.indent) + line);
530
+ }
531
+ /** Like push(), but `delta` indentation levels deeper (loop bodies). */
532
+ pushAt(delta, line) {
533
+ this.pending.push("\t".repeat(this.indent + delta) + line);
534
+ }
535
+ reg = new TypeRegistry();
536
+ /** Category of the return type of the function currently being emitted. */
537
+ currentRetCat = "plain";
538
+ /** True while emitting a function declared to return `object`. */
539
+ currentRetIsObject = false;
540
+ /**
541
+ * True while emitting a named function body: Skript functions cannot
542
+ * contain delays, so `await` is a compile error there (triggers and
543
+ * lambda/section bodies can await).
544
+ */
545
+ inFunction = false;
546
+ /**
547
+ * True once the current trigger or section body emitted an async run
548
+ * (`run section ... async`): Skript forbids `return` after a delay, so
549
+ * a later return is a compile error — the result belongs in a global.
550
+ */
551
+ sectionAwaited = false;
552
+ /**
553
+ * True while emitting the top-level `on load:` init trigger: top-level
554
+ * `let`/`const` declarations bind as globalThis globals, so triggers
555
+ * (and other scripts) can read them later.
556
+ */
557
+ topLevelInit = false;
558
+ /** Emits the brew_tag runtime helper when any NBT read occurs. */
559
+ usedTagHelper = false;
560
+ scopes = new ScopeStack(false);
561
+ tempCounter = 0;
562
+ /** Obfuscate local variables and locally-defined function names. */
563
+ obfuscate = false;
564
+ /** Local function renames for obfuscated builds (source name -> output). */
565
+ fnMap = new Map();
566
+ constructor(sf, opts) {
567
+ this.obfuscate = !!opts?.obfuscate;
568
+ this.scopes = new ScopeStack(this.obfuscate);
569
+ this.reg.collect(sf);
570
+ // imported API modules register AFTER the local declarations (they win)
571
+ if (opts?.importBase)
572
+ this.reg.collectImports(sf, opts.importBase);
573
+ }
574
+ // -- emitter helpers ------------------------------------------------------
575
+ emit(line) {
576
+ this.flushPending();
577
+ this.lines.push("\t".repeat(this.indent) + line);
578
+ }
579
+ flushPending() {
580
+ for (const l of this.pending)
581
+ this.lines.push(l);
582
+ this.pending = [];
583
+ }
584
+ body(stmts) {
585
+ for (const s of stmts)
586
+ this.statement(s);
587
+ }
588
+ indented(stmts) {
589
+ this.indent++;
590
+ this.body(stmts);
591
+ this.indent--;
592
+ }
593
+ // -- type helpers ---------------------------------------------------------
594
+ /** Skript variable reference for an identifier bound in scope. */
595
+ ref(v) {
596
+ return v.isList ? `{${v.key}::*}` : `{${v.key}}`;
597
+ }
598
+ /** Static type of an expression, when it can be determined. */
599
+ typeOfExpr(node) {
600
+ if (ts.isIdentifier(node)) {
601
+ const v = this.scopes.lookup(node.text);
602
+ if (v)
603
+ return v.typeText;
604
+ // an ambient @ffi const carries its declared type for dispatch
605
+ return this.reg.macros.get(node.text)?.type ?? "";
606
+ }
607
+ if (ts.isNumericLiteral(node))
608
+ return "number";
609
+ if (ts.isStringLiteral(node))
610
+ return "string";
611
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword)
612
+ return "boolean";
613
+ if (ts.isPropertyAccessExpression(node)) {
614
+ const recExpr = node.expression;
615
+ // a mirrored class used as receiver (static access) contributes its own
616
+ // members to the type of the access
617
+ const rec = ts.isIdentifier(recExpr) && this.staticClassOf(recExpr.text)
618
+ ? recExpr.text
619
+ : this.typeOfExpr(recExpr);
620
+ return this.reg.memberType(rec, node.name.text) ?? "";
621
+ }
622
+ if (ts.isAwaitExpression(node))
623
+ return this.typeOfExpr(node.expression);
624
+ if (ts.isCallExpression(node)) {
625
+ if (ts.isIdentifier(node.expression)) {
626
+ if (this.callableOf(node.expression.text))
627
+ return "";
628
+ return this.reg.signatures.get(node.expression.text)?.ret ?? "";
629
+ }
630
+ if (ts.isPropertyAccessExpression(node.expression)) {
631
+ const recExpr = node.expression.expression;
632
+ // static call on a mirrored class: the receiver's members type it
633
+ const rec = ts.isIdentifier(recExpr) && this.staticClassOf(recExpr.text)
634
+ ? recExpr.text
635
+ : this.typeOfExpr(recExpr);
636
+ return this.reg.memberType(rec, node.expression.name.text) ?? "";
637
+ }
638
+ }
639
+ if (ts.isObjectLiteralExpression(node))
640
+ return "object";
641
+ if (ts.isNewExpression(node))
642
+ return node.expression.getText();
643
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken)
644
+ return "number";
645
+ return "";
646
+ }
647
+ /** Category of an expression's value. */
648
+ categoryOfExpr(node) {
649
+ return this.reg.category(this.typeOfExpr(node));
650
+ }
651
+ /**
652
+ * If `name` refers to a mirrored class/interface with a @classloc and is
653
+ * not shadowed by a local variable or macro, return its registry entry —
654
+ * the receiver of a static call/field access (BukkitAdapter.worldIdentifier(w)).
655
+ */
656
+ staticClassOf(name) {
657
+ if (this.scopes.lookup(name) || this.reg.macros.has(name))
658
+ return undefined;
659
+ const info = this.reg.types.get(name);
660
+ return info?.classloc ? info : undefined;
661
+ }
662
+ /** True for calls to functions declared to return `object` (carrier ids). */
663
+ isObjCall(node) {
664
+ return ts.isCallExpression(node) && ts.isIdentifier(node.expression) &&
665
+ (this.reg.signatures.get(node.expression.text)?.ret ?? "").toLowerCase() === "object";
666
+ }
667
+ /** List form of a variable reference: {_x} -> {_x::*}. */
668
+ listForm(e) {
669
+ return e.replace(/^\{(_\w+|globalThis::\w+)\}$/, "{$1::*}");
670
+ }
671
+ /**
672
+ * The variable a call runs as a section, if the name is callable: a lambda
673
+ * or a `Function`-typed value (e.g. a parameter) holds a section object.
674
+ */
675
+ callableOf(name) {
676
+ const v = this.scopes.lookup(name);
677
+ if (!v)
678
+ return undefined;
679
+ return v.lambda || v.typeText.toLowerCase() === "function" ? v : undefined;
680
+ }
681
+ /**
682
+ * Whether a call on a callable variable runs async: awaited calls always
683
+ * do; lambdas default to their own `async` modifier, `Function` values
684
+ * (whose section came from elsewhere) run sync unless awaited.
685
+ */
686
+ callRunsAsync(v, awaited) {
687
+ return awaited || (v.lambda?.async ?? false);
688
+ }
689
+ /**
690
+ * Emit a lambda as a skript-reflect section:
691
+ *
692
+ * create new section with {_a}, {_b} stored in {_f}:
693
+ * ...body...
694
+ *
695
+ * The parameters bind to their own locals ({_a}); locals declared before
696
+ * the creation stay readable inside the section body (skript-reflect
697
+ * sections document this), which is how closures capture.
698
+ */
699
+ emitLambdaSection(arrow, key) {
700
+ this.scopes.push();
701
+ const prevInFunction = this.inFunction;
702
+ const prevAwaited = this.sectionAwaited;
703
+ this.inFunction = false; // sections may await even inside functions
704
+ this.sectionAwaited = false;
705
+ const params = arrow.parameters.map((p) => {
706
+ const t = p.type?.getText() ?? "";
707
+ const info = this.scopes.declare(p.name.getText(), t, this.reg.category(t));
708
+ return `{${info.key}}`;
709
+ });
710
+ const withArgs = params.length ? ` with ${params.join(", ")}` : "";
711
+ this.emit(`create new section${withArgs} stored in {${key}}:`);
712
+ this.indent++;
713
+ const prevObject = this.currentRetIsObject;
714
+ const prevCat = this.currentRetCat;
715
+ this.currentRetIsObject = false;
716
+ this.currentRetCat = arrow.type ? this.reg.category(arrow.type.getText()) : "plain";
717
+ if (arrow.body.kind === ts.SyntaxKind.Block) {
718
+ this.body(arrow.body.statements);
719
+ }
720
+ else {
721
+ // expression body: (a, b) => a + b
722
+ this.emit(`return ${this.expr(arrow.body)}`);
723
+ }
724
+ this.currentRetIsObject = prevObject;
725
+ this.currentRetCat = prevCat;
726
+ this.inFunction = prevInFunction;
727
+ this.sectionAwaited = prevAwaited;
728
+ this.indent--;
729
+ this.scopes.pop();
730
+ }
731
+ /**
732
+ * Emit `run section {_f} ... with args and store result in {_r}` for a
733
+ * lambda (or `Function`-typed value) call in expression position,
734
+ * returning the result's reference. Async sections only produce output
735
+ * when waited for, so async calls append `and wait` — that is what TS
736
+ * `await` maps onto.
737
+ */
738
+ emitLambdaRun(call, async) {
739
+ const v = this.callableOf(call.expression.getText());
740
+ const args = this.listString(call.arguments);
741
+ if (async)
742
+ this.sectionAwaited = true;
743
+ const tmp = `_ts_l${++this.tempCounter}`;
744
+ const withArgs = args ? ` with ${args}` : "";
745
+ const wait = async ? " and wait" : "";
746
+ this.push(`run section {${v.key}} ${async ? "async " : ""}${withArgs} and store result in {${tmp}}${wait}`
747
+ .replace(/\s+/g, " ").trim());
748
+ return `{${tmp}}`;
749
+ }
750
+ /**
751
+ * Store an object-returning call's result and read it into the target.
752
+ * The result is an NBT compound carrying the object (its keys ride in the
753
+ * brew:keys string list tag): compounds are single values, so they cross
754
+ * function boundaries and stay in local variables — global variables
755
+ * would not be usable inside async sections.
756
+ */
757
+ readObjectCall(call, targetKey, toNbt) {
758
+ const tmp = this.tempExpr(this.expr(call));
759
+ if (toNbt) {
760
+ this.emit(`set {${targetKey}} to {${tmp}}`);
761
+ return;
762
+ }
763
+ this.emit(`clear {${targetKey}::*}`);
764
+ this.convert(tmp, "nbt", "list", targetKey);
765
+ }
766
+ /** SkBee tag kind for a value type, used in `set <kind> tag ...`. */
767
+ /** SkBee nbt tag type for a value of the given TS type. Valid types:
768
+ * byte, short, int, long, float, double, string, compound, and the list
769
+ * variants — SkBee 3.4.1+ requires an explicit type when setting. */
770
+ nbtTagKind(typeText) {
771
+ const cat = this.reg.category(typeText);
772
+ switch (cat) {
773
+ case "nbt": return "compound";
774
+ case "java": return "string";
775
+ }
776
+ const lower = typeText.toLowerCase();
777
+ if (lower === "string")
778
+ return "string";
779
+ if (lower === "boolean")
780
+ return "byte";
781
+ if (lower === "number")
782
+ return "double";
783
+ if (typeText.endsWith("[]"))
784
+ return "compound list";
785
+ // unknown value types: string tags hold anything (stringified)
786
+ return "string";
787
+ }
788
+ // -- expressions ----------------------------------------------------------
789
+ expr(node) {
790
+ if (ts.isIdentifier(node)) {
791
+ const v = this.scopes.lookup(node.text);
792
+ if (v)
793
+ return this.ref(v);
794
+ // an ambient @ffi const: its pattern substitutes for the name
795
+ const macro = this.reg.macros.get(node.text);
796
+ if (macro)
797
+ return macro.pattern;
798
+ // undeclared names are globalThis properties (JS semantics)
799
+ return `{globalThis::${node.text}}`;
800
+ }
801
+ if (ts.isNumericLiteral(node))
802
+ return node.text;
803
+ if (ts.isStringLiteral(node))
804
+ return `"${node.text}"`;
805
+ if (node.kind === ts.SyntaxKind.TrueKeyword)
806
+ return "true";
807
+ if (node.kind === ts.SyntaxKind.FalseKeyword)
808
+ return "false";
809
+ if (node.kind === ts.SyntaxKind.NullKeyword)
810
+ return "<none>";
811
+ // Template strings: literal % is escaped as %%, expressions embed as %expr%.
812
+ if (ts.isTemplateExpression(node)) {
813
+ const chunkOf = (lit) => (lit.rawText ?? lit.text ?? "").replace(/%/g, "%%");
814
+ let text = chunkOf(node.head);
815
+ for (const span of node.templateSpans) {
816
+ let e = this.expr(span.expression);
817
+ if (e.includes('"')) {
818
+ // Skript cannot nest quotes inside %...% interpolation (e.g.
819
+ // `string tag "x" of {_n}`); hoist the expression into a temp
820
+ // variable and interpolate that instead
821
+ const t = `_ts_t${++this.tempCounter}`;
822
+ this.push(`set {${t}} to ${e}`);
823
+ e = `{${t}}`;
824
+ }
825
+ text += `%${e}%`;
826
+ text += chunkOf(span.literal);
827
+ }
828
+ return `"${text}"`;
829
+ }
830
+ if (ts.isBinaryExpression(node)) {
831
+ const l = this.expr(node.left);
832
+ const r = this.expr(node.right);
833
+ switch (node.operatorToken.kind) {
834
+ case ts.SyntaxKind.EqualsToken:
835
+ case ts.SyntaxKind.PlusEqualsToken:
836
+ case ts.SyntaxKind.MinusEqualsToken:
837
+ throw new Error("Assignments must be statements, not expressions (at: " + node.getText() + ")");
838
+ // Parenthesize the operands: Skript otherwise glues the `and`/`or`
839
+ // into the left comparison's value list, e.g.
840
+ // `{_x} is greater than 100 and {_y} isn't 0` fails to parse.
841
+ case ts.SyntaxKind.AmpersandAmpersandToken: return `(${l}) and (${r})`;
842
+ case ts.SyntaxKind.BarBarToken: return `(${l}) or (${r})`;
843
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
844
+ case ts.SyntaxKind.EqualsEqualsToken: return `${l} is ${r}`;
845
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
846
+ case ts.SyntaxKind.ExclamationEqualsToken: return `${l} isn't ${r}`;
847
+ case ts.SyntaxKind.GreaterThanToken: return `${l} is greater than ${r}`;
848
+ case ts.SyntaxKind.GreaterThanEqualsToken: return `${l} is greater than or equal to ${r}`;
849
+ case ts.SyntaxKind.LessThanToken: return `${l} is less than ${r}`;
850
+ case ts.SyntaxKind.LessThanEqualsToken: return `${l} is less than or equal to ${r}`;
851
+ case ts.SyntaxKind.PlusToken: return `${l} + ${r}`;
852
+ case ts.SyntaxKind.MinusToken: return `${l} - ${r}`;
853
+ case ts.SyntaxKind.AsteriskToken: return `${l} * ${r}`;
854
+ case ts.SyntaxKind.SlashToken: return `${l} / ${r}`;
855
+ case ts.SyntaxKind.PercentToken: return `mod(${l}, ${r})`;
856
+ default:
857
+ throw new Error("Unsupported binary operator: " + node.operatorToken.getText());
858
+ }
859
+ }
860
+ if (ts.isPrefixUnaryExpression(node)) {
861
+ if (node.operator === ts.SyntaxKind.ExclamationToken)
862
+ return `not ${this.expr(node.operand)}`;
863
+ if (node.operator === ts.SyntaxKind.MinusToken)
864
+ return `-1 * ${this.expr(node.operand)}`;
865
+ }
866
+ if (ts.isParenthesizedExpression(node))
867
+ return `(${this.expr(node.expression)})`;
868
+ // await someLambda(...): the section runs async and the result is waited
869
+ // for (skript-reflect sections)
870
+ if (ts.isAwaitExpression(node)) {
871
+ if (this.inFunction) {
872
+ throw new Error("await cannot be used inside a function body — Skript functions cannot wait. " +
873
+ "Move the await to the caller or use a lambda (at: " + node.getText() + ")");
874
+ }
875
+ const inner = node.expression;
876
+ if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) &&
877
+ this.callableOf(inner.expression.text)) {
878
+ return this.emitLambdaRun(inner, true);
879
+ }
880
+ throw new Error("await is only supported on lambda calls (at: " + node.getText() + ")");
881
+ }
882
+ // Ternary: Skript has no conditional expression, so hoist an if/else
883
+ // that writes the branches into a temp. Unlike JS, both branches are
884
+ // evaluated eagerly (hoisted-if tradeoff).
885
+ if (ts.isConditionalExpression(node)) {
886
+ const cond = this.condition(node.condition);
887
+ const a = this.expr(node.whenTrue);
888
+ const b = this.expr(node.whenFalse);
889
+ const t = `_ts_t${++this.tempCounter}`;
890
+ this.push(`if ${cond}:`);
891
+ this.pushAt(1, `set {${t}} to ${a}`);
892
+ this.push("else:");
893
+ this.pushAt(1, `set {${t}} to ${b}`);
894
+ return `{${t}}`;
895
+ }
896
+ // Math.* / Object.* builtins -> native Skript expressions
897
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) &&
898
+ ts.isIdentifier(node.expression.expression)) {
899
+ const recv = node.expression.expression.text;
900
+ const m = node.expression.name.text;
901
+ const args = node.arguments.map((a) => this.expr(a));
902
+ if (recv === "Math") {
903
+ switch (m) {
904
+ case "min":
905
+ case "max":
906
+ case "abs":
907
+ case "floor":
908
+ case "ceil":
909
+ case "round":
910
+ case "sqrt":
911
+ return `${m}(${args.join(", ")})`;
912
+ case "pow":
913
+ if (args.length !== 2)
914
+ throw new Error("Math.pow takes 2 arguments");
915
+ return `${args[0]} ^ ${args[1]}`;
916
+ case "random":
917
+ return `random integer between ${args[0] ?? "0"} and ${args[1] ?? "1"}`;
918
+ default:
919
+ throw new Error(`Unsupported Math builtin: Math.${m}`);
920
+ }
921
+ }
922
+ if (recv === "Object") {
923
+ const arg = node.arguments[0];
924
+ if (arg && m === "keys") {
925
+ const cat = this.categoryOfExpr(arg);
926
+ if (cat === "nbt") {
927
+ // the NBT carrier rides its keys in the brew:keys string list tag
928
+ return `string list tag "brew:keys" of ${this.expr(arg)}`;
929
+ }
930
+ return `indices of ${this.listForm(this.expr(arg))}`;
931
+ }
932
+ if (arg && m === "values") {
933
+ if (this.categoryOfExpr(arg) === "nbt") {
934
+ throw new Error("Object.values is not supported on nbt compounds — use Object.keys " +
935
+ "and index access instead (at: " + node.getText() + ")");
936
+ }
937
+ return this.listForm(this.expr(arg));
938
+ }
939
+ throw new Error(`Unsupported Object builtin: Object.${m}`);
940
+ }
941
+ }
942
+ // Function call -> Skript function call
943
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
944
+ // lambda (section) call: shadows plain functions
945
+ const callable = this.callableOf(node.expression.text);
946
+ if (callable) {
947
+ return this.emitLambdaRun(node, this.callRunsAsync(callable, false));
948
+ }
949
+ // a @macro call: the body expands inline (#define-style)
950
+ if (this.reg.macroDecls.has(node.expression.text)) {
951
+ return this.emitMacroCall(node, false) ?? "<none>";
952
+ }
953
+ const fnName = this.fnMap.get(node.expression.text) ?? node.expression.text;
954
+ const sig = this.reg.signatures.get(node.expression.text);
955
+ if (sig?.pattern) {
956
+ // Native syntax mapping: %1%, %2%, ... -> the call arguments.
957
+ // Text-typed params take a quoted text so variables resolve at
958
+ // runtime (`"%{_name}%" parsed as player`); literals are already
959
+ // quoted by the expression emitter.
960
+ return sig.pattern.replace(/%(\d+)%/g, (_, n) => {
961
+ const arg = node.arguments[Number(n) - 1];
962
+ if (!arg)
963
+ return "<none>";
964
+ const e = this.expr(arg);
965
+ const param = sig.params[Number(n) - 1] ?? "";
966
+ if (param.toLowerCase() === "string" &&
967
+ !ts.isStringLiteral(arg) && !ts.isTemplateExpression(arg)) {
968
+ return `"%${e}%"`;
969
+ }
970
+ return e;
971
+ });
972
+ }
973
+ const types = node.arguments.map((_, i) => this.paramType(fnName, i));
974
+ return `${fnName}(${this.listString(node.arguments, types)})`;
975
+ }
976
+ // Object literal -> Skript list variable (keyed lists, see below).
977
+ if (ts.isObjectLiteralExpression(node)) {
978
+ return this.emitObjectLiteral(node, null);
979
+ }
980
+ // `new Compound({...})` -> NBT compound (SkBee).
981
+ if (ts.isNewExpression(node) && node.expression.getText() === "Compound") {
982
+ const arg = node.arguments?.[0];
983
+ if (arg && ts.isObjectLiteralExpression(arg))
984
+ return this.nbtFromLiteral(arg);
985
+ return `nbt compound from "{}"`;
986
+ }
987
+ // Property access / method calls, dispatched on the receiver's type.
988
+ if (ts.isPropertyAccessExpression(node)) {
989
+ return this.propertyAccess(node, "read");
990
+ }
991
+ // Method call on a typed receiver: player.getLocation()
992
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
993
+ // static call on a mirrored class: BukkitAdapter.worldIdentifier(w) ->
994
+ // class "xyz...BukkitAdapter".worldIdentifier({_w}) (skript-Reflect)
995
+ const recvExpr = node.expression.expression;
996
+ if (ts.isIdentifier(recvExpr)) {
997
+ const cls = this.staticClassOf(recvExpr.text);
998
+ if (cls) {
999
+ this.reg.markJava(recvExpr.text); // classloc into the import: section
1000
+ return `class "${cls.classloc}".${node.expression.name.text}(${this.listString(node.arguments)})`;
1001
+ }
1002
+ }
1003
+ const recCat = this.categoryOfExpr(node.expression.expression);
1004
+ if (recCat === "java") {
1005
+ return `${this.propertyAccess(node.expression, "call")}(${this.listString(node.arguments)})`;
1006
+ }
1007
+ return `${this.propertyAccess(node.expression, "read")}(${this.listString(node.arguments)})`;
1008
+ }
1009
+ throw new Error("Unsupported expression: " + node.getText());
1010
+ }
1011
+ /**
1012
+ * Convert an expression between the object representations:
1013
+ * - list <-> nbt: keys <-> tag names (SkBee tag expressions)
1014
+ * - java values cannot be converted automatically (they only cross over
1015
+ * through their own methods) and pass through unchanged
1016
+ * Helper lines are pushed to pending; the returned expression is ready to
1017
+ * be used where the target representation is expected.
1018
+ */
1019
+ /**
1020
+ * Returns null when the conversion wrote the result DIRECTLY into the
1021
+ * target (nbt -> list with an assignKey), in which case the caller must
1022
+ * not emit a `set` — a list copy would flatten the indices
1023
+ * (SkriptLang#3127).
1024
+ */
1025
+ convert(e, from, to, assignKey) {
1026
+ if (from === to || from === "plain" || to === "plain" || from === "java" || to === "java") {
1027
+ return e;
1028
+ }
1029
+ // expressions (function calls, arithmetic) are evaluated once into a temp
1030
+ const hoist = /^\{[^{}]+\}$/.test(e) ? e : this.tempExpr(e);
1031
+ if (from === "list" && to === "nbt") {
1032
+ // SkBee needs an explicit tag type per value (3.4.1+), so branch on the
1033
+ // runtime type; the keys ride along in a string list tag so the list
1034
+ // can be reconstructed on the other side of the boundary.
1035
+ const n = ++this.tempCounter;
1036
+ const c = `_ts_c${n}`;
1037
+ const ki = `_ts_k${n}`;
1038
+ const ks = `_ts_kl${n}`;
1039
+ this.push(`set {${c}} to nbt compound from "{}"`);
1040
+ // loop the list form ({_x} -> {_x::*}) so the values iterate
1041
+ const list = hoist.replace(/^\{(_\w+)\}$/, "{$1::*}");
1042
+ this.push(`loop ${list}:`);
1043
+ // The key is hoisted into a variable and every dynamic tag name goes
1044
+ // through a quoted interpolated string — the only form proven to
1045
+ // behave at runtime in all of Skript's expression slots. Byte tags
1046
+ // hold integers, so booleans are stored as 1/0 like Minecraft NBT.
1047
+ this.pushAt(1, `set {${ki}} to loop-index`);
1048
+ this.pushAt(1, `if loop-value is a number:`);
1049
+ this.pushAt(2, `set double tag "%{${ki}}%" of {${c}} to loop-value`);
1050
+ this.pushAt(1, `else if loop-value is true:`);
1051
+ this.pushAt(2, `set byte tag "%{${ki}}%" of {${c}} to 1`);
1052
+ this.pushAt(1, `else if loop-value is false:`);
1053
+ this.pushAt(2, `set byte tag "%{${ki}}%" of {${c}} to 0`);
1054
+ this.pushAt(1, `else if loop-value is a nbt compound:`);
1055
+ this.pushAt(2, `set compound tag "%{${ki}}%" of {${c}} to loop-value`);
1056
+ this.pushAt(1, `else:`);
1057
+ this.pushAt(2, `set string tag "%{${ki}}%" of {${c}} to "%loop-value%"`);
1058
+ this.pushAt(1, `add {${ki}} to {${ks}::*}`);
1059
+ this.push(`set string list tag "brew:keys" of {${c}} to {${ks}::*}`);
1060
+ return `{${c}}`;
1061
+ }
1062
+ // nbt -> list: re-zip via the embedded keys list tag. SkBee's typed
1063
+ // getters return null when the requested type doesn't match the tag's
1064
+ // actual type (NBTApi.getTag), so first read the tag's runtime type and
1065
+ // then use the matching getter. With an assignKey the entries are
1066
+ // written DIRECTLY into the target — copying a list variable loses its
1067
+ // indices (SkriptLang#3127).
1068
+ const target = assignKey ?? `_ts_l${++this.tempCounter}`;
1069
+ const k = `_ts_k${++this.tempCounter}`;
1070
+ this.usedTagHelper = true;
1071
+ this.push(`loop string list tag "brew:keys" of ${hoist}:`);
1072
+ this.pushAt(1, `set {${k}} to loop-value`);
1073
+ this.pushAt(1, `set {${target}::%{${k}}%} to brew_tag(${hoist}, {${k}})`);
1074
+ return assignKey ? null : `{${target}::*}`;
1075
+ }
1076
+ /** Store an expression in a fresh temp variable, returning its reference. */
1077
+ tempExpr(e) {
1078
+ const t = `_ts_x${++this.tempCounter}`;
1079
+ this.push(`set {${t}} to ${e}`);
1080
+ return `{${t}}`;
1081
+ }
1082
+ /** Declared TS type text of argument `i` of the named function, if known. */
1083
+ paramType(fnName, i) {
1084
+ const sig = this.reg.signatures.get(fnName);
1085
+ if (!sig || i >= sig.params.length)
1086
+ return undefined;
1087
+ return sig.params[i];
1088
+ }
1089
+ /**
1090
+ * Comma-joined Skript list from expression nodes. When more than one
1091
+ * element is joined, every non-atomic element is wrapped in parentheses so
1092
+ * Skript doesn't glue multi-word expressions to their neighbours, e.g.
1093
+ * `set {_v::*} to (keyed {_k::*} from {_t::*}), 3`.
1094
+ */
1095
+ listString(nodes, paramTypes) {
1096
+ const parts = nodes.map((n, i) => {
1097
+ const e = this.expr(n);
1098
+ const to = paramTypes?.[i];
1099
+ if (to && to.toLowerCase() === "object") {
1100
+ // object param: the callee receives an NBT compound carrying the
1101
+ // object. A call that already returns an object IS one (passthrough);
1102
+ // anything else is packed from its keyed-list form.
1103
+ if (this.isObjCall(n))
1104
+ return e;
1105
+ const list = ts.isObjectLiteralExpression(n)
1106
+ ? this.emitObjectLiteral(n, null)
1107
+ : this.listForm(e);
1108
+ return this.convert(list, "list", "nbt") ?? e;
1109
+ }
1110
+ const cat = to ? this.reg.category(to) : undefined;
1111
+ return cat && cat !== this.categoryOfExpr(n)
1112
+ ? this.convert(e, this.categoryOfExpr(n), cat) ?? e
1113
+ : e;
1114
+ });
1115
+ if (parts.length < 2)
1116
+ return parts.join(", ");
1117
+ return parts.map((e, i) => (this.isAtomic(nodes[i]) ? e : `(${e})`)).join(", ");
1118
+ }
1119
+ /** True when an expression is safe unparenthesized inside a comma list. */
1120
+ isAtomic(node) {
1121
+ if (ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
1122
+ ts.isIdentifier(node) || ts.isTemplateExpression(node) ||
1123
+ ts.isParenthesizedExpression(node) || ts.isCallExpression(node) ||
1124
+ node.kind === ts.SyntaxKind.TrueKeyword ||
1125
+ node.kind === ts.SyntaxKind.FalseKeyword ||
1126
+ node.kind === ts.SyntaxKind.NullKeyword) {
1127
+ return true;
1128
+ }
1129
+ if (ts.isPropertyAccessExpression(node)) {
1130
+ // {_x::k} and {_p}.field are atomic; `tag "k" of {_x}` is not
1131
+ return this.categoryOfExpr(node.expression) !== "nbt";
1132
+ }
1133
+ return false;
1134
+ }
1135
+ /**
1136
+ * Emit an object literal as a Skript keyed list:
1137
+ *
1138
+ * set {_ts_k<N>::*} to "k1", "k2", ...
1139
+ * set {_ts_v<N>::*} to v1, v2, ...
1140
+ * loop {_ts_k<N>::*}:
1141
+ * set {<target>::%loop-value%} to {_ts_v<N>::%loop-index%}
1142
+ *
1143
+ * When `assignTo` is given (e.g. declaring variable `thing`), the entries
1144
+ * are written directly into `{_thing::*}`, creating `{_thing::k1} = v1`, ...
1145
+ */
1146
+ emitObjectLiteral(node, assignTo) {
1147
+ const n = ++this.tempCounter;
1148
+ const k = `_ts_k${n}`;
1149
+ const v = `_ts_v${n}`;
1150
+ const pairs = node.properties.filter(ts.isPropertyAssignment);
1151
+ // Empty literal: no keyed construction needed (and the plain `set to `
1152
+ // would emit a line ending early); an empty list is just `clear`.
1153
+ if (pairs.length === 0) {
1154
+ const target = assignTo ?? `_ts_e${n}`;
1155
+ this.push(`clear {${target}::*}`);
1156
+ return `{${target}::*}`;
1157
+ }
1158
+ const keys = pairs.map((p) => `"${p.name.getText().replace(/^["']|["']$/g, "")}"`);
1159
+ const vals = this.listString(pairs.map((p) => p.initializer));
1160
+ this.push(`set {${k}::*} to ${keys.join(", ")}`);
1161
+ this.push(`set {${v}::*} to ${vals}`);
1162
+ // Skript has no "keyed zip" expression: build the keyed list by looping
1163
+ // the keys and pulling the value at the matching index.
1164
+ // loop {_k::*}:
1165
+ // set {_target::%loop-value%} to {_v::%loop-index%}
1166
+ const target = assignTo ?? `_ts_o${++this.tempCounter}`;
1167
+ this.push(`loop {${k}::*}:`);
1168
+ this.pushAt(1, `set {${target}::%loop-value%} to {${v}::%loop-index%}`);
1169
+ return `{${target}::*}`;
1170
+ }
1171
+ /**
1172
+ * SNBT pairs of an object literal, e.g. `a: 1, b: %{_x}%, c: {inner: 2}`.
1173
+ * Nested object literals become nested SNBT compounds.
1174
+ */
1175
+ nbtPairs(node) {
1176
+ const pairs = node.properties.filter(ts.isPropertyAssignment).map((p) => {
1177
+ const key = p.name.getText().replace(/^["']|["']$/g, "");
1178
+ if (ts.isObjectLiteralExpression(p.initializer)) {
1179
+ return `${key}: {${this.nbtPairs(p.initializer)}}`;
1180
+ }
1181
+ const val = this.expr(p.initializer);
1182
+ // literals embed raw (SNBT knows numbers, booleans and quoted strings);
1183
+ // every other value is an expression and embeds as %expression%.
1184
+ // The whole SNBT goes inside a Skript string literal, so quotes in
1185
+ // string values must be doubled (""), like any other Skript string.
1186
+ // String-typed expressions get SNBT quotes too, so values with spaces
1187
+ // survive: `""%{_name}%""` -> `name: "the name"`.
1188
+ const literalRaw = ts.isNumericLiteral(p.initializer) || ts.isStringLiteral(p.initializer) ||
1189
+ p.initializer.kind === ts.SyntaxKind.TrueKeyword ||
1190
+ p.initializer.kind === ts.SyntaxKind.FalseKeyword;
1191
+ const raw = literalRaw
1192
+ ? val.replace(/"/g, '""')
1193
+ : this.typeOfExpr(p.initializer).toLowerCase() === "string"
1194
+ ? `""%${val}%""`
1195
+ : `%${val}%`;
1196
+ return `${key}: ${raw}`;
1197
+ });
1198
+ return pairs.join(", ");
1199
+ }
1200
+ /** Keys of an object literal (used for the brew:keys tag). */
1201
+ objectLiteralKeys(node) {
1202
+ return node.properties
1203
+ .filter(ts.isPropertyAssignment)
1204
+ .map((p) => p.name.getText().replace(/^["']|["']$/g, ""));
1205
+ }
1206
+ /** `nbt compound from "..."` from an object literal (SkBee). */
1207
+ nbtFromLiteral(node) {
1208
+ return `nbt compound from "{${this.nbtPairs(node)}}"`;
1209
+ }
1210
+ /**
1211
+ * Property access, dispatched on the receiver's type category:
1212
+ * - nbt: `tag "name" of {_x}` (SkBee)
1213
+ * - java: `{_x}.name` (skript-Reflect)
1214
+ * - list: `{_x::name}` (Skript list index)
1215
+ */
1216
+ /**
1217
+ * Expand a `@macro` call inline, C/C++ #define-style: the macro's body
1218
+ * is never a function — each call site evaluates the arguments (into
1219
+ * locals, once), binds the parameters, and emits the body statements plus
1220
+ * the trailing `return` expression as the call's value.
1221
+ *
1222
+ * Statement-bodied macros can only be called in statement position (the
1223
+ * statements emit in place); a macro whose body is a bare `return` works
1224
+ * in expression position too.
1225
+ */
1226
+ emitMacroCall(call, statement) {
1227
+ const decl = this.reg.macroDecls.get(call.expression.getText());
1228
+ const body = decl.body.statements;
1229
+ const last = body[body.length - 1];
1230
+ const returns = last && ts.isReturnStatement(last);
1231
+ const stmts = returns ? body.slice(0, -1) : body;
1232
+ if (stmts.length > 0 && !statement) {
1233
+ // an @inline function keeps its Skript function: a statement-bodied
1234
+ // inline can still be called for its value
1235
+ if (jsdocTagText(ts.getJSDocTags(decl), ["inline"]) !== undefined) {
1236
+ const name = call.expression.getText();
1237
+ const fnName = this.fnMap.get(name) ?? name;
1238
+ const sig = this.reg.signatures.get(name);
1239
+ return `${fnName}(${this.listString(call.arguments, sig?.params)})`;
1240
+ }
1241
+ throw new Error("this macro has statements before its return, so it can only be called as a " +
1242
+ "statement, not as a value (at: " + call.getText().slice(0, 60) + ")");
1243
+ }
1244
+ // arguments are evaluated in the caller's scope, hoisted into locals
1245
+ const argRefs = [];
1246
+ for (const a of call.arguments) {
1247
+ const e = this.expr(a);
1248
+ const tmp = `_ts_m${++this.tempCounter}`;
1249
+ this.push(`set {${tmp}} to ${e}`);
1250
+ argRefs.push(tmp);
1251
+ }
1252
+ this.scopes.push();
1253
+ decl.parameters.forEach((p, i) => {
1254
+ const t = p.type?.getText() ?? "";
1255
+ this.scopes.declareWithKey(p.name.getText(), argRefs[i] ?? `ts_m${++this.tempCounter}`, t, this.reg.category(t));
1256
+ });
1257
+ if (statement)
1258
+ this.body(stmts);
1259
+ const value = statement || !returns || !last.expression
1260
+ ? null
1261
+ : this.expr(last.expression);
1262
+ this.scopes.pop();
1263
+ return value;
1264
+ }
1265
+ /**
1266
+ * The @ffi pattern of a property access, if the receiver's declared
1267
+ * type annotates that member: native get/set syntax for NBT, Java
1268
+ * objects, lists — anything.
1269
+ */
1270
+ memberPattern(receiverExpr, member) {
1271
+ const type = this.typeOfExpr(receiverExpr);
1272
+ return type ? (this.reg.memberPatterns.get(type)?.get(member) ?? null) : null;
1273
+ }
1274
+ /**
1275
+ * Substitute a property's @skript pattern: %this% is the receiver
1276
+ * (also %1%), %key% the property name (%2%), and %value% the assigned
1277
+ * value (%3%) — the setter form.
1278
+ */
1279
+ applyMemberPattern(pattern, receiver, member, value) {
1280
+ return pattern
1281
+ .replace(/%this%|%1%/g, receiver)
1282
+ .replace(/%key%|%2%/g, member)
1283
+ .replace(/%value%|%3%/g, value ?? "<none>");
1284
+ }
1285
+ propertyAccess(node, mode) {
1286
+ // static field read on a mirrored class: Messages.RENDER_IN_PROGRESS ->
1287
+ // class "xyz...Messages".RENDER_IN_PROGRESS (skript-Reflect)
1288
+ if (ts.isIdentifier(node.expression)) {
1289
+ const cls = this.staticClassOf(node.expression.text);
1290
+ if (cls) {
1291
+ this.reg.markJava(node.expression.text);
1292
+ return `class "${cls.classloc}".${node.name.text}`;
1293
+ }
1294
+ }
1295
+ const receiver = this.expr(node.expression);
1296
+ const member = node.name.text;
1297
+ const cat = this.categoryOfExpr(node.expression);
1298
+ // a @skript pattern on the property replaces the default access form
1299
+ const pattern = this.memberPattern(node.expression, member);
1300
+ if (pattern) {
1301
+ if (pattern.includes("%value%") || pattern.includes("%3%")) {
1302
+ throw new Error("the property is set-only: its @skript pattern is a setter form " +
1303
+ "(it contains %value%) and cannot be read (at: " + node.getText().slice(0, 60) + ")");
1304
+ }
1305
+ return this.applyMemberPattern(pattern, receiver, member);
1306
+ }
1307
+ switch (cat) {
1308
+ case "nbt":
1309
+ // typed getters return null on type mismatch (NBTApi.getTag), so
1310
+ // reads go through the runtime helper that matches the tag's type
1311
+ this.usedTagHelper = true;
1312
+ return `brew_tag(${receiver}, "${member}")`;
1313
+ case "java":
1314
+ return `${receiver}.${member}`;
1315
+ default:
1316
+ return this.indexRef(node.expression, member, receiver);
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Skript list index of a member: `{_x::name}`. The index belongs INSIDE the
1321
+ * variable braces — `{_x}::name` is invalid Skript. For non-variable
1322
+ * receivers (e.g. call results) fall back to `{...}::name`, which is the
1323
+ * best we can do without an intermediate variable.
1324
+ */
1325
+ indexRef(receiverExpr, member, receiver) {
1326
+ const rec = receiver ?? this.expr(receiverExpr);
1327
+ const m = /^\{(_\w+|globalThis::\w+)\}$/.exec(rec);
1328
+ return m ? `{${m[1]}::${member}}` : `${rec}::${member}`;
1329
+ }
1330
+ /**
1331
+ * Skript cannot combine two conditions with and/or (the `and`/`or` glue
1332
+ * into value lists), so complex conditions are evaluated into a boolean
1333
+ * flag first, one single-condition line at a time:
1334
+ *
1335
+ * set {_ts_bN} to false
1336
+ * if {_p::y} is greater than 100:
1337
+ * set {_ts_bN} to true
1338
+ * ...
1339
+ * if {_ts_bN} is true: (or: isn't true)
1340
+ */
1341
+ isLogicalCond(e) {
1342
+ if (ts.isParenthesizedExpression(e))
1343
+ return this.isLogicalCond(e.expression);
1344
+ if (ts.isPrefixUnaryExpression(e))
1345
+ return e.operator === ts.SyntaxKind.ExclamationToken && this.isLogicalCond(e.operand);
1346
+ if (ts.isBinaryExpression(e)) {
1347
+ const k = e.operatorToken.kind;
1348
+ if (k === ts.SyntaxKind.AmpersandAmpersandToken || k === ts.SyntaxKind.BarBarToken)
1349
+ return true;
1350
+ }
1351
+ return false;
1352
+ }
1353
+ /** Skript condition text for a single (non-logical) TS condition. */
1354
+ condition(e) {
1355
+ if (ts.isParenthesizedExpression(e))
1356
+ return this.condition(e.expression);
1357
+ if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) {
1358
+ return this.invertedCondition(e.operand);
1359
+ }
1360
+ // bare values need explicit truthiness: booleans test against `true`,
1361
+ // everything else (numbers, texts, optional args bound by Skript)
1362
+ // tests for being set — JS truthiness maps to "has a value"
1363
+ if (ts.isIdentifier(e))
1364
+ return this.truthiness(e);
1365
+ return this.expr(e);
1366
+ }
1367
+ /** `x` as a condition: `{_x} is true` for booleans, `{_x} is set` else. */
1368
+ truthiness(e, invert = false) {
1369
+ const ref = this.expr(e);
1370
+ const op = invert ? "isn't" : "is";
1371
+ const t = (ts.isIdentifier(e) ? this.typeOfExpr(e) : "").toLowerCase();
1372
+ return t === "boolean" ? `${ref} ${op} true` : `${ref} ${op} set`;
1373
+ }
1374
+ /** The negation of a single TS condition. */
1375
+ invertedCondition(e) {
1376
+ e = ts.isParenthesizedExpression(e) ? e.expression : e;
1377
+ if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) {
1378
+ return this.condition(e.operand); // !!x == x
1379
+ }
1380
+ if (ts.isBinaryExpression(e)) {
1381
+ const l = this.expr(e.left);
1382
+ const r = this.expr(e.right);
1383
+ switch (e.operatorToken.kind) {
1384
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
1385
+ case ts.SyntaxKind.EqualsEqualsToken: return `${l} isn't ${r}`;
1386
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
1387
+ case ts.SyntaxKind.ExclamationEqualsToken: return `${l} is ${r}`;
1388
+ case ts.SyntaxKind.GreaterThanToken: return `${l} is less than or equal to ${r}`;
1389
+ case ts.SyntaxKind.GreaterThanEqualsToken: return `${l} is less than ${r}`;
1390
+ case ts.SyntaxKind.LessThanToken: return `${l} is greater than or equal to ${r}`;
1391
+ case ts.SyntaxKind.LessThanEqualsToken: return `${l} is greater than ${r}`;
1392
+ }
1393
+ }
1394
+ if (ts.isIdentifier(e))
1395
+ return this.truthiness(e, true);
1396
+ throw new Error("Cannot negate this condition: " + e.getText());
1397
+ }
1398
+ /**
1399
+ * Evaluate an arbitrary TS condition into a fresh boolean flag. Returns the
1400
+ * flag's key; the flag is set to true iff the condition holds.
1401
+ */
1402
+ emitCondFlag(e) {
1403
+ e = ts.isParenthesizedExpression(e) ? e.expression : e;
1404
+ const flag = (n = ++this.tempCounter) => `_ts_b${n}`;
1405
+ if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken && this.isLogicalCond(e.operand)) {
1406
+ // !(A && B) -> evaluate A&&B, then invert
1407
+ const inner = this.emitCondFlag(e.operand);
1408
+ const f = flag();
1409
+ this.push(`set {${f}} to false`);
1410
+ this.push(`if {${inner}} isn't true:`);
1411
+ this.pushAt(1, `set {${f}} to true`);
1412
+ return f;
1413
+ }
1414
+ if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) {
1415
+ const f = flag();
1416
+ this.push(`set {${f}} to false`);
1417
+ this.push(`if ${this.invertedCondition(e.operand)}:`);
1418
+ this.pushAt(1, `set {${f}} to true`);
1419
+ return f;
1420
+ }
1421
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
1422
+ const a = this.emitCondFlag(e.left);
1423
+ const b = this.emitCondFlag(e.right);
1424
+ const f = flag();
1425
+ this.push(`set {${f}} to false`);
1426
+ this.push(`if {${a}} is true:`);
1427
+ this.pushAt(1, `if {${b}} is true:`);
1428
+ this.pushAt(2, `set {${f}} to true`);
1429
+ return f;
1430
+ }
1431
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
1432
+ const a = this.emitCondFlag(e.left);
1433
+ const b = this.emitCondFlag(e.right);
1434
+ const f = flag();
1435
+ this.push(`set {${f}} to false`);
1436
+ this.push(`if {${a}} is true:`);
1437
+ this.pushAt(1, `set {${f}} to true`);
1438
+ this.push(`if {${b}} is true:`);
1439
+ this.pushAt(1, `set {${f}} to true`);
1440
+ return f;
1441
+ }
1442
+ // simple condition, evaluated directly
1443
+ const f = flag();
1444
+ this.push(`set {${f}} to false`);
1445
+ this.push(`if ${this.condition(e)}:`);
1446
+ this.pushAt(1, `set {${f}} to true`);
1447
+ return f;
1448
+ }
1449
+ /** Emit an `if`/`else if` line for a TS condition (flag machinery when logical). */
1450
+ emitConditionLine(prefix, e) {
1451
+ if (this.isLogicalCond(e)) {
1452
+ const f = this.emitCondFlag(e);
1453
+ this.emit(`${prefix} {${f}} is true:`);
1454
+ }
1455
+ else {
1456
+ this.emit(`${prefix} ${this.condition(e)}:`);
1457
+ }
1458
+ }
1459
+ // -- statements -----------------------------------------------------------
1460
+ statement(stmt) {
1461
+ if (ts.isBlock(stmt)) {
1462
+ this.scopes.push();
1463
+ this.body(stmt.statements);
1464
+ this.scopes.pop();
1465
+ return;
1466
+ }
1467
+ // let / const / var -> set {_name} to ...
1468
+ if (ts.isVariableStatement(stmt)) {
1469
+ for (const d of stmt.declarationList.declarations) {
1470
+ if (!ts.isIdentifier(d.name))
1471
+ throw new Error("Destructuring is not supported yet");
1472
+ const name = d.name.text;
1473
+ // lambda -> skript-reflect section: `create new section with {_a},
1474
+ // {_b} stored in {_f}:` (locals from before the creation stay
1475
+ // readable inside the section)
1476
+ // top-level declarations bind as globalThis globals: the init
1477
+ // trigger runs before every user trigger, and its bindings must
1478
+ // stay readable from them (like a module's exports)
1479
+ const declare = this.topLevelInit
1480
+ ? (n, t, c, l = false) => this.scopes.declareGlobal(n, t, c, l)
1481
+ : (n, t, c, l = false) => this.scopes.declare(n, t, c, l);
1482
+ if (d.initializer && ts.isArrowFunction(d.initializer)) {
1483
+ const arrow = d.initializer;
1484
+ const info = declare(name, "", "plain", false);
1485
+ info.lambda = {
1486
+ async: !!arrow.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword),
1487
+ };
1488
+ this.emitLambdaSection(arrow, info.key);
1489
+ continue;
1490
+ }
1491
+ const typeText = d.type?.getText() ?? this.typeOfExpr(d.initializer);
1492
+ const cat = this.reg.category(typeText);
1493
+ if (cat === "java")
1494
+ this.reg.markJava(typeText);
1495
+ const info = declare(name, typeText, cat, (d.type?.getText() ?? "").endsWith("[]"));
1496
+ const init = d.initializer;
1497
+ if (ts.isObjectLiteralExpression(init) && cat === "nbt") {
1498
+ // NBT-typed literal: build an actual nbt compound (SkBee SNBT)
1499
+ this.emit(`set {${info.key}} to ${this.nbtFromLiteral(init)}`);
1500
+ }
1501
+ else if (ts.isObjectLiteralExpression(init) && cat === "list") {
1502
+ this.emitObjectLiteral(init, info.key);
1503
+ }
1504
+ else if (ts.isObjectLiteralExpression(init)) {
1505
+ this.emit(`set {${info.key}} to ${this.expr(init)}`);
1506
+ }
1507
+ else if (this.isObjCall(init)) {
1508
+ // object-returning call: its NBT compound result is read straight
1509
+ // into the target
1510
+ this.readObjectCall(init, info.key, cat === "nbt");
1511
+ }
1512
+ else {
1513
+ const from = this.categoryOfExpr(init);
1514
+ const e = this.expr(init);
1515
+ if (from === "list" && cat !== "nbt") {
1516
+ // list values are copied key by key: a plain `set {x::*} to {y::*}`
1517
+ // loses the indices (SkriptLang#3127)
1518
+ this.push(`loop ${this.listForm(e)}:`);
1519
+ this.pushAt(1, `set {${info.key}::%loop-index%} to loop-value`);
1520
+ }
1521
+ else {
1522
+ const conv = this.convert(e, from, cat, info.key);
1523
+ // conv === null: the conversion wrote directly into {key::...}
1524
+ if (conv !== null)
1525
+ this.emit(`set ${this.ref(info)} to ${conv}`);
1526
+ }
1527
+ }
1528
+ }
1529
+ return;
1530
+ }
1531
+ if (ts.isExpressionStatement(stmt)) {
1532
+ const e = stmt.expression;
1533
+ // x++ / x-- (and ++x / --x) -> add/remove 1
1534
+ if ((ts.isPostfixUnaryExpression(e) || ts.isPrefixUnaryExpression(e)) &&
1535
+ (e.operator === ts.SyntaxKind.PlusPlusToken ||
1536
+ e.operator === ts.SyntaxKind.MinusMinusToken)) {
1537
+ const target = this.expr(e.operand);
1538
+ this.emit(e.operator === ts.SyntaxKind.PlusPlusToken
1539
+ ? `add 1 to ${target}`
1540
+ : `remove 1 from ${target}`);
1541
+ return;
1542
+ }
1543
+ if (ts.isBinaryExpression(e)) {
1544
+ const kind = e.operatorToken.kind;
1545
+ if (kind === ts.SyntaxKind.EqualsToken) {
1546
+ this.emitAssignment(e);
1547
+ return;
1548
+ }
1549
+ if (kind === ts.SyntaxKind.PlusEqualsToken) {
1550
+ this.emit(`add ${this.expr(e.right)} to ${this.expr(e.left)}`);
1551
+ return;
1552
+ }
1553
+ if (kind === ts.SyntaxKind.MinusEqualsToken) {
1554
+ this.emit(`remove ${this.expr(e.right)} from ${this.expr(e.left)}`);
1555
+ return;
1556
+ }
1557
+ if (kind === ts.SyntaxKind.AsteriskEqualsToken || kind === ts.SyntaxKind.SlashEqualsToken) {
1558
+ const op = kind === ts.SyntaxKind.AsteriskEqualsToken ? "*" : "/";
1559
+ this.emit(`set ${this.expr(e.left)} to ${this.expr(e.left)} ${op} ${this.expr(e.right)}`);
1560
+ return;
1561
+ }
1562
+ }
1563
+ // a @macro statement call: the body expands inline
1564
+ if (ts.isCallExpression(e) && ts.isIdentifier(e.expression) &&
1565
+ this.reg.macroDecls.has(e.expression.text)) {
1566
+ this.emitMacroCall(e, true);
1567
+ return;
1568
+ }
1569
+ // lambda (section) call as a statement: run without storing a result.
1570
+ // An awaited async call waits; without await it is fire-and-forget.
1571
+ {
1572
+ const target = ts.isAwaitExpression(e) ? e.expression : e;
1573
+ const awaited = ts.isAwaitExpression(e);
1574
+ if (ts.isCallExpression(target) && ts.isIdentifier(target.expression)) {
1575
+ const callable = this.callableOf(target.expression.text);
1576
+ if (callable) {
1577
+ const async = this.callRunsAsync(callable, awaited);
1578
+ const args = this.listString(target.arguments);
1579
+ if (async)
1580
+ this.sectionAwaited = true;
1581
+ const wait = async ? " and wait" : "";
1582
+ const withArgs = args ? ` with ${args}` : "";
1583
+ this.emit(`run section {${callable.key}} ${async ? "async " : ""}${withArgs}${wait}`
1584
+ .replace(/\s+/g, " ").trim());
1585
+ return;
1586
+ }
1587
+ }
1588
+ }
1589
+ this.emit(this.expr(e));
1590
+ return;
1591
+ }
1592
+ // if / else if / else
1593
+ if (ts.isIfStatement(stmt)) {
1594
+ this.scopes.push();
1595
+ this.emitConditionLine("if", stmt.expression);
1596
+ this.indented([stmt.thenStatement]);
1597
+ let cur = stmt;
1598
+ while (cur.elseStatement) {
1599
+ const els = cur.elseStatement;
1600
+ if (ts.isIfStatement(els)) {
1601
+ this.emitConditionLine("else if", els.expression);
1602
+ this.indented([els.thenStatement]);
1603
+ cur = els;
1604
+ }
1605
+ else {
1606
+ this.emit("else:");
1607
+ this.indented([els]);
1608
+ this.scopes.pop();
1609
+ return;
1610
+ }
1611
+ }
1612
+ this.scopes.pop();
1613
+ return;
1614
+ }
1615
+ if (ts.isWhileStatement(stmt)) {
1616
+ this.scopes.push();
1617
+ if (this.isLogicalCond(stmt.expression)) {
1618
+ // logical conditions must be re-evaluated every iteration: wrap the
1619
+ // loop and exit early when the flag ends up false
1620
+ this.emit("while 1 is 1:");
1621
+ this.indent++;
1622
+ const f = this.emitCondFlag(stmt.expression);
1623
+ this.emit(`if {${f}} isn't true:`);
1624
+ this.indent++;
1625
+ this.emit("exit loop");
1626
+ this.indent--;
1627
+ this.body([stmt.statement]);
1628
+ this.indent--;
1629
+ }
1630
+ else {
1631
+ this.emit(`while ${this.condition(stmt.expression)}:`);
1632
+ this.indented([stmt.statement]);
1633
+ }
1634
+ this.scopes.pop();
1635
+ return;
1636
+ }
1637
+ // for..of -> loop
1638
+ if (ts.isForOfStatement(stmt)) {
1639
+ const decl = stmt.initializer.declarations[0];
1640
+ const item = decl.name.text;
1641
+ this.scopes.push();
1642
+ // element type: the annotation, else T[] of the iterated expression
1643
+ const iterType = this.typeOfExpr(stmt.expression);
1644
+ const typeText = decl.type?.getText() ??
1645
+ (iterType.endsWith("[]") ? iterType.slice(0, -2) : "");
1646
+ const info = this.scopes.declare(item, typeText, this.reg.category(typeText));
1647
+ this.emit(`loop ${this.expr(stmt.expression)}:`);
1648
+ this.indent++;
1649
+ this.emit(`set ${this.ref(info)} to loop-value`);
1650
+ this.body([stmt.statement]);
1651
+ this.indent--;
1652
+ this.scopes.pop();
1653
+ return;
1654
+ }
1655
+ if (ts.isReturnStatement(stmt)) {
1656
+ // Skript forbids returning after a delay: the guard covers both a
1657
+ // return that follows an earlier async run AND `return await ...`
1658
+ // (the await sits inside the return expression itself)
1659
+ const containsAwait = (n) => ts.isAwaitExpression(n) || n.getChildren().some(containsAwait);
1660
+ if (stmt.expression && !this.inFunction &&
1661
+ (this.sectionAwaited || containsAwait(stmt.expression))) {
1662
+ throw new Error("a value cannot be returned after a delay (Skript forbids return after async runs) — " +
1663
+ "store it in a global instead and read it later (assign to an undeclared name, e.g. " +
1664
+ "`result = value`, which maps to {globalThis::result}) (at: " +
1665
+ stmt.getText().slice(0, 60) + ")");
1666
+ }
1667
+ if (stmt.expression && this.currentRetIsObject) {
1668
+ // object return: pack the keyed list into an NBT compound and return
1669
+ // it. A call that already returns an object IS one and passes
1670
+ // straight through.
1671
+ if (this.isObjCall(stmt.expression)) {
1672
+ this.emit(`return ${this.expr(stmt.expression)}`);
1673
+ return;
1674
+ }
1675
+ const list = ts.isObjectLiteralExpression(stmt.expression)
1676
+ ? this.emitObjectLiteral(stmt.expression, null)
1677
+ : this.expr(stmt.expression);
1678
+ const conv = this.convert(this.listForm(list), "list", "nbt");
1679
+ this.emit(`return ${conv ?? list}`);
1680
+ return;
1681
+ }
1682
+ if (stmt.expression && ts.isObjectLiteralExpression(stmt.expression)) {
1683
+ const out = this.emitObjectLiteral(stmt.expression, null);
1684
+ this.emit(`return ${this.convert(out, "list", this.currentRetCat)}`);
1685
+ return;
1686
+ }
1687
+ if (stmt.expression) {
1688
+ const e = this.expr(stmt.expression);
1689
+ const conv = this.convert(e, this.categoryOfExpr(stmt.expression), this.currentRetCat);
1690
+ this.emit(`return ${conv ?? e}`);
1691
+ }
1692
+ else {
1693
+ this.emit("stop");
1694
+ }
1695
+ return;
1696
+ }
1697
+ if (ts.isBreakStatement(stmt)) {
1698
+ this.emit("exit loop");
1699
+ return;
1700
+ }
1701
+ if (ts.isContinueStatement(stmt)) {
1702
+ this.emit("# continue (unsupported)");
1703
+ return;
1704
+ }
1705
+ throw new Error("Unsupported statement: " + stmt.getText().slice(0, 60));
1706
+ }
1707
+ /** Skript type name for a TS type node, using this registry. */
1708
+ skriptType(node) {
1709
+ return skriptType(node, this.reg);
1710
+ }
1711
+ /** Emit a Skript `function` declaration. */
1712
+ emitFunction(f) {
1713
+ this.scopes.push();
1714
+ this.bindParams(f); // keys first: the signature names come from the scope
1715
+ const params = f.parameters.map((p) => {
1716
+ const v = this.scopes.lookup(p.name.getText());
1717
+ // Skript names parameters WITHOUT the underscore: `f(o: text)` binds
1718
+ // {_o} in the body
1719
+ return `${v.key.replace(/^_/, "")}: ${skriptType(p.type, this.reg)}`;
1720
+ });
1721
+ // `void` (and a missing annotation) means no Skript return type
1722
+ const retText = f.type?.getText() ?? "";
1723
+ const isVoid = retText.toLowerCase() === "void";
1724
+ const ret = f.type && !isVoid ? ` :: ${skriptType(f.type, this.reg)}:` : ":";
1725
+ // `object` returns travel as an NBT compound (see TYPE_MAP); everything
1726
+ // else converts to its Skript representation as usual
1727
+ this.currentRetIsObject = retText.toLowerCase() === "object";
1728
+ this.currentRetCat = this.currentRetIsObject || isVoid || !f.type
1729
+ ? this.currentRetIsObject ? "list" : "plain"
1730
+ : this.reg.category(retText);
1731
+ const outName = this.fnMap.get(f.name.text) ?? f.name.text;
1732
+ this.emit(`function ${outName}(${params.join(", ")})${ret}`);
1733
+ this.indent++;
1734
+ const prevInFunction = this.inFunction;
1735
+ this.inFunction = true;
1736
+ this.emitObjectParamPrologue(f);
1737
+ this.body(f.body?.statements ?? []);
1738
+ this.inFunction = prevInFunction;
1739
+ this.indent--;
1740
+ this.scopes.pop();
1741
+ this.currentRetIsObject = false;
1742
+ }
1743
+ /**
1744
+ * Emit the top-level `on load:` init trigger: the statements run in
1745
+ * source order, and their `let`/`const` declarations bind as
1746
+ * globalThis globals so every later trigger can read them.
1747
+ */
1748
+ emitTopLevel(stmts) {
1749
+ this.scopes.push();
1750
+ this.emit("on load:");
1751
+ this.indent++;
1752
+ this.sectionAwaited = false;
1753
+ this.topLevelInit = true;
1754
+ this.body(stmts);
1755
+ this.topLevelInit = false;
1756
+ this.indent--;
1757
+ this.scopes.pop();
1758
+ }
1759
+ /** Emit a `on <event>:` trigger from a /** @on ... *\/ function. */
1760
+ emitEvent(event, decl) {
1761
+ this.currentRetCat = "plain";
1762
+ this.sectionAwaited = false; // each trigger starts fresh
1763
+ this.scopes.push();
1764
+ this.bindParams(decl);
1765
+ this.emit(`on ${event}:`);
1766
+ this.indent++;
1767
+ for (const p of decl.parameters) {
1768
+ const v = this.scopes.lookup(p.name.getText());
1769
+ const t = skriptType(p.type, this.reg).toLowerCase();
1770
+ const binding = v.isList
1771
+ ? "<none> # TODO: bind list event param"
1772
+ : t === "player"
1773
+ ? "player"
1774
+ : t === "world"
1775
+ ? "world"
1776
+ : "<none>";
1777
+ this.emit(`set ${this.ref(v)} to ${binding}`);
1778
+ }
1779
+ this.body(decl.body?.statements ?? []);
1780
+ this.indent--;
1781
+ this.scopes.pop();
1782
+ }
1783
+ /** Emit a `command /name <args>:` block from a /** @command ... *\/ function.
1784
+ * Skript binds each declared argument to {_name} inside the trigger, so
1785
+ * the params need no binding lines — the body compiles as usual. */
1786
+ emitCommand(name, decl) {
1787
+ this.currentRetCat = "plain";
1788
+ this.sectionAwaited = false;
1789
+ this.scopes.push();
1790
+ this.bindParams(decl);
1791
+ const tags = ts.getJSDocTags(decl);
1792
+ const opt = (tag) => {
1793
+ const v = jsdocTagText(tags, [tag]);
1794
+ return v !== undefined ? v : null;
1795
+ };
1796
+ const args = decl.parameters.map((p) => {
1797
+ const t = p.type ? skriptType(p.type, this.reg) : "text";
1798
+ const spec = `<${p.name.getText()}: ${t}>`;
1799
+ return p.questionToken ? `[${spec}]` : spec;
1800
+ }).join(" ");
1801
+ const path = name.replace(/^\//, "");
1802
+ this.emit(`command /${path}${args ? " " + args : ""}:`);
1803
+ this.indent++;
1804
+ const permission = opt("permission");
1805
+ if (permission)
1806
+ this.emit(`permission: ${permission}`);
1807
+ const usage = opt("usage");
1808
+ if (usage)
1809
+ this.emit(`usage: ${usage}`);
1810
+ const aliases = opt("aliases");
1811
+ if (aliases)
1812
+ this.emit(`aliases: ${aliases}`);
1813
+ this.emit("trigger:");
1814
+ this.indent++;
1815
+ this.body(decl.body?.statements ?? []);
1816
+ this.indent--;
1817
+ this.indent--;
1818
+ this.scopes.pop();
1819
+ }
1820
+ /** Bind function parameters as scope entries ({_name}, or {_oN} when
1821
+ * obfuscating). */
1822
+ bindParams(f) {
1823
+ for (const p of f.parameters) {
1824
+ const typeText = p.type?.getText() ?? "";
1825
+ const isList = typeText.endsWith("[]");
1826
+ const cat = this.reg.category(typeText);
1827
+ const key = this.obfuscate ? this.scopes.nextKey() : `_${p.name.getText()}`;
1828
+ this.scopes.declareWithKey(p.name.getText(), key, typeText, cat, isList);
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Prologue for functions with `object` parameters: the caller passed an
1833
+ * NBT compound, so re-zip it into the local keyed list. Body code then
1834
+ * reads {_p::key} as usual.
1835
+ */
1836
+ emitObjectParamPrologue(f) {
1837
+ for (const p of f.parameters) {
1838
+ if ((p.type?.getText() ?? "").toLowerCase() !== "object")
1839
+ continue;
1840
+ const name = p.name.getText();
1841
+ this.emit(`clear {_${name}::*}`);
1842
+ this.convert(`{_${name}}`, "nbt", "list", `_${name}`);
1843
+ }
1844
+ }
1845
+ /** `x.y = v`, dispatched on x's type. */
1846
+ emitAssignment(e) {
1847
+ const left = e.left;
1848
+ if (ts.isIdentifier(left)) {
1849
+ let v = this.scopes.lookup(left.text);
1850
+ // assigning to a name no let/const declared creates a global, like
1851
+ // JavaScript: {globalThis::<name>} (see ScopeStack.declareGlobal)
1852
+ if (!v) {
1853
+ const typeText = this.typeOfExpr(e.right);
1854
+ v = this.scopes.declareGlobal(left.text, typeText, this.reg.category(typeText), typeText.endsWith("[]"));
1855
+ }
1856
+ const key = this.ref(v);
1857
+ const rhs = e.right;
1858
+ if (ts.isObjectLiteralExpression(rhs) && (!v || v.category === "list" || v.category === "nbt")) {
1859
+ if (v && v.category === "nbt") {
1860
+ this.emit(`set {${v.key}} to ${this.nbtFromLiteral(rhs)}`);
1861
+ }
1862
+ else {
1863
+ this.emitObjectLiteral(rhs, v ? v.key : `_${left.text}`);
1864
+ }
1865
+ return;
1866
+ }
1867
+ const targetKey = v?.key ?? `_${left.text}`;
1868
+ if (this.isObjCall(rhs)) {
1869
+ // object-returning call: read its NBT compound result into the
1870
+ // target (see declarations)
1871
+ this.readObjectCall(rhs, targetKey, (v?.category ?? "list") === "nbt");
1872
+ return;
1873
+ }
1874
+ const value = this.expr(rhs);
1875
+ const from = this.categoryOfExpr(rhs);
1876
+ const to = v?.category ?? "plain";
1877
+ if (from === "list" && to !== "nbt") {
1878
+ // keyed copy (list assignment flattens indices, SkriptLang#3127)
1879
+ this.push(`loop ${this.listForm(value)}:`);
1880
+ this.pushAt(1, `set {${targetKey}::%loop-index%} to loop-value`);
1881
+ return;
1882
+ }
1883
+ const conv = this.convert(value, from, to, targetKey);
1884
+ if (conv !== null)
1885
+ this.emit(`set ${key} to ${conv}`);
1886
+ return;
1887
+ }
1888
+ if (ts.isPropertyAccessExpression(left)) {
1889
+ const receiverCat = this.categoryOfExpr(left.expression);
1890
+ const member = left.name.text;
1891
+ const receiver = this.expr(left.expression);
1892
+ const value = this.expr(e.right);
1893
+ // a @skript pattern on the property: setter form. Without %value%
1894
+ // the pattern is an expression and the value is assigned into it;
1895
+ // with %value% the pattern is the full setter effect.
1896
+ const pattern = this.memberPattern(left.expression, member);
1897
+ if (pattern) {
1898
+ const target = this.applyMemberPattern(pattern.replace(/set\s+/, ""), receiver, member, value);
1899
+ if (/%value%|%3%/.test(pattern)) {
1900
+ this.emit(this.applyMemberPattern(pattern, receiver, member, value));
1901
+ }
1902
+ else {
1903
+ this.emit(`set ${target} to ${value}`);
1904
+ }
1905
+ return;
1906
+ }
1907
+ switch (receiverCat) {
1908
+ case "nbt": {
1909
+ const kind = this.nbtTagKind(this.typeOfExpr(e.right));
1910
+ this.emit(`set ${kind} tag "${member}" of ${this.expr(left.expression)} to ${value}`);
1911
+ return;
1912
+ }
1913
+ case "java":
1914
+ this.emit(`set ${this.expr(left.expression)}.${member} to ${value}`);
1915
+ return;
1916
+ default:
1917
+ this.emit(`set ${this.indexRef(left.expression, member)} to ${value}`);
1918
+ return;
1919
+ }
1920
+ }
1921
+ this.emit(`set ${this.expr(left)} to ${this.expr(e.right)}`);
1922
+ }
1923
+ }
1924
+ // ---------------------------------------------------------------------------
1925
+ // Top level: functions + /** @on <event> */ triggers
1926
+ // ---------------------------------------------------------------------------
1927
+ function getEventName(decl) {
1928
+ const tag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "on");
1929
+ if (tag && typeof tag.comment === "string")
1930
+ return tag.comment.trim();
1931
+ return null;
1932
+ }
1933
+ /** The name/path of a `/** @command /name *\/` function, else null. */
1934
+ function getCommandName(decl) {
1935
+ const tag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "command");
1936
+ if (tag && typeof tag.comment === "string" && tag.comment.trim()) {
1937
+ return tag.comment.trim();
1938
+ }
1939
+ return null;
1940
+ }
1941
+ function transpile(source, scriptName = "generated", opts) {
1942
+ const sf = ts.createSourceFile(scriptName + ".ts", source, ts.ScriptTarget.Latest, true);
1943
+ const t = new Transpiler(sf, opts);
1944
+ const events = [];
1945
+ const commands = [];
1946
+ const funcs = [];
1947
+ const topLevel = [];
1948
+ for (const stmt of sf.statements) {
1949
+ if (ts.isFunctionDeclaration(stmt)) {
1950
+ // `declare function ...` = ambient declaration (native Skript/addon
1951
+ // function): skip it, don't emit a Skript function.
1952
+ if (!stmt.body)
1953
+ continue;
1954
+ // `@macro` functions are never emitted: they expand inline at each
1955
+ // call site (the registry holds their bodies)
1956
+ if (jsdocTagText(ts.getJSDocTags(stmt), ["macro"]) !== undefined)
1957
+ continue;
1958
+ const ev = getEventName(stmt);
1959
+ const cmd = getCommandName(stmt);
1960
+ if (cmd)
1961
+ commands.push([cmd, stmt]);
1962
+ else if (ev)
1963
+ events.push([ev, stmt]);
1964
+ else
1965
+ funcs.push(stmt);
1966
+ }
1967
+ else if (ts.isInterfaceDeclaration(stmt) || ts.isClassDeclaration(stmt) ||
1968
+ ts.isTypeAliasDeclaration(stmt) || ts.isImportDeclaration(stmt) ||
1969
+ ts.isImportEqualsDeclaration(stmt) || ts.isExportDeclaration(stmt)) {
1970
+ // Type-level declarations: consumed by the type registry, not emitted.
1971
+ continue;
1972
+ }
1973
+ else if (ts.isVariableStatement(stmt) &&
1974
+ stmt.declarationList.declarations.some((d) => (ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0)) {
1975
+ // ambient `declare const`: registered as a @skript macro, not emitted
1976
+ continue;
1977
+ }
1978
+ else {
1979
+ // any other top-level statement runs in the init trigger, in source
1980
+ // order (see below)
1981
+ topLevel.push(stmt);
1982
+ }
1983
+ }
1984
+ // obfuscation: functions defined in this file get a per-file id + index,
1985
+ // so same-named functions in different files never collide (like ES6
1986
+ // module symbols) and the original names are hidden in the output
1987
+ if (opts?.obfuscate) {
1988
+ const key = fileKey(source);
1989
+ funcs.forEach((f, i) => {
1990
+ if (f.name)
1991
+ t.fnMap.set(f.name.text, `${key}_f${i + 1}`);
1992
+ });
1993
+ }
1994
+ // Skript functions first, then event triggers
1995
+ for (const f of funcs) {
1996
+ t.emitFunction(f);
1997
+ t.emit("");
1998
+ }
1999
+ // Top-level statements run in their own `on load:` trigger, emitted first
2000
+ // so it precedes every user trigger (same-event triggers run in document
2001
+ // order): their top-level declarations bind as globalThis globals.
2002
+ if (topLevel.length > 0) {
2003
+ t.emitTopLevel(topLevel);
2004
+ t.emit("");
2005
+ }
2006
+ for (const [event, decl] of events) {
2007
+ t.emitEvent(event, decl);
2008
+ t.emit("");
2009
+ }
2010
+ // commands last: like triggers, they are not callable functions
2011
+ for (const [name, decl] of commands) {
2012
+ t.emitCommand(name, decl);
2013
+ t.emit("");
2014
+ }
2015
+ // NBT reads need the runtime tag helper (see TS2SK_TAG_HELPER)
2016
+ if (t.usedTagHelper)
2017
+ t.lines.unshift(...TS2SK_TAG_HELPER);
2018
+ // skript-Reflect needs the Java classes imported at the top of the script
2019
+ if (t.reg.usedClasslocs.size > 0) {
2020
+ const imports = ["import:", ...[...t.reg.usedClasslocs].sort().map((l) => `\t${l}`), ""];
2021
+ t.lines.unshift(...imports);
2022
+ }
2023
+ return t.lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
2024
+ }
2025
+ //# sourceMappingURL=transpiler.js.map