@triflare/fx 2.0.0 → 2.1.0-beta.1

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/merge.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Multi-file project merge.
3
+ *
4
+ * Kept separate from `project.js` because merging is pure: it needs no
5
+ * filesystem access, so editor tooling (including the browser VS Code
6
+ * extension host) can reuse it without pulling in `node:fs`.
7
+ */
8
+ import { ParseError } from "./errors.js";
9
+
10
+ /** @param {object} loc */
11
+ function errorInfo(loc) {
12
+ return {
13
+ file: loc && loc.file,
14
+ line: loc && loc.line,
15
+ column: loc && loc.column,
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Merge parsed programs into a single project AST.
21
+ *
22
+ * - At most one `stage { }` declaration is allowed across all files.
23
+ * - Top-level statements from every file become stage content.
24
+ * - At most one plain `sprite "Name" { }` declaration is allowed per name.
25
+ * Additional `partial sprite "Name" { }` fragments are appended to it.
26
+ * - Partial sprites only merge when `partialSprites` is enabled.
27
+ *
28
+ * Bodies are composed base-first, then fragments in build order, so costume
29
+ * order and local variable initializers stay predictable regardless of which
30
+ * file happens to sort first.
31
+ *
32
+ * @param {Array<{ast: object, file: string}>} entries
33
+ * @param {{partialSprites?: boolean, configName?: string}} [options]
34
+ * @returns {object} a `Program` node
35
+ */
36
+ export function mergePrograms(
37
+ entries,
38
+ { partialSprites = false, configName = "fx.yml" } = {},
39
+ ) {
40
+ let stage = null;
41
+ const topItems = [];
42
+ /** @type {Map<string, {name: string, loc: object, base: object|null, fragments: object[], files: string[]}>} */
43
+ const groups = new Map();
44
+
45
+ for (const { ast, file } of entries) {
46
+ if (ast.stage) {
47
+ if (stage) {
48
+ throw new ParseError(
49
+ "A project may only declare one stage",
50
+ errorInfo(ast.stage.loc),
51
+ );
52
+ }
53
+ stage = ast.stage;
54
+ }
55
+ topItems.push(...ast.topItems);
56
+ for (const sprite of ast.sprites) {
57
+ if (sprite.partial && !partialSprites) {
58
+ throw new ParseError(
59
+ `partial sprite ${JSON.stringify(sprite.name)} is disabled; ` +
60
+ `set "fx.partialSprites: true" in ${configName} to enable it`,
61
+ errorInfo(sprite.loc),
62
+ );
63
+ }
64
+
65
+ let group = groups.get(sprite.name);
66
+ if (!group) {
67
+ group = {
68
+ name: sprite.name,
69
+ loc: sprite.loc,
70
+ base: null,
71
+ fragments: [],
72
+ files: [],
73
+ };
74
+ groups.set(sprite.name, group);
75
+ }
76
+ group.files.push(file);
77
+
78
+ if (sprite.partial) {
79
+ group.fragments.push({ decl: sprite, file });
80
+ } else if (group.base) {
81
+ throw new ParseError(
82
+ `Duplicate sprite ${JSON.stringify(sprite.name)} ` +
83
+ `(declared in ${group.base.file} and ${file}); ` +
84
+ `use \`partial sprite ${JSON.stringify(sprite.name)}\` to split it across files`,
85
+ errorInfo(sprite.loc),
86
+ );
87
+ } else {
88
+ group.base = { decl: sprite, file };
89
+ }
90
+ }
91
+ }
92
+
93
+ const sprites = [...groups.values()].map((group) => {
94
+ const base = group.base ? [group.base.decl] : [];
95
+ const fragments = group.fragments.map(({ decl }) => decl);
96
+ const [first] = base.length ? base : fragments;
97
+ return {
98
+ type: "SpriteDecl",
99
+ name: group.name,
100
+ body: [...base, ...fragments].flatMap((decl) => decl.body),
101
+ loc: first.loc,
102
+ open: first.open ?? null,
103
+ partial: false,
104
+ files: group.files,
105
+ };
106
+ });
107
+
108
+ return { type: "Program", stage, sprites, topItems };
109
+ }
package/src/parser.js CHANGED
@@ -127,18 +127,31 @@ class Parser {
127
127
  parseSprite(partial = false, startTok = null) {
128
128
  const tok = this.expect("sprite");
129
129
  const name = this.expectType("string", "a sprite name string").value;
130
- const body = this.parseBlockBody();
131
- return { type: "SpriteDecl", name, body, loc: startTok ?? tok, partial };
130
+ const { body, open } = this.parseBlockBody(true);
131
+ return {
132
+ type: "SpriteDecl",
133
+ name,
134
+ body,
135
+ loc: startTok ?? tok,
136
+ // Tokens are the brace's position, needed to insert declarations into
137
+ // the body when the brace is not on the declaration line.
138
+ open,
139
+ partial,
140
+ };
132
141
  }
133
142
 
134
143
  parseStage() {
135
144
  const tok = this.expect("stage");
136
- const body = this.parseBlockBody();
137
- return { type: "StageDecl", name: "Stage", body, loc: tok };
145
+ const { body, open } = this.parseBlockBody(true);
146
+ return { type: "StageDecl", name: "Stage", body, loc: tok, open };
138
147
  }
139
148
 
140
- parseBlockBody() {
141
- this.expect("{");
149
+ /**
150
+ * Parse `{ ... }`.
151
+ * @param {boolean} [withOpen] return `{body, open}` instead of the items only
152
+ */
153
+ parseBlockBody(withOpen = false) {
154
+ const open = this.expect("{");
142
155
  const items = [];
143
156
  while (!this.atPunct("}")) {
144
157
  if (this.peek().type === "eof")
@@ -146,7 +159,7 @@ class Parser {
146
159
  items.push(this.parseBodyItem());
147
160
  }
148
161
  this.expect("}");
149
- return items;
162
+ return withOpen ? { body: items, open } : items;
150
163
  }
151
164
 
152
165
  parseBodyItem() {
@@ -161,6 +174,12 @@ class Parser {
161
174
  return this.parseVarDecl(true);
162
175
  }
163
176
  break;
177
+ case "thread":
178
+ case "runtime":
179
+ if (this.peek(1).type === "ident" && this.peek(1).value === "let") {
180
+ return this.parseTempVarDecl(tok.value);
181
+ }
182
+ break;
164
183
  case "list":
165
184
  if (this.peek(1).type === "ident") {
166
185
  return this.parseListDecl();
@@ -224,6 +243,16 @@ class Parser {
224
243
  return { type: "VarDecl", name, init, cloud, loc: tok };
225
244
  }
226
245
 
246
+ parseTempVarDecl(kind) {
247
+ const tok = this.next();
248
+ this.expect("let");
249
+ const name = this.expectIdent();
250
+ let init = null;
251
+ if (this.eat("=")) init = this.parseExpression();
252
+ this.expect(";");
253
+ return { type: "TempVarDecl", kind, name, init, loc: tok };
254
+ }
255
+
227
256
  parseListDecl() {
228
257
  const tok = this.expect("list");
229
258
  const name = this.expectIdent();
@@ -248,15 +277,16 @@ class Parser {
248
277
  const ptok = this.peek();
249
278
  const paramName = this.expectIdent();
250
279
  let boolean = false;
280
+ let type = null;
251
281
  // Optional type annotation: `flag: boolean`
252
282
  if (this.eat(":")) {
253
- const typeName = this.expectIdent();
254
- if (typeName === "boolean") boolean = true;
255
- else if (typeName !== "number" && typeName !== "string") {
256
- this.fail(`Unknown parameter type ${JSON.stringify(typeName)}`, ptok);
283
+ type = this.expectIdent();
284
+ if (type === "boolean") boolean = true;
285
+ else if (type !== "number" && type !== "string") {
286
+ this.fail(`Unknown parameter type ${JSON.stringify(type)}`, ptok);
257
287
  }
258
288
  }
259
- params.push({ name: paramName, boolean });
289
+ params.push({ name: paramName, boolean, type });
260
290
  if (!this.eat(",")) break;
261
291
  }
262
292
  this.expect(")");
@@ -603,6 +633,16 @@ class Parser {
603
633
  * @param {string} [file]
604
634
  */
605
635
  export function parse(source, file = "<input>") {
606
- const tokens = tokenize(source, file);
636
+ return parseTokens(tokenize(source, file), source, file);
637
+ }
638
+
639
+ /**
640
+ * Parse an existing token stream. Keeping the caller's tokens lets tools such
641
+ * as the formatter match AST locations back to tokens without re-lexing.
642
+ * @param {import('./lexer.js').Token[]} tokens
643
+ * @param {string} source
644
+ * @param {string} [file]
645
+ */
646
+ export function parseTokens(tokens, source, file = "<input>") {
607
647
  return new Parser(tokens, source, file).parse();
608
648
  }
package/src/project.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { ParseError } from "./errors.js";
11
10
  import { configFilename, sourceExtension } from "./brand.js";
11
+ import { mergePrograms as mergeParsed } from "./merge.js";
12
12
 
13
13
  /** Directories never scanned for `.tfx` sources. */
14
14
  const IGNORED_DIRS = new Set([
@@ -58,99 +58,19 @@ export function discoverSources(dir) {
58
58
  return found.sort();
59
59
  }
60
60
 
61
- /** @param {object} loc */
62
- function errorInfo(loc) {
63
- return {
64
- file: loc && loc.file,
65
- line: loc && loc.line,
66
- column: loc && loc.column,
67
- };
68
- }
69
-
70
61
  /**
71
62
  * Merge parsed programs into a single project AST.
72
63
  *
73
- * - At most one `stage { }` declaration is allowed across all files.
74
- * - Top-level statements from every file become stage content.
75
- * - At most one plain `sprite "Name" { }` declaration is allowed per name.
76
- * Additional `partial sprite "Name" { }` fragments are appended to it.
77
- * - Partial sprites only merge when `partialSprites` is enabled.
78
- *
79
- * Bodies are composed base-first, then fragments in build order, so costume
80
- * order and local variable initializers stay predictable regardless of which
81
- * file happens to sort first.
64
+ * The merge itself is pure (see `merge.js`); this wrapper only injects the
65
+ * configured config filename so project-facing errors keep the right name.
82
66
  *
83
67
  * @param {Array<{ast: object, file: string}>} entries
84
68
  * @param {{partialSprites?: boolean}} [options]
85
69
  * @returns {object} a `Program` node
86
70
  */
87
- export function mergePrograms(entries, { partialSprites = false } = {}) {
88
- let stage = null;
89
- const topItems = [];
90
- /** @type {Map<string, {name: string, loc: object, base: object|null, fragments: object[], files: string[]}>} */
91
- const groups = new Map();
92
-
93
- for (const { ast, file } of entries) {
94
- if (ast.stage) {
95
- if (stage) {
96
- throw new ParseError(
97
- "A project may only declare one stage",
98
- errorInfo(ast.stage.loc),
99
- );
100
- }
101
- stage = ast.stage;
102
- }
103
- topItems.push(...ast.topItems);
104
- for (const sprite of ast.sprites) {
105
- if (sprite.partial && !partialSprites) {
106
- throw new ParseError(
107
- `partial sprite ${JSON.stringify(sprite.name)} is disabled; ` +
108
- `set "fx.partialSprites: true" in ${configFilename()} to enable it`,
109
- errorInfo(sprite.loc),
110
- );
111
- }
112
-
113
- let group = groups.get(sprite.name);
114
- if (!group) {
115
- group = {
116
- name: sprite.name,
117
- loc: sprite.loc,
118
- base: null,
119
- fragments: [],
120
- files: [],
121
- };
122
- groups.set(sprite.name, group);
123
- }
124
- group.files.push(file);
125
-
126
- if (sprite.partial) {
127
- group.fragments.push({ decl: sprite, file });
128
- } else if (group.base) {
129
- throw new ParseError(
130
- `Duplicate sprite ${JSON.stringify(sprite.name)} ` +
131
- `(declared in ${group.base.file} and ${file}); ` +
132
- `use \`partial sprite ${JSON.stringify(sprite.name)}\` to split it across files`,
133
- errorInfo(sprite.loc),
134
- );
135
- } else {
136
- group.base = { decl: sprite, file };
137
- }
138
- }
139
- }
140
-
141
- const sprites = [...groups.values()].map((group) => {
142
- const base = group.base ? [group.base.decl] : [];
143
- const fragments = group.fragments.map(({ decl }) => decl);
144
- const [first] = base.length ? base : fragments;
145
- return {
146
- type: "SpriteDecl",
147
- name: group.name,
148
- body: [...base, ...fragments].flatMap((decl) => decl.body),
149
- loc: first.loc,
150
- partial: false,
151
- files: group.files,
152
- };
71
+ export function mergePrograms(entries, options = {}) {
72
+ return mergeParsed(entries, {
73
+ ...options,
74
+ configName: configFilename(),
153
75
  });
154
-
155
- return { type: "Program", stage, sprites, topItems };
156
76
  }
@@ -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);