@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.
- package/LICENSE +373 -0
- package/README.md +348 -2
- package/build/cli.d.ts +2 -0
- package/build/cli.js +256 -0
- package/build/cli.js.map +1 -0
- package/build/fetch.d.ts +294 -0
- package/build/fetch.js +1221 -0
- package/build/fetch.js.map +1 -0
- package/build/fetchgit.d.ts +67 -0
- package/build/fetchgit.js +612 -0
- package/build/fetchgit.js.map +1 -0
- package/build/transpiler.d.ts +13 -0
- package/build/transpiler.js +2025 -0
- package/build/transpiler.js.map +1 -0
- package/package.json +38 -4
|
@@ -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
|