@triflare/fx 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +73 -2
- package/bin/fx.js +949 -0
- package/package.json +64 -4
- package/product.yml +28 -0
- package/src/brand.js +105 -0
- package/src/build.js +181 -0
- package/src/catalog.js +753 -0
- package/src/compiler.js +1658 -0
- package/src/config-edit.js +338 -0
- package/src/config.js +400 -0
- package/src/decompiler.js +504 -0
- package/src/dev.js +390 -0
- package/src/errors.js +56 -0
- package/src/extensions.js +707 -0
- package/src/index.js +31 -0
- package/src/language.js +12 -0
- package/src/lexer.js +202 -0
- package/src/log.js +70 -0
- package/src/package.js +202 -0
- package/src/packager.js +177 -0
- package/src/parser.js +608 -0
- package/src/product.js +27 -0
- package/src/project.js +156 -0
- package/src/sb3.js +340 -0
- package/src/sprite.js +81 -0
- package/vendor/pm-packager-dist/.fx-build.json +6 -0
- package/vendor/pm-packager-dist/LICENSE +202 -0
- package/vendor/pm-packager-dist/NOTICE +6 -0
- package/vendor/pm-packager-dist/dist/290e09e569a1cab8e61ba93b0d23863f.png +0 -0
- package/vendor/pm-packager-dist/dist/icns.js +1 -0
- package/vendor/pm-packager-dist/dist/packager.js +154 -0
- package/vendor/pm-packager-dist/dist/scaffolding/addons.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/extension worker.js +2 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-full.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/scaffolding-min.js +93 -0
- package/vendor/pm-packager-dist/dist/scaffolding/vendors~iframe-extension-worker.js +2 -0
- package/vendor/pm-packager-dist/dist/vendors~sha256.js +3 -0
- package/vendor/pm-packager-dist/package.json +3 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triflare Fx decompiler: Scratch 3 / PenguinMod project JSON -> `.tfx` source.
|
|
3
|
+
*
|
|
4
|
+
* This is the inverse of `compiler.js`. It leans on the same catalog (reverse
|
|
5
|
+
* opcode lookup) so the two stay in sync.
|
|
6
|
+
*/
|
|
7
|
+
import { CATALOG, BY_OPCODE } from "./catalog.js";
|
|
8
|
+
import { unzipSync } from "./sb3.js";
|
|
9
|
+
|
|
10
|
+
const MENU_OPCODES = new Set([
|
|
11
|
+
"motion_goto_menu",
|
|
12
|
+
"motion_glideto_menu",
|
|
13
|
+
"motion_pointtowards_menu",
|
|
14
|
+
"sensing_touchingobjectmenu",
|
|
15
|
+
"sensing_distancetomenu",
|
|
16
|
+
"sensing_keyoptions",
|
|
17
|
+
"looks_costume",
|
|
18
|
+
"looks_backdrops",
|
|
19
|
+
"sound_sounds_menu",
|
|
20
|
+
"control_create_clone_of_menu",
|
|
21
|
+
"sensing_of_object_menu",
|
|
22
|
+
"pen_menu_colorParam",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const BINARY_OPCODES = {
|
|
26
|
+
operator_add: "+",
|
|
27
|
+
operator_subtract: "-",
|
|
28
|
+
operator_multiply: "*",
|
|
29
|
+
operator_divide: "/",
|
|
30
|
+
operator_mod: "%",
|
|
31
|
+
operator_gt: ">",
|
|
32
|
+
operator_lt: "<",
|
|
33
|
+
operator_equals: "==",
|
|
34
|
+
operator_and: "and",
|
|
35
|
+
operator_or: "or",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const LIST_OPCODE_METHOD = {
|
|
39
|
+
data_addtolist: { method: "push", inputs: ["ITEM"] },
|
|
40
|
+
data_insertatlist: { method: "insert", inputs: ["INDEX", "ITEM"] },
|
|
41
|
+
data_replaceitemoflist: { method: "replace", inputs: ["INDEX", "ITEM"] },
|
|
42
|
+
data_deleteoflist: { method: "deleteAt", inputs: ["INDEX"] },
|
|
43
|
+
data_deletealloflist: { method: "deleteAll", inputs: [] },
|
|
44
|
+
data_showlist: { method: "show", inputs: [] },
|
|
45
|
+
data_hidelist: { method: "hide", inputs: [] },
|
|
46
|
+
data_itemoflist: { method: "itemAt", inputs: ["INDEX"] },
|
|
47
|
+
data_itemnumoflist: { method: "itemNumber", inputs: ["ITEM"] },
|
|
48
|
+
data_lengthoflist: { method: "length", inputs: [] },
|
|
49
|
+
data_listcontainsitem: { method: "contains", inputs: ["ITEM"] },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function escapeString(value) {
|
|
53
|
+
return String(value)
|
|
54
|
+
.replace(/\\/g, "\\\\")
|
|
55
|
+
.replace(/"/g, '\\"')
|
|
56
|
+
.replace(/\n/g, "\\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function numberText(value) {
|
|
60
|
+
const n = Number(value);
|
|
61
|
+
if (Number.isFinite(n) && String(n) === String(value)) return String(value);
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class Decompiler {
|
|
66
|
+
constructor(project, options = {}) {
|
|
67
|
+
this.project = project;
|
|
68
|
+
this.assetPrefix = options.assetPrefix || "";
|
|
69
|
+
this.returningProccodes = new Set();
|
|
70
|
+
for (const target of project.targets) {
|
|
71
|
+
for (const block of Object.values(target.blocks)) {
|
|
72
|
+
if (
|
|
73
|
+
block.opcode === "procedures_call" &&
|
|
74
|
+
block.mutation &&
|
|
75
|
+
(block.mutation.returns === "true" || block.mutation.return)
|
|
76
|
+
) {
|
|
77
|
+
this.returningProccodes.add(block.mutation.proccode);
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
block.opcode === "procedures_definition" ||
|
|
81
|
+
block.opcode === "procedures_definition_return"
|
|
82
|
+
) {
|
|
83
|
+
const proto = target.blocks[firstOf(block.inputs.custom_block)];
|
|
84
|
+
const proccode = proto && proto.mutation && proto.mutation.proccode;
|
|
85
|
+
if (
|
|
86
|
+
proccode &&
|
|
87
|
+
(block.opcode === "procedures_definition_return" ||
|
|
88
|
+
(proto.mutation && proto.mutation.returns === "true") ||
|
|
89
|
+
stackHasReturn(target, block.next))
|
|
90
|
+
)
|
|
91
|
+
this.returningProccodes.add(proccode);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
targetVars(target) {
|
|
98
|
+
const map = new Map();
|
|
99
|
+
for (const [id, desc] of Object.entries(target.variables || {}))
|
|
100
|
+
map.set(id, desc[0]);
|
|
101
|
+
for (const [id, desc] of Object.entries(target.lists || {}))
|
|
102
|
+
map.set(id, desc[0]);
|
|
103
|
+
return map;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
renderExpression(value) {
|
|
107
|
+
if (value == null) return '""';
|
|
108
|
+
if (!Array.isArray(value)) return this.renderBlock(value);
|
|
109
|
+
const type = value[0];
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 1:
|
|
112
|
+
case 3:
|
|
113
|
+
return this.renderInputRef(value[1]);
|
|
114
|
+
case 2:
|
|
115
|
+
return this.renderInputRef(value[1]);
|
|
116
|
+
case 4:
|
|
117
|
+
case 5:
|
|
118
|
+
case 6:
|
|
119
|
+
case 7:
|
|
120
|
+
return numberText(value[1]);
|
|
121
|
+
case 8:
|
|
122
|
+
return numberText(value[1]);
|
|
123
|
+
case 9:
|
|
124
|
+
return `"${escapeString(value[1])}"`;
|
|
125
|
+
case 10:
|
|
126
|
+
return `"${escapeString(value[1])}"`;
|
|
127
|
+
case 11:
|
|
128
|
+
return `"${escapeString(value[1])}"`;
|
|
129
|
+
case 12:
|
|
130
|
+
return String(value[1]);
|
|
131
|
+
case 13:
|
|
132
|
+
return String(value[1]);
|
|
133
|
+
default:
|
|
134
|
+
return "0";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
renderInputRef(ref) {
|
|
139
|
+
if (Array.isArray(ref)) return this.renderExpression(ref);
|
|
140
|
+
if (ref == null) return "0";
|
|
141
|
+
return this.renderBlock(ref);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
renderBlock(id) {
|
|
145
|
+
const block = this.target.blocks[id];
|
|
146
|
+
if (!block) return "0";
|
|
147
|
+
|
|
148
|
+
if (MENU_OPCODES.has(block.opcode)) {
|
|
149
|
+
const field = Object.values(block.fields || {})[0];
|
|
150
|
+
return field ? `"${escapeString(field[0])}"` : '""';
|
|
151
|
+
}
|
|
152
|
+
if (block.opcode === "data_variable") return block.fields.VARIABLE[0];
|
|
153
|
+
if (block.opcode === "data_listcontents") return block.fields.LIST[0];
|
|
154
|
+
if (
|
|
155
|
+
Object.prototype.hasOwnProperty.call(LIST_OPCODE_METHOD, block.opcode)
|
|
156
|
+
) {
|
|
157
|
+
const { method, inputs } = LIST_OPCODE_METHOD[block.opcode];
|
|
158
|
+
const listName = block.fields.LIST[0];
|
|
159
|
+
const args = inputs.map((input) =>
|
|
160
|
+
this.renderExpression(block.inputs[input]),
|
|
161
|
+
);
|
|
162
|
+
return `${listName}.${method}(${args.join(", ")})`;
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
block.opcode === "argument_reporter_string_number" ||
|
|
166
|
+
block.opcode === "argument_reporter_boolean"
|
|
167
|
+
) {
|
|
168
|
+
const value = block.fields.VALUE ? block.fields.VALUE[0] : "";
|
|
169
|
+
// TurboWarp/PenguinMod encode a few "PenguinMod" reporters as modified
|
|
170
|
+
// argument reporters; see docs.turbowarp.org/blocks and the VM compiler.
|
|
171
|
+
const magic = {
|
|
172
|
+
"last key pressed": "pm.lastKeyPressed()",
|
|
173
|
+
"is compiled?": "pm.isCompiled()",
|
|
174
|
+
"is penguinmod?": "pm.isPenguinMod()",
|
|
175
|
+
}[value];
|
|
176
|
+
return magic || value;
|
|
177
|
+
}
|
|
178
|
+
if (block.opcode === "procedures_call") {
|
|
179
|
+
return this.renderProcedureCall(block);
|
|
180
|
+
}
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(BINARY_OPCODES, block.opcode)) {
|
|
182
|
+
const left = this.renderExpression(
|
|
183
|
+
block.inputs.OPERAND1 || block.inputs.NUM1,
|
|
184
|
+
);
|
|
185
|
+
const right = this.renderExpression(
|
|
186
|
+
block.inputs.OPERAND2 || block.inputs.NUM2,
|
|
187
|
+
);
|
|
188
|
+
return `(${left} ${BINARY_OPCODES[block.opcode]} ${right})`;
|
|
189
|
+
}
|
|
190
|
+
if (block.opcode === "operator_not") {
|
|
191
|
+
return `(not ${this.renderExpression(block.inputs.OPERAND)})`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const name = BY_OPCODE.get(block.opcode);
|
|
195
|
+
if (name) return this.renderCatalogCall(name, block);
|
|
196
|
+
return `/* unknown ${block.opcode} */`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
renderCatalogCall(name, block) {
|
|
200
|
+
const entry = CATALOG[name];
|
|
201
|
+
const args = [];
|
|
202
|
+
for (const key of Object.keys(entry.fields || {})) {
|
|
203
|
+
const field = block.fields[key];
|
|
204
|
+
if (field) args.push(`"${escapeString(field[0])}"`);
|
|
205
|
+
}
|
|
206
|
+
for (const [inputName, spec] of Object.entries(entry.inputs || {})) {
|
|
207
|
+
if (spec.type === "substack") continue;
|
|
208
|
+
args.push(this.renderExpression(block.inputs[inputName]));
|
|
209
|
+
}
|
|
210
|
+
return `${name}(${args.join(", ")})`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
renderProcedureCall(block) {
|
|
214
|
+
const mutation = block.mutation || {};
|
|
215
|
+
const proccode = mutation.proccode || "";
|
|
216
|
+
const name = proccode.split(" ")[0];
|
|
217
|
+
const ids = mutation.argumentids ? JSON.parse(mutation.argumentids) : [];
|
|
218
|
+
const args = ids.map((argId) => this.renderExpression(block.inputs[argId]));
|
|
219
|
+
return `${name}(${args.join(", ")})`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------- statements
|
|
223
|
+
|
|
224
|
+
renderStack(firstId, indent) {
|
|
225
|
+
const lines = [];
|
|
226
|
+
let id = firstId;
|
|
227
|
+
while (id) {
|
|
228
|
+
const block = this.target.blocks[id];
|
|
229
|
+
if (!block) break;
|
|
230
|
+
lines.push(this.renderStatement(block, indent));
|
|
231
|
+
id = block.next;
|
|
232
|
+
}
|
|
233
|
+
return lines.filter(Boolean).join("\n");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
renderStatement(block, indent) {
|
|
237
|
+
const pad = " ".repeat(indent);
|
|
238
|
+
const sub = (firstId) => {
|
|
239
|
+
if (!firstId) return "";
|
|
240
|
+
const body = this.renderStack(firstId, indent + 1);
|
|
241
|
+
return body ? `\n${body}` : "";
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
switch (block.opcode) {
|
|
245
|
+
case "data_setvariableto":
|
|
246
|
+
return `${pad}${block.fields.VARIABLE[0]} = ${this.renderExpression(block.inputs.VALUE)};`;
|
|
247
|
+
case "data_changevariableby":
|
|
248
|
+
return `${pad}${block.fields.VARIABLE[0]} += ${this.renderExpression(block.inputs.VALUE)};`;
|
|
249
|
+
case "control_if": {
|
|
250
|
+
const cond = this.renderExpression(block.inputs.CONDITION);
|
|
251
|
+
return `${pad}if (${stripParen(cond)}) {${sub(firstOf(block.inputs.SUBSTACK))}\n${pad}}`;
|
|
252
|
+
}
|
|
253
|
+
case "control_if_else": {
|
|
254
|
+
const cond = this.renderExpression(block.inputs.CONDITION);
|
|
255
|
+
return `${pad}if (${stripParen(cond)}) {${sub(firstOf(block.inputs.SUBSTACK))}\n${pad}} else {${sub(firstOf(block.inputs.SUBSTACK2))}\n${pad}}`;
|
|
256
|
+
}
|
|
257
|
+
case "control_forever":
|
|
258
|
+
return `${pad}control.forever {${sub(firstOf(block.inputs.SUBSTACK))}\n${pad}}`;
|
|
259
|
+
case "control_repeat":
|
|
260
|
+
return `${pad}repeat (${this.renderExpression(block.inputs.TIMES)}) {${sub(firstOf(block.inputs.SUBSTACK))}\n${pad}}`;
|
|
261
|
+
case "control_repeat_until":
|
|
262
|
+
return `${pad}control.repeatUntil(${stripParen(this.renderExpression(block.inputs.CONDITION))}) {${sub(firstOf(block.inputs.SUBSTACK))}\n${pad}}`;
|
|
263
|
+
case "control_wait_until":
|
|
264
|
+
return `${pad}control.waitUntil(${stripParen(this.renderExpression(block.inputs.CONDITION))});`;
|
|
265
|
+
case "procedures_return":
|
|
266
|
+
return `${pad}return ${this.renderExpression(block.inputs.return ?? block.inputs.VALUE)};`;
|
|
267
|
+
case "procedures_call":
|
|
268
|
+
return `${pad}${this.renderProcedureCall(block)};`;
|
|
269
|
+
case "control_stop":
|
|
270
|
+
return `${pad}control.stop("${block.fields.STOP_OPTION[0]}");`;
|
|
271
|
+
default: {
|
|
272
|
+
if (
|
|
273
|
+
Object.prototype.hasOwnProperty.call(LIST_OPCODE_METHOD, block.opcode)
|
|
274
|
+
) {
|
|
275
|
+
const { method, inputs } = LIST_OPCODE_METHOD[block.opcode];
|
|
276
|
+
const listName = block.fields.LIST[0];
|
|
277
|
+
const args = inputs.map((input) =>
|
|
278
|
+
this.renderExpression(block.inputs[input]),
|
|
279
|
+
);
|
|
280
|
+
return `${pad}${listName}.${method}(${args.join(", ")});`;
|
|
281
|
+
}
|
|
282
|
+
const name = BY_OPCODE.get(block.opcode);
|
|
283
|
+
if (name && CATALOG[name].kind === "command") {
|
|
284
|
+
return `${pad}${this.renderCatalogCall(name, block)};`;
|
|
285
|
+
}
|
|
286
|
+
if (name) return `${pad}${this.renderCatalogCall(name, block)};`;
|
|
287
|
+
return `${pad}// unsupported block ${block.opcode}`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
renderHat(block, indent) {
|
|
293
|
+
const pad = "";
|
|
294
|
+
void indent;
|
|
295
|
+
const body = (firstId) => this.renderStack(firstId, 1);
|
|
296
|
+
switch (block.opcode) {
|
|
297
|
+
case "event_whenflagclicked":
|
|
298
|
+
return `${pad}when greenFlag {\n${body(block.next)}\n}`;
|
|
299
|
+
case "event_whenkeypressed": {
|
|
300
|
+
const key = block.fields.KEY_OPTION
|
|
301
|
+
? block.fields.KEY_OPTION[0]
|
|
302
|
+
: "space";
|
|
303
|
+
return `${pad}when keyPressed("${escapeString(key)}") {\n${body(block.next)}\n}`;
|
|
304
|
+
}
|
|
305
|
+
case "event_whenthisspriteclicked":
|
|
306
|
+
return `${pad}when spriteClicked {\n${body(block.next)}\n}`;
|
|
307
|
+
case "event_whenstageclicked":
|
|
308
|
+
return `${pad}when stageClicked {\n${body(block.next)}\n}`;
|
|
309
|
+
case "control_start_as_clone":
|
|
310
|
+
return `${pad}when cloneStart {\n${body(block.next)}\n}`;
|
|
311
|
+
case "event_whenbroadcastreceived":
|
|
312
|
+
return `${pad}when broadcast("${escapeString(block.fields.BROADCAST_OPTION[0])}") {\n${body(block.next)}\n}`;
|
|
313
|
+
case "event_whenbackdropswitchesto":
|
|
314
|
+
return `${pad}when backdropSwitchesTo("${escapeString(block.fields.BACKDROP[0])}") {\n${body(block.next)}\n}`;
|
|
315
|
+
case "event_whengreaterthan": {
|
|
316
|
+
const option = String(
|
|
317
|
+
(block.fields.WHENGREATERTHANMENU || ["LOUDNESS"])[0],
|
|
318
|
+
).toLowerCase();
|
|
319
|
+
const kind =
|
|
320
|
+
option === "timer" ? "timerGreaterThan" : "loudnessGreaterThan";
|
|
321
|
+
return `${pad}when ${kind}(${this.renderExpression(block.inputs.VALUE)}) {\n${body(block.next)}\n}`;
|
|
322
|
+
}
|
|
323
|
+
default:
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
renderFunction(defBlock) {
|
|
329
|
+
const protoId = firstOf(defBlock.inputs.custom_block);
|
|
330
|
+
const proto = this.target.blocks[protoId];
|
|
331
|
+
const mutation = (proto && proto.mutation) || {};
|
|
332
|
+
const proccode = mutation.proccode || "procedure";
|
|
333
|
+
const names = mutation.argumentnames
|
|
334
|
+
? JSON.parse(mutation.argumentnames)
|
|
335
|
+
: [];
|
|
336
|
+
const baseName = proccode.split(" ")[0];
|
|
337
|
+
const returning = this.returningProccodes.has(proccode) ? "returning " : "";
|
|
338
|
+
const params = names.join(", ");
|
|
339
|
+
return `${returning}function ${baseName}(${params}) {\n${this.renderStack(defBlock.next, 1)}\n}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
decompileTarget(target, indent = 0) {
|
|
343
|
+
const previous = this.target;
|
|
344
|
+
this.target = target;
|
|
345
|
+
const pad = " ".repeat(indent);
|
|
346
|
+
const sections = [];
|
|
347
|
+
|
|
348
|
+
// costumes / sounds
|
|
349
|
+
const costumeEntries = (target.costumes || [])
|
|
350
|
+
.map(
|
|
351
|
+
(c) => ` ${safeKey(c.name)}: "${this.assetPrefix}${c.md5ext}"`,
|
|
352
|
+
)
|
|
353
|
+
.join(",\n");
|
|
354
|
+
const soundEntries = (target.sounds || [])
|
|
355
|
+
.map(
|
|
356
|
+
(s) => ` ${safeKey(s.name)}: "${this.assetPrefix}${s.md5ext}"`,
|
|
357
|
+
)
|
|
358
|
+
.join(",\n");
|
|
359
|
+
if (costumeEntries)
|
|
360
|
+
sections.push(`${pad}costumes: {\n${costumeEntries}\n${pad}};`);
|
|
361
|
+
if (soundEntries)
|
|
362
|
+
sections.push(`${pad}sounds: {\n${soundEntries}\n${pad}};`);
|
|
363
|
+
if (target.layerOrder != null && !target.isStage)
|
|
364
|
+
sections.push(`${pad}layer: ${target.layerOrder};`);
|
|
365
|
+
|
|
366
|
+
// variables + lists
|
|
367
|
+
for (const [, desc] of Object.entries(target.variables || {})) {
|
|
368
|
+
const cloud = desc[2] ? "cloud " : "";
|
|
369
|
+
sections.push(`${pad}${cloud}let ${desc[0]} = ${literalText(desc[1])};`);
|
|
370
|
+
}
|
|
371
|
+
for (const [, desc] of Object.entries(target.lists || {})) {
|
|
372
|
+
const items = (desc[1] || []).map((v) => literalText(v)).join(", ");
|
|
373
|
+
sections.push(`${pad}list ${desc[0]} = [${items}];`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// blocks
|
|
377
|
+
const blockIds = Object.keys(target.blocks || {});
|
|
378
|
+
const hats = blockIds.filter((id) => {
|
|
379
|
+
const b = target.blocks[id];
|
|
380
|
+
return (
|
|
381
|
+
b.topLevel &&
|
|
382
|
+
b.opcode !== "procedures_definition" &&
|
|
383
|
+
b.opcode !== "procedures_definition_return"
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
const defs = blockIds.filter((id) => {
|
|
387
|
+
const opcode = target.blocks[id].opcode;
|
|
388
|
+
return (
|
|
389
|
+
opcode === "procedures_definition" ||
|
|
390
|
+
opcode === "procedures_definition_return"
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
for (const id of defs) {
|
|
395
|
+
sections.push(indentText(this.renderFunction(target.blocks[id]), pad));
|
|
396
|
+
}
|
|
397
|
+
for (const id of hats) {
|
|
398
|
+
const rendered = this.renderHat(target.blocks[id], indent);
|
|
399
|
+
if (rendered) sections.push(indentText(rendered, pad));
|
|
400
|
+
else
|
|
401
|
+
sections.push(`${pad}// unsupported block ${target.blocks[id].opcode}`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.target = previous;
|
|
405
|
+
return sections.join("\n\n");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
decompile() {
|
|
409
|
+
const out = [];
|
|
410
|
+
const stage = this.project.targets.find((t) => t.isStage);
|
|
411
|
+
const sprites = this.project.targets.filter((t) => !t.isStage);
|
|
412
|
+
|
|
413
|
+
if (stage) {
|
|
414
|
+
const body = this.decompileTarget(stage);
|
|
415
|
+
if (body.trim()) out.push(body);
|
|
416
|
+
}
|
|
417
|
+
for (const sprite of sprites) {
|
|
418
|
+
out.push(
|
|
419
|
+
`sprite "${escapeString(sprite.name)}" {\n${this.decompileTarget(sprite, 1)}\n}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return `${out.join("\n\n")}\n`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function firstOf(input) {
|
|
427
|
+
if (!input) return null;
|
|
428
|
+
if (Array.isArray(input))
|
|
429
|
+
return typeof input[1] === "string" ? input[1] : null;
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Walk a statement stack (including substacks) looking for `return`. */
|
|
434
|
+
function stackHasReturn(target, firstId) {
|
|
435
|
+
let id = firstId;
|
|
436
|
+
while (id) {
|
|
437
|
+
const block = target.blocks[id];
|
|
438
|
+
if (!block) break;
|
|
439
|
+
if (block.opcode === "procedures_return") return true;
|
|
440
|
+
for (const [name, input] of Object.entries(block.inputs || {})) {
|
|
441
|
+
if (!name.startsWith("SUBSTACK")) continue;
|
|
442
|
+
const child = firstOf(input);
|
|
443
|
+
if (child && stackHasReturn(target, child)) return true;
|
|
444
|
+
}
|
|
445
|
+
id = block.next;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function stripParen(text) {
|
|
451
|
+
return text;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function literalText(value) {
|
|
455
|
+
if (typeof value === "number") return String(value);
|
|
456
|
+
if (value === true || value === false) return String(value);
|
|
457
|
+
return `"${escapeString(value)}"`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function safeKey(name) {
|
|
461
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)
|
|
462
|
+
? name
|
|
463
|
+
: `"${escapeString(name)}"`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function indentText(text, prefix) {
|
|
467
|
+
if (!text) return "";
|
|
468
|
+
return text
|
|
469
|
+
.split("\n")
|
|
470
|
+
.map((line) => `${prefix}${line}`)
|
|
471
|
+
.join("\n");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Decompile project JSON into Triflare Fx source.
|
|
476
|
+
* @param {object} project
|
|
477
|
+
* @param {{assetPrefix?: string}} [options]
|
|
478
|
+
* @returns {string}
|
|
479
|
+
*/
|
|
480
|
+
export function decompileProject(project, options) {
|
|
481
|
+
return new Decompiler(project, options).decompile();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Decompile a `.pmp`/`.sb3` buffer (or project JSON) into Triflare Fx source.
|
|
486
|
+
* @param {Buffer|Uint8Array|object} input
|
|
487
|
+
* @param {{assetPrefix?: string}} [options]
|
|
488
|
+
* @returns {string}
|
|
489
|
+
*/
|
|
490
|
+
export function decompile(input, options) {
|
|
491
|
+
if (
|
|
492
|
+
input &&
|
|
493
|
+
typeof input === "object" &&
|
|
494
|
+
!Buffer.isBuffer(input) &&
|
|
495
|
+
!(input instanceof Uint8Array) &&
|
|
496
|
+
input.targets
|
|
497
|
+
) {
|
|
498
|
+
return decompileProject(input, options);
|
|
499
|
+
}
|
|
500
|
+
const files = unzipSync(input);
|
|
501
|
+
const json = files.get("project.json");
|
|
502
|
+
if (!json) throw new Error("Not a Scratch project: missing project.json");
|
|
503
|
+
return decompileProject(JSON.parse(json.toString("utf8")), options);
|
|
504
|
+
}
|