@triflare/fx 2.0.0 → 2.1.0-beta.2

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/src/vm.js ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Headless PenguinMod VM adapter.
3
+ *
4
+ * `fx test` runs projects against the real PenguinMod runtime. The VM is a
5
+ * browser module that touches a handful of DOM globals at import time, so this
6
+ * module provides minimal in-memory stand-ins for storage and rendering and
7
+ * requires the VM lazily. Loading a project never draws anything, so the
8
+ * no-op renderer is enough to deserialize targets, costumes, and blocks.
9
+ *
10
+ * The VM is shipped as a runtime dependency: `fx test` reports a clear
11
+ * error when it is not installed instead of failing at import time.
12
+ */
13
+ import crypto from "node:crypto";
14
+ import { createRequire } from "node:module";
15
+ import { runInThisContext } from "node:vm";
16
+
17
+ let modules = null;
18
+
19
+ /** Blob URLs the VM created for project extension sources, keyed by URL. */
20
+ const extensionBlobs = new Map();
21
+
22
+ /** Install the DOM globals the VM reads while it is being imported. */
23
+ function installGlobals() {
24
+ globalThis.location ||= {
25
+ search: "",
26
+ href: "http://localhost/",
27
+ origin: "http://localhost",
28
+ protocol: "http:",
29
+ host: "localhost",
30
+ hostname: "localhost",
31
+ pathname: "/",
32
+ hash: "",
33
+ };
34
+ globalThis.DOMParser ||= class {
35
+ parseFromString() {
36
+ const empty = () => [];
37
+ return {
38
+ querySelectorAll: empty,
39
+ getElementsByTagName: empty,
40
+ querySelector: () => null,
41
+ };
42
+ }
43
+ };
44
+ globalThis.window ||= globalThis;
45
+ globalThis.self ||= globalThis;
46
+
47
+ // The VM loads a project extension like a page does: fetch its URL, wrap the
48
+ // result in a Blob, read that Blob with FileReader, then append a script.
49
+ // These shims keep that flow working without a DOM.
50
+ globalThis.FileReader ||= class {
51
+ readAsText(blob) {
52
+ Promise.resolve(blob.text()).then(
53
+ (result) => this.onload?.({ target: { result } }),
54
+ (error) => this.onerror?.(error),
55
+ );
56
+ }
57
+ };
58
+ if (!globalThis.URL.createObjectURL.__fxHeadless) {
59
+ const createObjectURL = globalThis.URL.createObjectURL.bind(globalThis.URL);
60
+ globalThis.URL.createObjectURL = (blob) => {
61
+ const url = createObjectURL(blob);
62
+ extensionBlobs.set(url, blob);
63
+ return url;
64
+ };
65
+ globalThis.URL.createObjectURL.__fxHeadless = true;
66
+ }
67
+
68
+ /** Run an appended `<script>` whose `src` is one of the VM's Blob URLs. */
69
+ const runScript = (node) => {
70
+ const blob = extensionBlobs.get(node.src);
71
+ if (!blob) {
72
+ node.onerror?.(new Error(`Cannot read extension script ${node.src}`));
73
+ return;
74
+ }
75
+ Promise.resolve(blob.text()).then(
76
+ (source) => {
77
+ try {
78
+ // The unsandboxed loader installs `global.Scratch` before appending
79
+ // the script, so evaluating here matches a browser script tag.
80
+ runInThisContext(source, { filename: node.src });
81
+ node.onload?.();
82
+ } catch (error) {
83
+ node.onerror?.(error);
84
+ }
85
+ },
86
+ (error) => node.onerror?.(error),
87
+ );
88
+ };
89
+
90
+ globalThis.document ||= {
91
+ createElement: (tagName) => ({
92
+ tagName: String(tagName).toUpperCase(),
93
+ style: {},
94
+ getContext: () => null,
95
+ appendChild() {},
96
+ setAttribute() {},
97
+ addEventListener() {},
98
+ remove() {},
99
+ }),
100
+ body: {
101
+ appendChild(node) {
102
+ if (node?.tagName === "SCRIPT") runScript(node);
103
+ },
104
+ },
105
+ querySelector: () => null,
106
+ addEventListener() {},
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Load `scratch-vm` on first use.
112
+ * @returns {{VirtualMachine: any, centralDispatch: any}}
113
+ */
114
+ export function loadVmModules() {
115
+ if (modules) return modules;
116
+ installGlobals();
117
+ const require = createRequire(import.meta.url);
118
+ let VirtualMachine;
119
+ let centralDispatch;
120
+ try {
121
+ VirtualMachine = require("scratch-vm");
122
+ centralDispatch = require("scratch-vm/src/dispatch/central-dispatch");
123
+ } catch (error) {
124
+ throw new Error(
125
+ "Running tests needs the PenguinMod VM, which could not be loaded. " +
126
+ "Reinstall @triflare/fx with its runtime dependencies and try again.",
127
+ { cause: error },
128
+ );
129
+ }
130
+ modules = { VirtualMachine, centralDispatch };
131
+ return modules;
132
+ }
133
+
134
+ const AssetType = {
135
+ ImageVector: { name: "ImageVector", runtimeFormat: "svg" },
136
+ ImageBitmap: { name: "ImageBitmap", runtimeFormat: "png" },
137
+ Sound: { name: "Sound", runtimeFormat: "wav" },
138
+ };
139
+
140
+ const DataFormat = {
141
+ SVG: "svg",
142
+ PNG: "png",
143
+ JPG: "jpg",
144
+ BMP: "bmp",
145
+ GIF: "gif",
146
+ WAV: "wav",
147
+ MP3: "mp3",
148
+ OGG: "ogg",
149
+ FLAC: "flac",
150
+ };
151
+
152
+ const md5 = (data) =>
153
+ crypto.createHash("md5").update(Buffer.from(data)).digest("hex");
154
+
155
+ /** In-memory storage module so the VM can load assets headlessly. */
156
+ export function createStorage() {
157
+ return {
158
+ AssetType,
159
+ DataFormat,
160
+ createAsset(assetType, dataFormat, data, assetId, generateId) {
161
+ const bytes =
162
+ data instanceof Uint8Array ? data : new Uint8Array(data ?? []);
163
+ return {
164
+ assetType,
165
+ dataFormat,
166
+ data: bytes,
167
+ assetId: assetId ?? (generateId ? md5(bytes) : null),
168
+ clean: true,
169
+ encodeDataURI() {
170
+ const base64 = Buffer.from(this.data).toString("base64");
171
+ return `data:${dataFormat};base64,${base64}`;
172
+ },
173
+ decodeText() {
174
+ return Buffer.from(this.data).toString("utf8");
175
+ },
176
+ encodeTextData(text) {
177
+ this.data = new Uint8Array(Buffer.from(text, "utf8"));
178
+ this.assetId = md5(this.data);
179
+ },
180
+ };
181
+ },
182
+ };
183
+ }
184
+
185
+ /** No-op renderer: enough state for blocks that inspect or create skins. */
186
+ export function createRenderer() {
187
+ let nextId = 0;
188
+ const skins = {};
189
+ const createSkin = (size = [0, 0]) => {
190
+ const skinId = nextId++;
191
+ skins[skinId] = {
192
+ id: skinId,
193
+ renderQuality: 1,
194
+ _size: size,
195
+ _setTexture() {},
196
+ };
197
+ return skinId;
198
+ };
199
+ return {
200
+ _allSkins: skins,
201
+ _groupOrdering: [],
202
+ useHighQualityRender: true,
203
+ setLayerGroupOrdering() {},
204
+ setPrivateSkinAccess() {},
205
+ setUseHighQualityRender() {},
206
+ setRenderOffscreen() {},
207
+ setStageSize() {},
208
+ draw() {},
209
+ markDrawableAsNoninteractive() {},
210
+ createDrawable() {
211
+ return nextId++;
212
+ },
213
+ destroyDrawable() {},
214
+ updateDrawableSkinId() {},
215
+ updateDrawablePosition() {},
216
+ updateDrawableDirectionScale() {},
217
+ updateDrawableVisible() {},
218
+ updateDrawableEffect() {},
219
+ setDrawableOrder() {},
220
+ getDrawableOrder() {
221
+ return 0;
222
+ },
223
+ getFencedPositionOfDrawable(id, position) {
224
+ return position;
225
+ },
226
+ getBounds() {
227
+ return { left: 0, right: 0, top: 0, bottom: 0 };
228
+ },
229
+ getBoundsForBubble() {
230
+ return { left: 0, right: 0, top: 0, bottom: 0 };
231
+ },
232
+ isTouchingDrawables() {
233
+ return false;
234
+ },
235
+ drawableTouching() {
236
+ return false;
237
+ },
238
+ getTouchingDrawablesPoint() {
239
+ return [];
240
+ },
241
+ isTouchingColor() {
242
+ return false;
243
+ },
244
+ createSVGSkin() {
245
+ return createSkin();
246
+ },
247
+ createBitmapSkin() {
248
+ return createSkin();
249
+ },
250
+ createPenSkin() {
251
+ return createSkin();
252
+ },
253
+ getSkinSize(skinId) {
254
+ return skins[skinId] ? skins[skinId]._size : [0, 0];
255
+ },
256
+ getSkinRotationCenter() {
257
+ return [0, 0];
258
+ },
259
+ penClear() {},
260
+ penLine() {},
261
+ penPoint() {},
262
+ penStamp() {},
263
+ penTranslate() {},
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Create a headless VM with in-memory storage and a no-op renderer.
269
+ *
270
+ * Project extensions are plain JavaScript, and the only way to run them here is
271
+ * unsandboxed in this Node process: loading one is equivalent to running the
272
+ * project's code with full filesystem and network access. Nothing is loaded
273
+ * unless `allowProjectExtensions` is set. Callers opt in explicitly (for `fx
274
+ * test` that means the user passes `--allow-project-extensions`) to state that
275
+ * the project's extensions are trusted for this run.
276
+ *
277
+ * The dispatch broker is a process-wide singleton; dropping its services keeps
278
+ * one VM's extensions from logging a provider conflict for the next.
279
+ *
280
+ * @param {{allowProjectExtensions?: boolean}} [options]
281
+ */
282
+ export function createVm({ allowProjectExtensions = false } = {}) {
283
+ const { VirtualMachine, centralDispatch } = loadVmModules();
284
+ centralDispatch.services = {};
285
+ const vm = new VirtualMachine();
286
+ vm.attachStorage(createStorage());
287
+ vm.attachRenderer(createRenderer());
288
+ // Denying the load keeps both the extension source and its top-level code
289
+ // from running, even when the project only configured it and no block uses
290
+ // it. The VM reports "Permission to load extension denied" for the project.
291
+ vm.securityManager.canLoadExtensionFromProject = () =>
292
+ Promise.resolve(allowProjectExtensions);
293
+ // Node has neither the web worker nor the iframe sandbox the VM would use, so
294
+ // a permitted extension runs in this process, exactly like PenguinMod's
295
+ // unsandboxed mode. Denied extensions never reach the sandbox selector.
296
+ vm.securityManager.getSandboxMode = () =>
297
+ Promise.resolve(allowProjectExtensions ? "unsandboxed" : "iframe");
298
+ return vm;
299
+ }
300
+
301
+ /**
302
+ * Build a fresh VM and load an sb3/pmp buffer into it.
303
+ * @param {Buffer} sb3
304
+ * @param {{allowProjectExtensions?: boolean}} [options]
305
+ */
306
+ export const loadProject = async (sb3, options = {}) => {
307
+ const vm = createVm(options);
308
+ // Loading an extension logs its whole source to the console; keep reporter
309
+ // output (especially JSON and JUnit) parseable.
310
+ const log = console.log;
311
+ console.log = () => {};
312
+ try {
313
+ await vm.loadProject(sb3);
314
+ } catch (error) {
315
+ try {
316
+ vm.stopAll();
317
+ } catch {
318
+ // Preserve the original load error if cleanup also fails.
319
+ }
320
+ throw error;
321
+ } finally {
322
+ console.log = log;
323
+ }
324
+ return vm;
325
+ };
@@ -2,5 +2,5 @@
2
2
  "name": "@penguinmod/packager",
3
3
  "version": "1.9.2",
4
4
  "commit": "53f868b85ec21d2edab9ce00fd2d709da93da6d6",
5
- "builtAt": "2026-11-05T20:22:27.350Z"
5
+ "builtAt": "2026-11-06T05:46:49.113Z"
6
6
  }
@@ -0,0 +1,321 @@
1
+ // Fx fork of Temporary Variables (upstream id lmsTempVars2).
2
+ // Only the extension id and the runtime expose key differ; see README.md.
3
+ //
4
+ // Name: Temporary Variables
5
+ // ID: fxTempVars
6
+ // Description: Create disposable runtime or thread variables.
7
+ // By: LilyMakesThings <https://scratch.mit.edu/users/LilyMakesThings/>
8
+ // By: Mio <https://scratch.mit.edu/users/0znzw/>
9
+ // License: MIT AND LGPL-3.0
10
+
11
+ /* generated l10n code */Scratch.translate.setup({"de":{"_Temporary Variables":"Temporäre Variablen"},"fi":{"_Runtime Variables":"Ajonaikaiset muuttujat","_Temporary Variables":"Väliaikaiset muuttujat","_Thread Variables":"Säiemuuttujat","_active runtime variables":"käytössä olevat ajonaikaiset muuttujat","_active thread variables":"käytössä olevat säiemuuttujat","_change runtime var [VAR] by [NUM]":"lisää ajonaikaiseen muuttujaan [VAR] arvo [NUM]","_change thread var [VAR] by [NUM]":"lisää säiemuuttujaan [VAR] arvo [NUM]","_delete all runtime variables":"poista kaikki ajonaikaiset muuttujat","_delete runtime var [VAR]":"poista ajonaikainen muuttuja [VAR]","_for [VAR] in [NUM]":"toista [NUM] kertaa ja laske säiemuuttujalla [VAR]","_runtime var [VAR]":"ajonaikainen muuttuja [VAR]","_runtime var [VAR] exists?":"onko ajonaikainen muuttuja [VAR] olemassa?","_set runtime var [VAR] to [STRING]":"aseta ajonaikainen muuttuja [VAR] arvoon [STRING] ","_set thread var [VAR] to [STRING]":"aseta säiemuuttuja [VAR] arvoon [STRING]","_thread var [VAR]":"säiemuuttuja [VAR]","_thread var [VAR] exists?":"onko säiemuuttuja [VAR] olemassa?"},"it":{"_Runtime Variables":"Variabili Runtime","_Temporary Variables":"Variabili Temporanee","_Thread Variables":"Varabili del Thread","_active runtime variables":"variabili runtime attive","_active thread variables":"variabili del thread attivo","_change runtime var [VAR] by [NUM]":"change variabile runtime [VAR] di [NUM]","_change thread var [VAR] by [NUM]":"cambia variabile [VAR] del thread di [NUM]","_delete all runtime variables":"rimuovi tutte le variabili runtime","_delete runtime var [VAR]":"rimuovi variabile runtime [VAR]","_for [VAR] in [NUM]":"per ciascun valore della variabile [VAR] da 1 a [NUM]","_runtime var [VAR]":"variabile runtime [VAR]","_runtime var [VAR] exists?":"variabile runtime [VAR] esiste","_set runtime var [VAR] to [STRING]":"imposta variabile runtime [VAR] a [STRING]","_set thread var [VAR] to [STRING]":"imposta variabile [VAR] del thread a [STRING]","_thread var [VAR]":"variabile [VAR] del thread","_thread var [VAR] exists?":"variabile [VAR] del thread esiste"},"ja":{"_Runtime Variables":"ランタイム変数","_Temporary Variables":"一時変数","_Thread Variables":"スレッド変数","_active runtime variables":"アクティブなランタイム変数","_active thread variables":"アクティブなスレッド変数","_change runtime var [VAR] by [NUM]":"ランタイム変数[VAR]を[NUM]ずつ変える","_change thread var [VAR] by [NUM]":"スレッド変数[VAR]を[NUM]ずつ変える","_delete all runtime variables":"すべてのランタイム変数を削除する","_delete runtime var [VAR]":"ランタイム変数[VAR]を削除する","_for [VAR] in [NUM]":"[NUM]の要素を[VAR]に入れて繰り返す","_runtime var [VAR]":"ランタイム変数[VAR]","_runtime var [VAR] exists?":"ランタイム変数[VAR]が存在している","_set runtime var [VAR] to [STRING]":"ランタイム変数[VAR]を[STRING]にする","_set thread var [VAR] to [STRING]":"スレッド変数[VAR]を[STRING]にする","_thread var [VAR]":"スレッド変数[VAR]","_thread var [VAR] exists?":"スレッド変数[VAR]が存在している"},"ko":{"_Runtime Variables":"런타임 변수","_Temporary Variables":"일시적 변수","_Thread Variables":"스레드 변수","_active runtime variables":"활성화된 런타임 변수 목록","_active thread variables":"활성화된 스레드 변수 목록","_change runtime var [VAR] by [NUM]":"런타임 변수 [VAR]을(를) [NUM]만큼 바꾸기","_change thread var [VAR] by [NUM]":"스레드 변수 [VAR]을(를) [NUM]만큼 바꾸기","_delete all runtime variables":"모든 런타임 변수 삭제하기","_delete runtime var [VAR]":"런타임 변수 [VAR] 삭제하기","_for [VAR] in [NUM]":"[NUM]만큼 [VAR](으)로 순회하기","_runtime var [VAR]":"런타임 변수 [VAR]","_runtime var [VAR] exists?":"런타임 변수 [VAR]이(가) 존재하는가?","_set runtime var [VAR] to [STRING]":"런타임 변수 [VAR]을(를) [STRING](으)로 정하기","_set thread var [VAR] to [STRING]":"스레드 변수 [VAR]을(를) [STRING](으)로 정하기","_thread var [VAR]":"스레드 변수 [VAR]","_thread var [VAR] exists?":"스레드 변수 [VAR]이(가) 존재하는가?"},"nb":{"_Temporary Variables":"Midlertidige variabler"},"nl":{"_Runtime Variables":"Looptijdvariabelen","_Temporary Variables":"Tijdelijke variabelen","_Thread Variables":"Threadvariabelen","_active runtime variables":"actieve looptijdvariabelen","_active thread variables":"actieve threadvariabelen","_change runtime var [VAR] by [NUM]":"verander looptijdvar. [VAR] met [NUM]","_change thread var [VAR] by [NUM]":"verander threadvar. [VAR] met [NUM]","_delete all runtime variables":"verwijder alle looptijdvariabelen","_delete runtime var [VAR]":"verwijder looptijdvar. [VAR]","_for [VAR] in [NUM]":"voor elke [VAR] in [NUM]","_runtime var [VAR]":"looptijdvar. [VAR]","_runtime var [VAR] exists?":"looptijdvar. [VAR] bestaat?","_set runtime var [VAR] to [STRING]":"maak looptijdvar. [VAR] [STRING]","_set thread var [VAR] to [STRING]":"maak threadvar. [VAR] [STRING]","_thread var [VAR]":"threadvar. [VAR]","_thread var [VAR] exists?":"threadvar. [VAR] bestaat?"},"ru":{"_Runtime Variables":"Время Выполнения Переменных","_Temporary Variables":"Временные Переменные","_Thread Variables":"Переменные Ветки","_active runtime variables":"активные переменные с временем выполнения","_active thread variables":"активные веточные переменные","_change runtime var [VAR] by [NUM]":"изменить время выполнения переменной [VAR] в [NUM]","_change thread var [VAR] by [NUM]":"изменить веточную переменную [VAR] на [NUM]","_delete all runtime variables":"удалить все переменные с временем выполнения","_delete runtime var [VAR]":"удалить переменную [VAR] с временем выполнения","_for [VAR] in [NUM]":"повторить [VAR] раз [NUM]","_runtime var [VAR]":"время выполнения переменной [VAR]","_runtime var [VAR] exists?":"время выполнения для переменной [VAR] существует?","_set runtime var [VAR] to [STRING]":"задать время выполнения переменной [VAR] в [STRING]","_set thread var [VAR] to [STRING]":"задать веточную переменную [VAR] в [STRING]","_thread var [VAR]":"веточная переменная [VAR]","_thread var [VAR] exists?":"веточная переменная [VAR] существует?"},"tr":{"_Temporary Variables":"Geçici Değişkenler"},"uk":{"_Temporary Variables":"Тимчасові Змінні"},"zh-cn":{"_Runtime Variables":"临时变量","_Temporary Variables":"临时变量","_Thread Variables":"线程变量","_active runtime variables":"所有临时变量","_active thread variables":"所有线程变量","_change runtime var [VAR] by [NUM]":"将临时变量[VAR]增加[NUM]","_change thread var [VAR] by [NUM]":"将线程变量[VAR]增加[NUM]","_delete all runtime variables":"删除所有临时变量","_delete runtime var [VAR]":"删除临时变量[VAR]","_for [VAR] in [NUM]":"对于[NUM]中的每个线程变量[VAR]","_runtime var [VAR]":"临时变量[VAR]","_runtime var [VAR] exists?":"临时变量[VAR]是否存在?","_set runtime var [VAR] to [STRING]":"将临时变量[VAR]设为[STRING]","_set thread var [VAR] to [STRING]":"将线程变量[VAR]设为[STRING]","_thread var [VAR]":"线程变量[VAR]","_thread var [VAR] exists?":"线程变量[VAR]是否存在?"}});/* end generated l10n code */(function (Scratch) {
12
+ "use strict";
13
+
14
+ // Credit to skyhigh173 for the idea of this.
15
+ const label = (name, hidden) => ({
16
+ blockType: Scratch.BlockType.LABEL,
17
+ text: name,
18
+ hideFromPalette: hidden,
19
+ });
20
+
21
+ class TempVars {
22
+ constructor() {
23
+ // this.resetRuntimeVariables would be preferable but,
24
+ // its easier on TS when defined in the constructor,
25
+ // and not abstracted out.
26
+ //
27
+ // Object.create(null) prevents "variable [toString]" from returning a function.
28
+ this.runtimeVariables = Object.create(null);
29
+
30
+ Scratch.vm.runtime.on("PROJECT_START", () => {
31
+ this.resetRuntimeVariables();
32
+ });
33
+
34
+ Scratch.vm.runtime.on("PROJECT_STOP_ALL", () => {
35
+ this.resetRuntimeVariables();
36
+ });
37
+ }
38
+
39
+ getInfo() {
40
+ return {
41
+ id: "fxTempVars",
42
+ name: Scratch.translate("Temporary Variables"),
43
+ color1: "#FF791A",
44
+ color2: "#E15D00",
45
+ blocks: [
46
+ label(Scratch.translate("Thread Variables"), false),
47
+
48
+ {
49
+ opcode: "setThreadVariable",
50
+ blockType: Scratch.BlockType.COMMAND,
51
+ text: Scratch.translate("set thread var [VAR] to [STRING]"),
52
+ arguments: {
53
+ VAR: {
54
+ type: Scratch.ArgumentType.STRING,
55
+ defaultValue: "variable",
56
+ },
57
+ STRING: {
58
+ type: Scratch.ArgumentType.STRING,
59
+ defaultValue: "0",
60
+ },
61
+ },
62
+ },
63
+ {
64
+ opcode: "changeThreadVariable",
65
+ blockType: Scratch.BlockType.COMMAND,
66
+ text: Scratch.translate("change thread var [VAR] by [NUM]"),
67
+ arguments: {
68
+ VAR: {
69
+ type: Scratch.ArgumentType.STRING,
70
+ defaultValue: "variable",
71
+ },
72
+ NUM: {
73
+ type: Scratch.ArgumentType.NUMBER,
74
+ defaultValue: "1",
75
+ },
76
+ },
77
+ },
78
+
79
+ "---",
80
+
81
+ {
82
+ opcode: "getThreadVariable",
83
+ blockType: Scratch.BlockType.REPORTER,
84
+ text: Scratch.translate("thread var [VAR]"),
85
+ disableMonitor: true,
86
+ allowDropAnywhere: true,
87
+ arguments: {
88
+ VAR: {
89
+ type: Scratch.ArgumentType.STRING,
90
+ defaultValue: "variable",
91
+ },
92
+ },
93
+ },
94
+ {
95
+ opcode: "threadVariableExists",
96
+ blockType: Scratch.BlockType.BOOLEAN,
97
+ text: Scratch.translate("thread var [VAR] exists?"),
98
+ arguments: {
99
+ VAR: {
100
+ type: Scratch.ArgumentType.STRING,
101
+ defaultValue: "variable",
102
+ },
103
+ },
104
+ },
105
+
106
+ "---",
107
+
108
+ {
109
+ opcode: "forEachThreadVariable",
110
+ blockType: Scratch.BlockType.LOOP,
111
+ text: Scratch.translate("for [VAR] in [NUM]"),
112
+ arguments: {
113
+ VAR: {
114
+ type: Scratch.ArgumentType.STRING,
115
+ defaultValue: "thread variable",
116
+ },
117
+ NUM: {
118
+ type: Scratch.ArgumentType.NUMBER,
119
+ defaultValue: "10",
120
+ },
121
+ },
122
+ },
123
+ {
124
+ opcode: "listThreadVariables",
125
+ blockType: Scratch.BlockType.REPORTER,
126
+ text: Scratch.translate("active thread variables"),
127
+ disableMonitor: true,
128
+ },
129
+
130
+ "---",
131
+
132
+ label(Scratch.translate("Runtime Variables"), false),
133
+
134
+ {
135
+ opcode: "setRuntimeVariable",
136
+ blockType: Scratch.BlockType.COMMAND,
137
+ text: Scratch.translate("set runtime var [VAR] to [STRING]"),
138
+ arguments: {
139
+ VAR: {
140
+ type: Scratch.ArgumentType.STRING,
141
+ defaultValue: "variable",
142
+ },
143
+ STRING: {
144
+ type: Scratch.ArgumentType.STRING,
145
+ defaultValue: "0",
146
+ },
147
+ },
148
+ },
149
+ {
150
+ opcode: "changeRuntimeVariable",
151
+ blockType: Scratch.BlockType.COMMAND,
152
+ text: Scratch.translate("change runtime var [VAR] by [NUM]"),
153
+ arguments: {
154
+ VAR: {
155
+ type: Scratch.ArgumentType.STRING,
156
+ defaultValue: "variable",
157
+ },
158
+ NUM: {
159
+ type: Scratch.ArgumentType.STRING,
160
+ defaultValue: "1",
161
+ },
162
+ },
163
+ },
164
+
165
+ "---",
166
+
167
+ {
168
+ opcode: "getRuntimeVariable",
169
+ blockType: Scratch.BlockType.REPORTER,
170
+ text: Scratch.translate("runtime var [VAR]"),
171
+ disableMonitor: true,
172
+ allowDropAnywhere: true,
173
+ arguments: {
174
+ VAR: {
175
+ type: Scratch.ArgumentType.STRING,
176
+ defaultValue: "variable",
177
+ },
178
+ },
179
+ },
180
+ {
181
+ opcode: "runtimeVariableExists",
182
+ blockType: Scratch.BlockType.BOOLEAN,
183
+ text: Scratch.translate("runtime var [VAR] exists?"),
184
+ arguments: {
185
+ VAR: {
186
+ type: Scratch.ArgumentType.STRING,
187
+ defaultValue: "variable",
188
+ },
189
+ },
190
+ },
191
+
192
+ "---",
193
+
194
+ {
195
+ opcode: "deleteRuntimeVariable",
196
+ blockType: Scratch.BlockType.COMMAND,
197
+ text: Scratch.translate("delete runtime var [VAR]"),
198
+ arguments: {
199
+ VAR: {
200
+ type: Scratch.ArgumentType.STRING,
201
+ defaultValue: "variable",
202
+ },
203
+ },
204
+ },
205
+ {
206
+ opcode: "deleteAllRuntimeVariables",
207
+ func: "resetRuntimeVariables",
208
+ blockType: Scratch.BlockType.COMMAND,
209
+ text: Scratch.translate("delete all runtime variables"),
210
+ },
211
+ {
212
+ opcode: "listRuntimeVariables",
213
+ blockType: Scratch.BlockType.REPORTER,
214
+ text: Scratch.translate("active runtime variables"),
215
+ },
216
+ ],
217
+ };
218
+ }
219
+
220
+ /* THREAD VARIABLES */
221
+
222
+ setThreadVariable(args, util) {
223
+ const thread = util.thread;
224
+ if (!thread.variables) {
225
+ thread.variables = Object.create(null);
226
+ }
227
+ thread.variables[args.VAR] = args.STRING;
228
+ }
229
+
230
+ changeThreadVariable(args, util) {
231
+ const thread = util.thread;
232
+ if (!thread.variables) {
233
+ thread.variables = Object.create(null);
234
+ }
235
+ const vars = thread.variables;
236
+ const prev = Scratch.Cast.toNumber(vars[args.VAR]);
237
+ const next = Scratch.Cast.toNumber(args.NUM);
238
+ vars[args.VAR] = prev + next;
239
+ }
240
+
241
+ getThreadVariable(args, util) {
242
+ const thread = util.thread;
243
+ if (!thread.variables) {
244
+ thread.variables = Object.create(null);
245
+ }
246
+ return thread.variables[args.VAR] ?? "";
247
+ }
248
+
249
+ threadVariableExists(args, util) {
250
+ const thread = util.thread;
251
+ if (!thread.variables) {
252
+ thread.variables = Object.create(null);
253
+ }
254
+ return Object.prototype.hasOwnProperty.call(thread.variables, args.VAR);
255
+ }
256
+
257
+ forEachThreadVariable(args, util) {
258
+ const thread = util.thread;
259
+ if (!thread.variables) {
260
+ thread.variables = Object.create(null);
261
+ }
262
+ const vars = thread.variables;
263
+ if (!Object.prototype.hasOwnProperty.call(util.stackFrame, "index")) {
264
+ util.stackFrame.index = 0;
265
+ }
266
+ if (util.stackFrame.index < Scratch.Cast.toNumber(args.NUM)) {
267
+ util.stackFrame.index++;
268
+ vars[args.VAR] = util.stackFrame.index;
269
+ return true;
270
+ }
271
+ }
272
+
273
+ listThreadVariables(args, util) {
274
+ const thread = util.thread;
275
+ if (!thread.variables) {
276
+ thread.variables = Object.create(null);
277
+ }
278
+ return Object.keys(thread.variables).join(",");
279
+ }
280
+
281
+ /* RUNTIME VARIABLES */
282
+
283
+ setRuntimeVariable(args) {
284
+ this.runtimeVariables[args.VAR] = args.STRING;
285
+ }
286
+
287
+ changeRuntimeVariable(args) {
288
+ const prev = Scratch.Cast.toNumber(this.runtimeVariables[args.VAR]);
289
+ const next = Scratch.Cast.toNumber(args.NUM);
290
+ this.runtimeVariables[args.VAR] = prev + next;
291
+ }
292
+
293
+ getRuntimeVariable(args) {
294
+ return this.runtimeVariables[args.VAR] ?? "";
295
+ }
296
+
297
+ runtimeVariableExists(args) {
298
+ return Object.prototype.hasOwnProperty.call(
299
+ this.runtimeVariables,
300
+ args.VAR
301
+ );
302
+ }
303
+
304
+ listRuntimeVariables(args, util) {
305
+ return Object.keys(this.runtimeVariables).join(",");
306
+ }
307
+
308
+ deleteRuntimeVariable(args) {
309
+ Reflect.deleteProperty(this.runtimeVariables, args.VAR);
310
+ }
311
+
312
+ resetRuntimeVariables() {
313
+ this.runtimeVariables = Object.create(null);
314
+ }
315
+ }
316
+ // The expose format follows TurboWarp's convention of `ext_${extensionId}`.
317
+ // Expose the extension on runtime for others to use.
318
+ const extension = new TempVars();
319
+ Scratch.vm.runtime.ext_fxTempVars = extension;
320
+ Scratch.extensions.register(extension);
321
+ })(Scratch);