moonscratch 0.1.0-alpha.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.
Files changed (151) hide show
  1. package/.agents/skills/moonbit-agent-guide/LICENSE +202 -0
  2. package/.agents/skills/moonbit-agent-guide/SKILL.mbt.md +1126 -0
  3. package/.agents/skills/moonbit-agent-guide/SKILL.md +1126 -0
  4. package/.agents/skills/moonbit-agent-guide/ide.md +116 -0
  5. package/.agents/skills/moonbit-agent-guide/references/advanced-moonbit-build.md +106 -0
  6. package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.mbt.md +422 -0
  7. package/.agents/skills/moonbit-agent-guide/references/moonbit-language-fundamentals.md +422 -0
  8. package/.agents/skills/moonbit-practice/SKILL.md +258 -0
  9. package/.agents/skills/moonbit-practice/assets/ci.yaml +25 -0
  10. package/.agents/skills/moonbit-practice/reference/agents.md +1469 -0
  11. package/.agents/skills/moonbit-practice/reference/configuration.md +228 -0
  12. package/.agents/skills/moonbit-practice/reference/ffi.md +229 -0
  13. package/.agents/skills/moonbit-practice/reference/ide.md +189 -0
  14. package/.agents/skills/moonbit-practice/reference/performance.md +217 -0
  15. package/.agents/skills/moonbit-practice/reference/refactor.md +154 -0
  16. package/.agents/skills/moonbit-practice/reference/stdlib.md +351 -0
  17. package/.agents/skills/moonbit-practice/reference/testing.md +228 -0
  18. package/.agents/skills/moonbit-refactoring/LICENSE +21 -0
  19. package/.agents/skills/moonbit-refactoring/SKILL.md +323 -0
  20. package/.githooks/README.md +23 -0
  21. package/.githooks/pre-commit +3 -0
  22. package/.github/workflows/copilot-setup-steps.yml +40 -0
  23. package/.turbo/turbo-typecheck.log +2 -0
  24. package/AGENTS.md +91 -0
  25. package/LICENSE +21 -0
  26. package/PLAN.md +64 -0
  27. package/README.mbt.md +77 -0
  28. package/README.md +84 -0
  29. package/TODO.md +120 -0
  30. package/a.png +0 -0
  31. package/benchmarks/calc.bench.ts +144 -0
  32. package/benchmarks/draw.bench.ts +215 -0
  33. package/benchmarks/load.bench.ts +28 -0
  34. package/benchmarks/render.bench.ts +53 -0
  35. package/benchmarks/run.bench.ts +8 -0
  36. package/benchmarks/types.d.ts +15 -0
  37. package/docs/scratch-vm-specs/eventloop.md +103 -0
  38. package/docs/scratch-vm-specs/moonscratch-time-separation.md +50 -0
  39. package/index.html +91 -0
  40. package/js/AGENTS.md +5 -0
  41. package/js/a.ts +52 -0
  42. package/js/assets/AGENTS.md +5 -0
  43. package/js/assets/base64.test.ts +14 -0
  44. package/js/assets/base64.ts +21 -0
  45. package/js/assets/build-asset.test.ts +26 -0
  46. package/js/assets/build-asset.ts +28 -0
  47. package/js/assets/create.test.ts +142 -0
  48. package/js/assets/create.ts +122 -0
  49. package/js/assets/index.test.ts +15 -0
  50. package/js/assets/index.ts +2 -0
  51. package/js/assets/types.ts +26 -0
  52. package/js/assets/validation.test.ts +34 -0
  53. package/js/assets/validation.ts +25 -0
  54. package/js/assets.test.ts +14 -0
  55. package/js/assets.ts +1 -0
  56. package/js/index.test.ts +26 -0
  57. package/js/index.ts +3 -0
  58. package/js/render/index.test.ts +65 -0
  59. package/js/render/index.ts +13 -0
  60. package/js/render/sharp.ts +87 -0
  61. package/js/render/svg.ts +68 -0
  62. package/js/render/types.ts +35 -0
  63. package/js/render/utils.ts +108 -0
  64. package/js/render/webgl.ts +274 -0
  65. package/js/sharp-optional.d.ts +16 -0
  66. package/js/test/helpers.ts +116 -0
  67. package/js/test/hikkaku-sample.test.ts +37 -0
  68. package/js/test/rubik-components.input-motion.test.ts +60 -0
  69. package/js/test/rubik-components.lists.test.ts +49 -0
  70. package/js/test/rubik-components.operators.test.ts +104 -0
  71. package/js/test/rubik-components.pen.test.ts +112 -0
  72. package/js/test/rubik-components.procedures-loops.test.ts +72 -0
  73. package/js/test/rubik-components.variables-branches.test.ts +57 -0
  74. package/js/test/rubik-components.visibility-entry.test.ts +31 -0
  75. package/js/test/test-projects.ts +598 -0
  76. package/js/test/variable.ts +200 -0
  77. package/js/test/warp.test.ts +59 -0
  78. package/js/vm/AGENTS.md +6 -0
  79. package/js/vm/README.md +183 -0
  80. package/js/vm/bindings.test.ts +13 -0
  81. package/js/vm/bindings.ts +5 -0
  82. package/js/vm/compare-operators.test.ts +145 -0
  83. package/js/vm/constants.test.ts +11 -0
  84. package/js/vm/constants.ts +4 -0
  85. package/js/vm/effect-guards.test.ts +68 -0
  86. package/js/vm/effect-guards.ts +44 -0
  87. package/js/vm/factory.test.ts +486 -0
  88. package/js/vm/factory.ts +615 -0
  89. package/js/vm/headless-vm.test.ts +131 -0
  90. package/js/vm/headless-vm.ts +342 -0
  91. package/js/vm/index.test.ts +28 -0
  92. package/js/vm/index.ts +5 -0
  93. package/js/vm/internal-types.ts +32 -0
  94. package/js/vm/json.test.ts +40 -0
  95. package/js/vm/json.ts +273 -0
  96. package/js/vm/normalize.test.ts +48 -0
  97. package/js/vm/normalize.ts +65 -0
  98. package/js/vm/options.test.ts +30 -0
  99. package/js/vm/options.ts +55 -0
  100. package/js/vm/pen-transparency.test.ts +115 -0
  101. package/js/vm/program-wasm.ts +322 -0
  102. package/js/vm/scheduler-render.test.ts +401 -0
  103. package/js/vm/scratch-assets.test.ts +136 -0
  104. package/js/vm/scratch-assets.ts +202 -0
  105. package/js/vm/types.ts +358 -0
  106. package/js/vm/value-guards.test.ts +25 -0
  107. package/js/vm/value-guards.ts +18 -0
  108. package/moon.mod.json +10 -0
  109. package/package.json +33 -0
  110. package/scripts/preinstall.ts +4 -0
  111. package/src/AGENTS.md +6 -0
  112. package/src/api.mbt +161 -0
  113. package/src/api_aot_commands.mbt +184 -0
  114. package/src/api_effects_json.mbt +72 -0
  115. package/src/api_options.mbt +60 -0
  116. package/src/api_program_wasm.mbt +1647 -0
  117. package/src/api_program_wat.mbt +2206 -0
  118. package/src/api_snapshot_json.mbt +44 -0
  119. package/src/cmd/AGENTS.md +5 -0
  120. package/src/cmd/main/AGENTS.md +5 -0
  121. package/src/cmd/main/main.mbt +29 -0
  122. package/src/cmd/main/moon.pkg +7 -0
  123. package/src/cmd/main/pkg.generated.mbti +13 -0
  124. package/src/json_helpers.mbt +176 -0
  125. package/src/moon.pkg +65 -0
  126. package/src/moonscratch.mbt +3 -0
  127. package/src/moonscratch_wbtest.mbt +40 -0
  128. package/src/parser_sb3.mbt +890 -0
  129. package/src/pkg.generated.mbti +479 -0
  130. package/src/runtime_eval.mbt +2844 -0
  131. package/src/runtime_exec.mbt +3850 -0
  132. package/src/runtime_render.mbt +2550 -0
  133. package/src/runtime_state.mbt +870 -0
  134. package/src/test/AGENTS.md +3 -0
  135. package/src/test/projects/AGENTS.md +6 -0
  136. package/src/test/projects/moon.pkg +4 -0
  137. package/src/test/projects/moonscratch_compat_test.mbt +642 -0
  138. package/src/test/projects/moonscratch_core_test.mbt +1332 -0
  139. package/src/test/projects/moonscratch_runtime_test.mbt +1087 -0
  140. package/src/test/projects/pkg.generated.mbti +13 -0
  141. package/src/test/projects/test_support.mbt +35 -0
  142. package/src/types_effects.mbt +20 -0
  143. package/src/types_error.mbt +4 -0
  144. package/src/types_options.mbt +31 -0
  145. package/src/types_runtime_structs.mbt +254 -0
  146. package/src/types_vm.mbt +109 -0
  147. package/tsconfig.json +29 -0
  148. package/viewer/index.ts +399 -0
  149. package/viewer/vite.d.ts +1 -0
  150. package/viewer/worker.ts +161 -0
  151. package/vite.config.ts +11 -0
@@ -0,0 +1,3850 @@
1
+ ///|
2
+ fn repeat_frame(remaining : Int, substack : Int?, after : Int?) -> ControlFrame {
3
+ { kind: ControlFrameKind::Repeat, remaining, substack, after }
4
+ }
5
+
6
+ ///|
7
+ fn forever_frame(substack : Int?, after : Int?) -> ControlFrame {
8
+ { kind: ControlFrameKind::Forever, remaining: 0, substack, after }
9
+ }
10
+
11
+ ///|
12
+ fn clamp_0_100(value : Double) -> Double {
13
+ if value < 0.0 {
14
+ 0.0
15
+ } else if value > 100.0 {
16
+ 100.0
17
+ } else {
18
+ value
19
+ }
20
+ }
21
+
22
+ ///|
23
+ fn clamp_double(value : Double, min : Double, max : Double) -> Double {
24
+ if value < min {
25
+ min
26
+ } else if value > max {
27
+ max
28
+ } else {
29
+ value
30
+ }
31
+ }
32
+
33
+ ///|
34
+ fn clamp_music_tempo(value : Double) -> Double {
35
+ clamp_double(value, 20.0, 500.0)
36
+ }
37
+
38
+ ///|
39
+ fn clamp_music_beats(value : Double) -> Double {
40
+ clamp_double(value, 0.0, 100.0)
41
+ }
42
+
43
+ ///|
44
+ fn wrap_int(value : Int, min : Int, max : Int) -> Int {
45
+ if max < min {
46
+ return min
47
+ }
48
+ let span = max - min + 1
49
+ let shifted = (value - min).mod(span)
50
+ if shifted < 0 {
51
+ shifted + span + min
52
+ } else {
53
+ shifted + min
54
+ }
55
+ }
56
+
57
+ ///|
58
+ fn normalize_music_drum(drum : Double) -> Int {
59
+ let raw = drum.round().to_int() - 1
60
+ wrap_int(raw, 0, 17) + 1
61
+ }
62
+
63
+ ///|
64
+ fn normalize_music_instrument_index(instrument : Double) -> Int {
65
+ let raw = instrument.round().to_int() - 1
66
+ wrap_int(raw, 0, 20)
67
+ }
68
+
69
+ ///|
70
+ fn music_beats_to_ms(vm : Vm, beats : Double) -> Int {
71
+ if vm.music_tempo <= 0.0 {
72
+ 0
73
+ } else {
74
+ (60.0 / vm.music_tempo * beats * 1000.0).to_int()
75
+ }
76
+ }
77
+
78
+ ///|
79
+ fn input_or_field_string(
80
+ vm : Vm,
81
+ target_index : Int,
82
+ block : ScratchBlock,
83
+ input_name : String,
84
+ field_name : String,
85
+ ) -> String {
86
+ let from_input = json_to_string_value(
87
+ value_from_input(vm, target_index, block, input_name, 0),
88
+ )
89
+ .trim()
90
+ .to_string()
91
+ if from_input != "" {
92
+ from_input
93
+ } else {
94
+ match field_value(block, field_name) {
95
+ Some((value, _)) => value
96
+ None => ""
97
+ }
98
+ }
99
+ }
100
+
101
+ ///|
102
+ fn block_constant_number_input(
103
+ block : ScratchBlock,
104
+ input_name : String,
105
+ ) -> Double? {
106
+ block.const_number_inputs.get(input_name)
107
+ }
108
+
109
+ ///|
110
+ fn reporter_opcode_guaranteed_number(opcode : String) -> Bool {
111
+ match opcode {
112
+ "math_number"
113
+ | "math_integer"
114
+ | "math_whole_number"
115
+ | "math_positive_number"
116
+ | "math_angle"
117
+ | "operator_add"
118
+ | "operator_subtract"
119
+ | "operator_multiply"
120
+ | "operator_divide"
121
+ | "operator_mod"
122
+ | "operator_round"
123
+ | "operator_mathop"
124
+ | "operator_random"
125
+ | "motion_xposition"
126
+ | "motion_yposition"
127
+ | "motion_direction"
128
+ | "looks_size"
129
+ | "sound_volume"
130
+ | "sensing_timer"
131
+ | "sensing_mousex"
132
+ | "sensing_mousey"
133
+ | "sensing_current"
134
+ | "sensing_dayssince2000"
135
+ | "sensing_loudness"
136
+ | "music_getTempo"
137
+ | "control_get_counter" => true
138
+ _ => false
139
+ }
140
+ }
141
+
142
+ ///|
143
+ fn fast_numeric_value_for_setvariable(
144
+ vm : Vm,
145
+ target : TargetState,
146
+ target_index : Int,
147
+ block : ScratchBlock,
148
+ ) -> Double? {
149
+ match block.const_number_inputs.get("VALUE") {
150
+ Some(value) => return Some(value)
151
+ None => ()
152
+ }
153
+ match block_input_block_id(block, "VALUE") {
154
+ Some(reporter_id) =>
155
+ match target.blocks.get(reporter_id) {
156
+ Some(reporter) =>
157
+ if reporter_opcode_guaranteed_number(reporter.opcode) {
158
+ Some(number_from_input(vm, target_index, block, "VALUE", 0))
159
+ } else {
160
+ None
161
+ }
162
+ None => None
163
+ }
164
+ None => None
165
+ }
166
+ }
167
+
168
+ ///|
169
+ fn can_fast_numeric_value_for_setvariable(
170
+ target : TargetState,
171
+ block : ScratchBlock,
172
+ ) -> Bool {
173
+ if block.const_number_inputs.contains("VALUE") {
174
+ return true
175
+ }
176
+ match block_input_block_id(block, "VALUE") {
177
+ Some(reporter_id) =>
178
+ match target.blocks.get(reporter_id) {
179
+ Some(reporter) => reporter_opcode_guaranteed_number(reporter.opcode)
180
+ None => false
181
+ }
182
+ None => false
183
+ }
184
+ }
185
+
186
+ ///|
187
+ fn eval_reporter_number_or_fallback(
188
+ vm : Vm,
189
+ target_index : Int,
190
+ block_id : String,
191
+ ) -> Double {
192
+ match eval_reporter_number_block_depth(vm, target_index, block_id, 1) {
193
+ Some(value) => value
194
+ None =>
195
+ json_to_number_value(
196
+ eval_reporter_block_depth(vm, target_index, block_id, 1),
197
+ )
198
+ }
199
+ }
200
+
201
+ ///|
202
+ fn try_fast_repeat_motion_step(
203
+ vm : Vm,
204
+ target_index : Int,
205
+ times : Int,
206
+ substack_pc : Int?,
207
+ ) -> Bool {
208
+ if times <= 0 {
209
+ return true
210
+ }
211
+ if target_index < 0 || target_index >= vm.targets.length() {
212
+ return false
213
+ }
214
+ let target = vm.targets[target_index]
215
+ if target.pen_down {
216
+ if target.pen_transparency > 0.0 || target.pen_size != 1.0 {
217
+ return false
218
+ }
219
+ if target.x.floor() != target.x || target.y.floor() != target.y {
220
+ return false
221
+ }
222
+ }
223
+ match substack_pc {
224
+ Some(block_pc) =>
225
+ if block_pc >= 0 &&
226
+ block_pc < vm.targets[target_index].blocks_by_pc.length() {
227
+ let body = vm.targets[target_index].blocks_by_pc[block_pc]
228
+ if body.next is None {
229
+ match body.opcode {
230
+ "motion_changexby" =>
231
+ match block_constant_number_input(body, "DX") {
232
+ Some(dx) => {
233
+ if target.pen_down && dx.abs() != 1.0 {
234
+ return false
235
+ }
236
+ let total = dx * Double::from_int(times)
237
+ move_target_with_pen(
238
+ vm,
239
+ target_index,
240
+ vm.targets[target_index].x + total,
241
+ vm.targets[target_index].y,
242
+ )
243
+ true
244
+ }
245
+ None => false
246
+ }
247
+ "motion_changeyby" =>
248
+ match block_constant_number_input(body, "DY") {
249
+ Some(dy) => {
250
+ if target.pen_down && dy.abs() != 1.0 {
251
+ return false
252
+ }
253
+ let total = dy * Double::from_int(times)
254
+ move_target_with_pen(
255
+ vm,
256
+ target_index,
257
+ vm.targets[target_index].x,
258
+ vm.targets[target_index].y + total,
259
+ )
260
+ true
261
+ }
262
+ None => false
263
+ }
264
+ _ => false
265
+ }
266
+ } else {
267
+ false
268
+ }
269
+ } else {
270
+ false
271
+ }
272
+ None => false
273
+ }
274
+ }
275
+
276
+ ///|
277
+ fn try_fast_repeat_numeric_variable_loop(
278
+ vm : Vm,
279
+ target_index : Int,
280
+ times : Int,
281
+ substack_pc : Int?,
282
+ ) -> Bool {
283
+ if times <= 0 {
284
+ return true
285
+ }
286
+ if target_index < 0 || target_index >= vm.targets.length() {
287
+ return false
288
+ }
289
+ let target = vm.targets[target_index]
290
+ let start_pc = match substack_pc {
291
+ Some(pc) => pc
292
+ None => return false
293
+ }
294
+ if start_pc < 0 || start_pc >= target.blocks_by_pc.length() {
295
+ return false
296
+ }
297
+
298
+ let op_block_pcs = []
299
+ let op_kinds = []
300
+ let op_owner_indices = []
301
+ let op_variable_ids = []
302
+ let op_variable_slots = []
303
+ let op_value_modes = []
304
+ let op_value_consts = []
305
+ let op_value_reporters = []
306
+
307
+ let mut cursor_pc : Int? = Some(start_pc)
308
+ let mut traversed = 0
309
+ while traversed < 64 {
310
+ traversed += 1
311
+ let block_pc = match cursor_pc {
312
+ Some(pc) => pc
313
+ None => break
314
+ }
315
+ if block_pc < 0 || block_pc >= target.blocks_by_pc.length() {
316
+ return false
317
+ }
318
+ let block = target.blocks_by_pc[block_pc]
319
+ match block.opcode {
320
+ "data_setvariableto" =>
321
+ match field_value(block, "VARIABLE") {
322
+ Some((name, id)) =>
323
+ match resolve_variable_ref(vm, target_index, id, Some(name)) {
324
+ Some((owner_index, variable_id, variable_slot)) => {
325
+ if !can_fast_numeric_value_for_setvariable(target, block) {
326
+ return false
327
+ }
328
+ op_block_pcs.push(block_pc)
329
+ op_kinds.push(0)
330
+ op_owner_indices.push(owner_index)
331
+ op_variable_ids.push(variable_id)
332
+ op_variable_slots.push(variable_slot)
333
+ match block.const_number_inputs.get("VALUE") {
334
+ Some(value) => {
335
+ op_value_modes.push(0)
336
+ op_value_consts.push(value)
337
+ op_value_reporters.push("")
338
+ }
339
+ None =>
340
+ match block_input_block_id(block, "VALUE") {
341
+ Some(reporter_id) => {
342
+ op_value_modes.push(1)
343
+ op_value_consts.push(0.0)
344
+ op_value_reporters.push(reporter_id)
345
+ }
346
+ None => return false
347
+ }
348
+ }
349
+ }
350
+ None => return false
351
+ }
352
+ None => return false
353
+ }
354
+ "data_changevariableby" =>
355
+ match field_value(block, "VARIABLE") {
356
+ Some((name, id)) =>
357
+ match resolve_variable_ref(vm, target_index, id, Some(name)) {
358
+ Some((owner_index, variable_id, variable_slot)) => {
359
+ op_block_pcs.push(block_pc)
360
+ op_kinds.push(1)
361
+ op_owner_indices.push(owner_index)
362
+ op_variable_ids.push(variable_id)
363
+ op_variable_slots.push(variable_slot)
364
+ match block.const_number_inputs.get("VALUE") {
365
+ Some(value) => {
366
+ op_value_modes.push(0)
367
+ op_value_consts.push(value)
368
+ op_value_reporters.push("")
369
+ }
370
+ None =>
371
+ match block_input_block_id(block, "VALUE") {
372
+ Some(reporter_id) => {
373
+ op_value_modes.push(1)
374
+ op_value_consts.push(0.0)
375
+ op_value_reporters.push(reporter_id)
376
+ }
377
+ None => {
378
+ op_value_modes.push(2)
379
+ op_value_consts.push(0.0)
380
+ op_value_reporters.push("")
381
+ }
382
+ }
383
+ }
384
+ }
385
+ None => return false
386
+ }
387
+ None => return false
388
+ }
389
+ _ => return false
390
+ }
391
+ cursor_pc = target_block_pc_from_id(target, block.next)
392
+ }
393
+
394
+ if cursor_pc is Some(_) || op_block_pcs.is_empty() {
395
+ return false
396
+ }
397
+
398
+ for _ in 0..<times {
399
+ for i in 0..<op_block_pcs.length() {
400
+ let block_pc = op_block_pcs[i]
401
+ let block = vm.targets[target_index].blocks_by_pc[block_pc]
402
+ let owner_index = op_owner_indices[i]
403
+ let variable_id = op_variable_ids[i]
404
+ let variable_slot = op_variable_slots[i]
405
+ let value = match op_value_modes[i] {
406
+ 0 => op_value_consts[i]
407
+ 1 =>
408
+ eval_reporter_number_or_fallback(
409
+ vm,
410
+ target_index,
411
+ op_value_reporters[i],
412
+ )
413
+ _ => number_from_input(vm, target_index, block, "VALUE", 0)
414
+ }
415
+ if op_kinds[i] == 0 {
416
+ write_variable_by_ref(
417
+ vm,
418
+ owner_index,
419
+ variable_id,
420
+ variable_slot,
421
+ json_number(value),
422
+ )
423
+ } else {
424
+ let current = json_to_number_value(
425
+ read_variable_by_ref(vm, owner_index, variable_id, variable_slot),
426
+ )
427
+ write_variable_by_ref(
428
+ vm,
429
+ owner_index,
430
+ variable_id,
431
+ variable_slot,
432
+ json_number(current + value),
433
+ )
434
+ }
435
+ }
436
+ }
437
+ true
438
+ }
439
+
440
+ ///|
441
+ fn text2speech_voice_ids() -> Array[String] {
442
+ ["ALTO", "TENOR", "SQUEAK", "GIANT", "KITTEN"]
443
+ }
444
+
445
+ ///|
446
+ fn normalize_tts_voice(raw : String, fallback : String) -> String {
447
+ let input = raw.trim().to_string()
448
+ if input == "" {
449
+ return fallback
450
+ }
451
+
452
+ let voices = text2speech_voice_ids()
453
+ match parse_double_or_none(input) {
454
+ Some(number) => {
455
+ let index = wrap_int(number.to_int() - 1, 0, voices.length() - 1)
456
+ voices[index]
457
+ }
458
+ None => {
459
+ let upper = input.to_upper().to_string()
460
+ if voices.any(fn(voice) { voice == upper }) {
461
+ upper
462
+ } else {
463
+ fallback
464
+ }
465
+ }
466
+ }
467
+ }
468
+
469
+ ///|
470
+ fn wrap_0_100(value : Double) -> Double {
471
+ let wrapped = value.mod(100.0)
472
+ if wrapped < 0.0 {
473
+ wrapped + 100.0
474
+ } else {
475
+ wrapped
476
+ }
477
+ }
478
+
479
+ ///|
480
+ fn set_target_pen_color_param(
481
+ vm : Vm,
482
+ target_index : Int,
483
+ param : String,
484
+ value : Double,
485
+ change : Bool,
486
+ ) -> Unit {
487
+ if target_index < 0 || target_index >= vm.targets.length() {
488
+ return
489
+ }
490
+ match lower_trim(param) {
491
+ "color" => {
492
+ let base = if change { vm.targets[target_index].pen_color } else { 0.0 }
493
+ vm.targets[target_index].pen_color = wrap_0_100(base + value)
494
+ }
495
+ "saturation" => {
496
+ let base = if change {
497
+ vm.targets[target_index].pen_saturation
498
+ } else {
499
+ 0.0
500
+ }
501
+ vm.targets[target_index].pen_saturation = clamp_0_100(base + value)
502
+ }
503
+ "brightness" => {
504
+ let base = if change {
505
+ vm.targets[target_index].pen_brightness
506
+ } else {
507
+ 0.0
508
+ }
509
+ vm.targets[target_index].pen_brightness = clamp_0_100(base + value)
510
+ }
511
+ "transparency" => {
512
+ let base = if change {
513
+ vm.targets[target_index].pen_transparency
514
+ } else {
515
+ 0.0
516
+ }
517
+ vm.targets[target_index].pen_transparency = clamp_0_100(base + value)
518
+ }
519
+ _ => ()
520
+ }
521
+ }
522
+
523
+ ///|
524
+ fn set_target_pen_color_from_rgb(
525
+ vm : Vm,
526
+ target_index : Int,
527
+ r : Int,
528
+ g : Int,
529
+ b : Int,
530
+ ) -> Unit {
531
+ if target_index < 0 || target_index >= vm.targets.length() {
532
+ return
533
+ }
534
+ let (h, s, v) = render_rgb_to_hsv01(
535
+ Double::from_int(r) / 255.0,
536
+ Double::from_int(g) / 255.0,
537
+ Double::from_int(b) / 255.0,
538
+ )
539
+ vm.targets[target_index].pen_color = h * 100.0
540
+ vm.targets[target_index].pen_saturation = s * 100.0
541
+ vm.targets[target_index].pen_brightness = v * 100.0
542
+ vm.targets[target_index].pen_transparency = 0.0
543
+ vm.targets[target_index].pen_legacy_shade = vm.targets[target_index].pen_brightness /
544
+ 2.0
545
+ }
546
+
547
+ ///|
548
+ fn apply_target_legacy_pen_shade(vm : Vm, target_index : Int) -> Unit {
549
+ if target_index < 0 || target_index >= vm.targets.length() {
550
+ return
551
+ }
552
+ let (base_r, base_g, base_b) = render_hsv01_to_rgb(
553
+ vm.targets[target_index].pen_color / 100.0,
554
+ 1.0,
555
+ 1.0,
556
+ )
557
+ let shade = if vm.targets[target_index].pen_legacy_shade > 100.0 {
558
+ 200.0 - vm.targets[target_index].pen_legacy_shade
559
+ } else {
560
+ vm.targets[target_index].pen_legacy_shade
561
+ }
562
+ let (mixed_r, mixed_g, mixed_b) = if shade < 50.0 {
563
+ let ratio = (10.0 + shade) / 60.0
564
+ (base_r * ratio, base_g * ratio, base_b * ratio)
565
+ } else {
566
+ let ratio = (shade - 50.0) / 60.0
567
+ (
568
+ base_r * (1.0 - ratio) + 1.0 * ratio,
569
+ base_g * (1.0 - ratio) + 1.0 * ratio,
570
+ base_b * (1.0 - ratio) + 1.0 * ratio,
571
+ )
572
+ }
573
+ let (h, s, v) = render_rgb_to_hsv01(mixed_r, mixed_g, mixed_b)
574
+ vm.targets[target_index].pen_color = h * 100.0
575
+ vm.targets[target_index].pen_saturation = s * 100.0
576
+ vm.targets[target_index].pen_brightness = v * 100.0
577
+ }
578
+
579
+ ///|
580
+ fn set_target_looks_effect(
581
+ vm : Vm,
582
+ target_index : Int,
583
+ effect : String,
584
+ value : Double,
585
+ change : Bool,
586
+ ) -> Unit {
587
+ if target_index < 0 || target_index >= vm.targets.length() {
588
+ return
589
+ }
590
+ match lower_trim(effect) {
591
+ "color" =>
592
+ vm.targets[target_index].looks_effect_color = if change {
593
+ vm.targets[target_index].looks_effect_color + value
594
+ } else {
595
+ value
596
+ }
597
+ "fisheye" =>
598
+ vm.targets[target_index].looks_effect_fisheye = if change {
599
+ vm.targets[target_index].looks_effect_fisheye + value
600
+ } else {
601
+ value
602
+ }
603
+ "whirl" =>
604
+ vm.targets[target_index].looks_effect_whirl = if change {
605
+ vm.targets[target_index].looks_effect_whirl + value
606
+ } else {
607
+ value
608
+ }
609
+ "pixelate" =>
610
+ vm.targets[target_index].looks_effect_pixelate = if change {
611
+ vm.targets[target_index].looks_effect_pixelate + value
612
+ } else {
613
+ value
614
+ }
615
+ "mosaic" =>
616
+ vm.targets[target_index].looks_effect_mosaic = if change {
617
+ vm.targets[target_index].looks_effect_mosaic + value
618
+ } else {
619
+ value
620
+ }
621
+ "brightness" =>
622
+ vm.targets[target_index].looks_effect_brightness = if change {
623
+ vm.targets[target_index].looks_effect_brightness + value
624
+ } else {
625
+ value
626
+ }
627
+ "ghost" =>
628
+ vm.targets[target_index].looks_effect_ghost = if change {
629
+ vm.targets[target_index].looks_effect_ghost + value
630
+ } else {
631
+ value
632
+ }
633
+ _ => ()
634
+ }
635
+ }
636
+
637
+ ///|
638
+ fn clear_target_looks_effect(vm : Vm, target_index : Int) -> Unit {
639
+ if target_index < 0 || target_index >= vm.targets.length() {
640
+ return
641
+ }
642
+ vm.targets[target_index].looks_effect_color = 0.0
643
+ vm.targets[target_index].looks_effect_fisheye = 0.0
644
+ vm.targets[target_index].looks_effect_whirl = 0.0
645
+ vm.targets[target_index].looks_effect_pixelate = 0.0
646
+ vm.targets[target_index].looks_effect_mosaic = 0.0
647
+ vm.targets[target_index].looks_effect_brightness = 0.0
648
+ vm.targets[target_index].looks_effect_ghost = 0.0
649
+ }
650
+
651
+ ///|
652
+ fn sync_thread_warp_state(vm : Vm, thread : Thread) -> Unit {
653
+ match vm.procedure_frames.get(thread.id) {
654
+ Some(stack) =>
655
+ if stack.is_empty() {
656
+ thread.warp_mode = false
657
+ thread.warp_started_ms = 0
658
+ } else {
659
+ let frame = stack[stack.length() - 1]
660
+ thread.warp_mode = frame.warp_mode
661
+ if thread.warp_mode && thread.warp_started_ms == 0 {
662
+ thread.warp_started_ms = vm.now_ms
663
+ } else if !thread.warp_mode {
664
+ thread.warp_started_ms = 0
665
+ }
666
+ }
667
+ None => {
668
+ thread.warp_mode = false
669
+ thread.warp_started_ms = 0
670
+ }
671
+ }
672
+ }
673
+
674
+ ///|
675
+ fn unwind_control(vm : Vm, thread : Thread) -> Thread {
676
+ let thread = thread
677
+ while thread.pc is None {
678
+ let mut should_pop_procedure_first = false
679
+ match vm.procedure_frames.get(thread.id) {
680
+ Some(proc_stack) =>
681
+ if !proc_stack.is_empty() {
682
+ let current_proc = proc_stack[proc_stack.length() - 1]
683
+ if thread.stack.length() <= current_proc.control_depth {
684
+ should_pop_procedure_first = true
685
+ }
686
+ }
687
+ None => ()
688
+ }
689
+
690
+ if should_pop_procedure_first {
691
+ match pop_procedure_frame(vm, thread.id) {
692
+ Some(frame) => {
693
+ thread.pc = frame.return_pc
694
+ sync_thread_warp_state(vm, thread)
695
+ continue
696
+ }
697
+ None => ()
698
+ }
699
+ }
700
+
701
+ match thread.stack.pop() {
702
+ Some(frame) =>
703
+ match frame.kind {
704
+ ControlFrameKind::Repeat =>
705
+ if frame.remaining > 1 {
706
+ let next_frame = frame
707
+ next_frame.remaining = frame.remaining - 1
708
+ thread.stack.push(next_frame)
709
+ thread.pc = frame.substack
710
+ } else {
711
+ thread.pc = frame.after
712
+ }
713
+ ControlFrameKind::Forever =>
714
+ match frame.substack {
715
+ Some(substack) => {
716
+ // Re-push forever frame so the loop does not terminate after one unwind.
717
+ thread.stack.push(frame)
718
+ thread.pc = Some(substack)
719
+ }
720
+ None => thread.pc = frame.after
721
+ }
722
+ }
723
+ None =>
724
+ match pop_procedure_frame(vm, thread.id) {
725
+ Some(frame) => {
726
+ thread.pc = frame.return_pc
727
+ sync_thread_warp_state(vm, thread)
728
+ }
729
+ None => {
730
+ thread.done = true
731
+ thread.warp_mode = false
732
+ thread.warp_started_ms = 0
733
+ return thread
734
+ }
735
+ }
736
+ }
737
+ }
738
+ thread
739
+ }
740
+
741
+ ///|
742
+ fn kill_other_scripts_for_target(
743
+ vm : Vm,
744
+ target_index : Int,
745
+ current_thread_id : Int,
746
+ ) -> Unit {
747
+ for i, thread in vm.threads {
748
+ if thread.target_index == target_index && thread.id != current_thread_id {
749
+ vm.threads[i].done = true
750
+ }
751
+ }
752
+ }
753
+
754
+ ///|
755
+ fn block_broadcast_name(
756
+ vm : Vm,
757
+ target_index : Int,
758
+ block : ScratchBlock,
759
+ ) -> String {
760
+ let from_input = value_from_input(
761
+ vm, target_index, block, "BROADCAST_INPUT", 0,
762
+ )
763
+ let from_input_text = json_to_string_value(from_input)
764
+ if from_input_text != "" {
765
+ return from_input_text
766
+ }
767
+ match field_value(block, "BROADCAST_OPTION") {
768
+ Some((name, _)) => name
769
+ None => ""
770
+ }
771
+ }
772
+
773
+ ///|
774
+ fn block_clone_option(
775
+ vm : Vm,
776
+ target_index : Int,
777
+ block : ScratchBlock,
778
+ ) -> String {
779
+ let from_input = json_to_string_value(
780
+ value_from_input(vm, target_index, block, "CLONE_OPTION", 0),
781
+ )
782
+ if from_input != "" {
783
+ return from_input
784
+ }
785
+ match field_value(block, "CLONE_OPTION") {
786
+ Some((name, _)) => name
787
+ None => ""
788
+ }
789
+ }
790
+
791
+ ///|
792
+ fn find_clone_source_target(
793
+ vm : Vm,
794
+ current_target_index : Int,
795
+ name : String,
796
+ ) -> Int? {
797
+ if name == "_myself_" {
798
+ if current_target_index >= 0 &&
799
+ current_target_index < vm.targets.length() &&
800
+ !vm.targets[current_target_index].deleted {
801
+ return Some(current_target_index)
802
+ }
803
+ return None
804
+ }
805
+
806
+ for i, target in vm.targets {
807
+ if !target.deleted &&
808
+ !target.is_stage &&
809
+ target.is_original &&
810
+ target.name == name {
811
+ return Some(i)
812
+ }
813
+ }
814
+ for i, target in vm.targets {
815
+ if !target.deleted && !target.is_stage && target.name == name {
816
+ return Some(i)
817
+ }
818
+ }
819
+ None
820
+ }
821
+
822
+ ///|
823
+ fn spawn_clone_start_hats(vm : Vm, target_index : Int) -> Int {
824
+ if target_index < 0 || target_index >= vm.targets.length() {
825
+ return 0
826
+ }
827
+ let target = vm.targets[target_index]
828
+ if target.deleted {
829
+ return 0
830
+ }
831
+
832
+ let mut count = 0
833
+ for start in target.clone_start_starts {
834
+ spawn_thread(vm, target_index, start, None)
835
+ count += 1
836
+ }
837
+ count
838
+ }
839
+
840
+ ///|
841
+ fn find_target_by_name(vm : Vm, name : String) -> Int? {
842
+ for i, target in vm.targets {
843
+ if !target.deleted && !target.is_stage && target.name == name {
844
+ return Some(i)
845
+ }
846
+ }
847
+ None
848
+ }
849
+
850
+ ///|
851
+ fn read_mouse_xy(vm : Vm) -> (Double, Double) {
852
+ match vm.io_state.get("mouse") {
853
+ Some(Object(obj)) => {
854
+ let x = object_get_number_or(obj, "x", 0.0)
855
+ let y = object_get_number_or(obj, "y", 0.0)
856
+ (x, y)
857
+ }
858
+ _ => (0.0, 0.0)
859
+ }
860
+ }
861
+
862
+ ///|
863
+ fn resolve_motion_menu_target(
864
+ vm : Vm,
865
+ current_target_index : Int,
866
+ menu_value : String,
867
+ ) -> (Double, Double)? {
868
+ let key = menu_value.trim().to_string()
869
+ if key == "_myself_" {
870
+ if current_target_index >= 0 && current_target_index < vm.targets.length() {
871
+ let target = vm.targets[current_target_index]
872
+ if !target.deleted {
873
+ return Some((target.x, target.y))
874
+ }
875
+ }
876
+ return None
877
+ }
878
+ if key == "_mouse_" {
879
+ return Some(read_mouse_xy(vm))
880
+ }
881
+ if key == "_random_" {
882
+ let x = next_random_unit(vm) * 480.0 - 240.0
883
+ let y = next_random_unit(vm) * 360.0 - 180.0
884
+ return Some((x, y))
885
+ }
886
+ match find_target_by_name(vm, key) {
887
+ Some(index) => {
888
+ let target = vm.targets[index]
889
+ Some((target.x, target.y))
890
+ }
891
+ None => None
892
+ }
893
+ }
894
+
895
+ ///|
896
+ fn normalized_scratch_direction(direction : Double) -> Double {
897
+ let mut out = direction
898
+ while out > 180.0 {
899
+ out -= 360.0
900
+ }
901
+ while out <= -180.0 {
902
+ out += 360.0
903
+ }
904
+ out
905
+ }
906
+
907
+ ///|
908
+ fn point_towards_position(
909
+ vm : Vm,
910
+ target_index : Int,
911
+ dest_x : Double,
912
+ dest_y : Double,
913
+ ) -> Unit {
914
+ let dx = dest_x - vm.targets[target_index].x
915
+ let dy = dest_y - vm.targets[target_index].y
916
+ if dx == 0.0 && dy == 0.0 {
917
+ return
918
+ }
919
+ let old_direction = vm.targets[target_index].direction
920
+ let angle = 90.0 - @math.atan2(dy, dx) * 180.0 / @math.PI
921
+ let new_direction = normalized_scratch_direction(angle)
922
+ vm.targets[target_index].direction = new_direction
923
+ if old_direction != new_direction {
924
+ request_redraw(vm)
925
+ }
926
+ }
927
+
928
+ ///|
929
+ fn apply_if_on_edge_bounce(vm : Vm, target_index : Int) -> Unit {
930
+ let original_x = vm.targets[target_index].x
931
+ let original_y = vm.targets[target_index].y
932
+ let mut x = original_x
933
+ let mut y = original_y
934
+ let mut direction = vm.targets[target_index].direction
935
+
936
+ if x > 240.0 {
937
+ x = 240.0
938
+ direction = 180.0 - direction
939
+ } else if x < -240.0 {
940
+ x = -240.0
941
+ direction = 180.0 - direction
942
+ }
943
+
944
+ if y > 180.0 {
945
+ y = 180.0
946
+ direction = -direction
947
+ } else if y < -180.0 {
948
+ y = -180.0
949
+ direction = -direction
950
+ }
951
+
952
+ if x != original_x || y != original_y {
953
+ move_target_with_pen(vm, target_index, x, y)
954
+ }
955
+ let old_direction = vm.targets[target_index].direction
956
+ let new_direction = normalized_scratch_direction(direction)
957
+ vm.targets[target_index].direction = new_direction
958
+ if old_direction != new_direction {
959
+ request_redraw(vm)
960
+ }
961
+ }
962
+
963
+ ///|
964
+ fn wrap_index(index : Int, count : Int) -> Int {
965
+ if count <= 0 {
966
+ return 0
967
+ }
968
+ let wrapped = index % count
969
+ if wrapped < 0 {
970
+ wrapped + count
971
+ } else {
972
+ wrapped
973
+ }
974
+ }
975
+
976
+ ///|
977
+ fn target_costume_count(target : TargetState) -> Int {
978
+ if target.costume_names.is_empty() {
979
+ 1
980
+ } else {
981
+ target.costume_names.length()
982
+ }
983
+ }
984
+
985
+ ///|
986
+ fn normalized_target_costume_index(target : TargetState, index : Int) -> Int {
987
+ wrap_index(index, target_costume_count(target))
988
+ }
989
+
990
+ ///|
991
+ fn target_costume_name(target : TargetState, index : Int) -> String {
992
+ if target.costume_names.is_empty() {
993
+ "costume_\{index + 1}"
994
+ } else {
995
+ let normalized = normalized_target_costume_index(target, index)
996
+ if normalized >= 0 && normalized < target.costume_names.length() {
997
+ target.costume_names[normalized]
998
+ } else {
999
+ "costume_\{normalized + 1}"
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ ///|
1005
+ fn find_costume_index_by_name(target : TargetState, name : String) -> Int? {
1006
+ for i, costume_name in target.costume_names {
1007
+ if costume_name == name {
1008
+ return Some(i)
1009
+ }
1010
+ }
1011
+ let lowered = name.to_lower().to_string()
1012
+ for i, costume_name in target.costume_names {
1013
+ if costume_name.to_lower().to_string() == lowered {
1014
+ return Some(i)
1015
+ }
1016
+ }
1017
+ None
1018
+ }
1019
+
1020
+ ///|
1021
+ fn resolve_target_costume_index(
1022
+ vm : Vm,
1023
+ target_index : Int,
1024
+ value : Json,
1025
+ backdrop_mode : Bool,
1026
+ ) -> Int? {
1027
+ if target_index < 0 || target_index >= vm.targets.length() {
1028
+ return None
1029
+ }
1030
+ let target = vm.targets[target_index]
1031
+ let count = target_costume_count(target)
1032
+ let current = normalized_target_costume_index(target, target.current_costume)
1033
+
1034
+ let raw = json_to_string_value(value).trim().to_string()
1035
+ let lowered = raw.to_lower().to_string()
1036
+ if backdrop_mode {
1037
+ if lowered == "next backdrop" {
1038
+ return Some(wrap_index(current + 1, count))
1039
+ }
1040
+ if lowered == "previous backdrop" {
1041
+ return Some(wrap_index(current - 1, count))
1042
+ }
1043
+ if lowered == "random backdrop" {
1044
+ let sampled = (next_random_unit(vm) * Double::from_int(count))
1045
+ .floor()
1046
+ .to_int()
1047
+ return Some(sampled.clamp(min=0, max=count - 1))
1048
+ }
1049
+ } else {
1050
+ if lowered == "next costume" {
1051
+ return Some(wrap_index(current + 1, count))
1052
+ }
1053
+ if lowered == "previous costume" {
1054
+ return Some(wrap_index(current - 1, count))
1055
+ }
1056
+ if lowered == "random costume" {
1057
+ let sampled = (next_random_unit(vm) * Double::from_int(count))
1058
+ .floor()
1059
+ .to_int()
1060
+ return Some(sampled.clamp(min=0, max=count - 1))
1061
+ }
1062
+ }
1063
+
1064
+ match value {
1065
+ Number(n, ..) => {
1066
+ let index = n.floor().to_int() - 1
1067
+ return Some(wrap_index(index, count))
1068
+ }
1069
+ _ => ()
1070
+ }
1071
+
1072
+ match parse_double_or_none(raw) {
1073
+ Some(parsed) => {
1074
+ let index = parsed.floor().to_int() - 1
1075
+ return Some(wrap_index(index, count))
1076
+ }
1077
+ None => ()
1078
+ }
1079
+
1080
+ match find_costume_index_by_name(target, raw) {
1081
+ Some(index) => Some(index)
1082
+ None => None
1083
+ }
1084
+ }
1085
+
1086
+ ///|
1087
+ fn block_backdrop_value(
1088
+ vm : Vm,
1089
+ target_index : Int,
1090
+ block : ScratchBlock,
1091
+ ) -> Json {
1092
+ let from_input = value_from_input(vm, target_index, block, "BACKDROP", 0)
1093
+ match from_input {
1094
+ Null =>
1095
+ match field_value(block, "BACKDROP") {
1096
+ Some((name, _)) => json_string(name)
1097
+ None => Json::null()
1098
+ }
1099
+ _ => from_input
1100
+ }
1101
+ }
1102
+
1103
+ ///|
1104
+ fn spawn_hats_for_backdrop(
1105
+ vm : Vm,
1106
+ backdrop_name : String,
1107
+ parent_waiter : Int?,
1108
+ ) -> Int {
1109
+ let mut count = 0
1110
+ for target_index, target in vm.targets {
1111
+ if target.deleted {
1112
+ continue
1113
+ }
1114
+ for hat in target.backdrop_hats {
1115
+ if hat.backdrop_name == backdrop_name {
1116
+ spawn_thread(vm, target_index, hat.start_block, parent_waiter)
1117
+ count += 1
1118
+ }
1119
+ }
1120
+ }
1121
+ count
1122
+ }
1123
+
1124
+ ///|
1125
+ fn set_stage_backdrop_index(
1126
+ vm : Vm,
1127
+ next_index : Int,
1128
+ parent_waiter : Int?,
1129
+ ) -> Int {
1130
+ if vm.stage_index < 0 || vm.stage_index >= vm.targets.length() {
1131
+ return 0
1132
+ }
1133
+ let stage_index = vm.stage_index
1134
+ let stage = vm.targets[stage_index]
1135
+ let normalized = normalized_target_costume_index(stage, next_index)
1136
+ let before = target_costume_name(stage, stage.current_costume)
1137
+ vm.targets[stage_index].current_costume = normalized
1138
+ let after = target_costume_name(vm.targets[stage_index], normalized)
1139
+ if after != before {
1140
+ request_redraw(vm)
1141
+ spawn_hats_for_backdrop(vm, after, parent_waiter)
1142
+ } else {
1143
+ 0
1144
+ }
1145
+ }
1146
+
1147
+ ///|
1148
+ fn set_stage_backdrop_from_value(
1149
+ vm : Vm,
1150
+ value : Json,
1151
+ parent_waiter : Int?,
1152
+ ) -> Int {
1153
+ match resolve_target_costume_index(vm, vm.stage_index, value, true) {
1154
+ Some(index) => set_stage_backdrop_index(vm, index, parent_waiter)
1155
+ None => 0
1156
+ }
1157
+ }
1158
+
1159
+ ///|
1160
+ fn block_mutation_string(block : ScratchBlock, key : String) -> String? {
1161
+ match block.mutation.get(key) {
1162
+ Some(String(value)) => Some(value)
1163
+ Some(Number(value, ..)) => Some(value.to_string())
1164
+ Some(True) => Some("true")
1165
+ Some(False) => Some("false")
1166
+ _ => None
1167
+ }
1168
+ }
1169
+
1170
+ ///|
1171
+ fn block_mutation_bool(block : ScratchBlock, key : String) -> Bool {
1172
+ match block.mutation.get(key) {
1173
+ Some(True) => true
1174
+ Some(False) => false
1175
+ Some(String(value)) => {
1176
+ let normalized = value.trim().to_lower()
1177
+ if normalized == "true" || normalized == "1" {
1178
+ true
1179
+ } else {
1180
+ false
1181
+ }
1182
+ }
1183
+ Some(Number(value, ..)) => value.to_int() != 0
1184
+ _ => false
1185
+ }
1186
+ }
1187
+
1188
+ ///|
1189
+ fn parse_json_string_array(raw : String) -> Array[String] {
1190
+ let parsed = try? @json.parse(raw)
1191
+ match parsed {
1192
+ Ok(Array(values)) => values.map(json_to_string_value)
1193
+ _ => []
1194
+ }
1195
+ }
1196
+
1197
+ ///|
1198
+ fn parse_json_value_array(raw : String) -> Array[Json] {
1199
+ let parsed = try? @json.parse(raw)
1200
+ match parsed {
1201
+ Ok(Array(values)) => values
1202
+ _ => []
1203
+ }
1204
+ }
1205
+
1206
+ ///|
1207
+ fn find_procedure_body(
1208
+ target : TargetState,
1209
+ proccode : String,
1210
+ ) -> (String, Array[String], Array[String], Array[Json], Bool)? {
1211
+ match target.procedures.get(proccode) {
1212
+ Some(spec) =>
1213
+ Some(
1214
+ (
1215
+ spec.start_block,
1216
+ spec.param_names.copy(),
1217
+ spec.param_ids.copy(),
1218
+ spec.param_defaults.copy(),
1219
+ spec.warp_mode,
1220
+ ),
1221
+ )
1222
+ None => None
1223
+ }
1224
+ }
1225
+
1226
+ ///|
1227
+ fn push_procedure_frame(
1228
+ vm : Vm,
1229
+ thread_id : Int,
1230
+ frame : ProcedureFrame,
1231
+ ) -> Unit {
1232
+ let stack = vm.procedure_frames.get_or_default(thread_id, [])
1233
+ stack.push(frame)
1234
+ vm.procedure_frames[thread_id] = stack
1235
+ }
1236
+
1237
+ ///|
1238
+ fn pop_procedure_frame(vm : Vm, thread_id : Int) -> ProcedureFrame? {
1239
+ let stack = vm.procedure_frames.get_or_default(thread_id, [])
1240
+ match stack.pop() {
1241
+ Some(frame) => {
1242
+ if stack.is_empty() {
1243
+ vm.procedure_frames.remove(thread_id)
1244
+ } else {
1245
+ vm.procedure_frames[thread_id] = stack
1246
+ }
1247
+ Some(frame)
1248
+ }
1249
+ None => None
1250
+ }
1251
+ }
1252
+
1253
+ ///|
1254
+ fn lower_trim(value : String) -> String {
1255
+ value.trim().to_lower().to_string()
1256
+ }
1257
+
1258
+ ///|
1259
+ fn push_string_if_non_empty(
1260
+ out : Array[String],
1261
+ raw : Json,
1262
+ lower : Bool,
1263
+ ) -> Unit {
1264
+ let text = json_to_string_value(raw).trim().to_string()
1265
+ if text == "" {
1266
+ return
1267
+ }
1268
+ if lower {
1269
+ out.push(text.to_lower().to_string())
1270
+ } else {
1271
+ out.push(text)
1272
+ }
1273
+ }
1274
+
1275
+ ///|
1276
+ fn parse_key_events(payload : Json) -> Array[String] {
1277
+ let out = []
1278
+ match payload {
1279
+ String(_) | Number(_, ..) => push_string_if_non_empty(out, payload, true)
1280
+ Array(items) =>
1281
+ for item in items {
1282
+ push_string_if_non_empty(out, item, true)
1283
+ }
1284
+ Object(obj) => {
1285
+ match obj.get("key") {
1286
+ Some(value) => push_string_if_non_empty(out, value, true)
1287
+ None => ()
1288
+ }
1289
+ match obj.get("keys") {
1290
+ Some(Array(items)) =>
1291
+ for item in items {
1292
+ push_string_if_non_empty(out, item, true)
1293
+ }
1294
+ _ => ()
1295
+ }
1296
+ match obj.get("pressed") {
1297
+ Some(Array(items)) =>
1298
+ for item in items {
1299
+ push_string_if_non_empty(out, item, true)
1300
+ }
1301
+ _ => ()
1302
+ }
1303
+ }
1304
+ _ => ()
1305
+ }
1306
+ out
1307
+ }
1308
+
1309
+ ///|
1310
+ fn parse_name_events(
1311
+ payload : Json,
1312
+ single_key : String,
1313
+ array_key : String,
1314
+ ) -> Array[String] {
1315
+ let out = []
1316
+ match payload {
1317
+ String(_) | Number(_, ..) => push_string_if_non_empty(out, payload, false)
1318
+ Array(items) =>
1319
+ for item in items {
1320
+ push_string_if_non_empty(out, item, false)
1321
+ }
1322
+ Object(obj) => {
1323
+ match obj.get(single_key) {
1324
+ Some(value) => push_string_if_non_empty(out, value, false)
1325
+ None => ()
1326
+ }
1327
+ match obj.get(array_key) {
1328
+ Some(Array(items)) =>
1329
+ for item in items {
1330
+ push_string_if_non_empty(out, item, false)
1331
+ }
1332
+ _ => ()
1333
+ }
1334
+ }
1335
+ _ => ()
1336
+ }
1337
+ out
1338
+ }
1339
+
1340
+ ///|
1341
+ fn parse_key_state_from_io(io_state : Map[String, Json]) -> Array[String] {
1342
+ let out = []
1343
+ match io_state.get("keys_down") {
1344
+ Some(payload) =>
1345
+ for key in parse_key_events(payload) {
1346
+ out.push(lower_trim(key))
1347
+ }
1348
+ None => ()
1349
+ }
1350
+ match io_state.get("keyboard") {
1351
+ Some(payload) =>
1352
+ for key in parse_key_events(payload) {
1353
+ out.push(lower_trim(key))
1354
+ }
1355
+ None => ()
1356
+ }
1357
+ out
1358
+ }
1359
+
1360
+ ///|
1361
+ fn parse_backdrop_state_from_io(io_state : Map[String, Json]) -> Array[String] {
1362
+ let out = []
1363
+ match io_state.get("backdrop") {
1364
+ Some(payload) =>
1365
+ for name in parse_name_events(payload, "backdrop", "backdrops") {
1366
+ out.push(name)
1367
+ }
1368
+ None => ()
1369
+ }
1370
+ out
1371
+ }
1372
+
1373
+ ///|
1374
+ fn unique_names(values : Array[String]) -> Array[String] {
1375
+ let seen = {}
1376
+ let out = []
1377
+ for raw in values {
1378
+ let value = raw.trim().to_string()
1379
+ if value == "" || seen.get_or_default(value, false) {
1380
+ continue
1381
+ }
1382
+ seen[value] = true
1383
+ out.push(value)
1384
+ }
1385
+ out
1386
+ }
1387
+
1388
+ ///|
1389
+ fn newly_added_names(
1390
+ current_values : Array[String],
1391
+ previous_values : Array[String],
1392
+ ) -> Array[String] {
1393
+ let previous_set = {}
1394
+ for raw in previous_values {
1395
+ let value = raw.trim().to_string()
1396
+ if value == "" {
1397
+ continue
1398
+ }
1399
+ previous_set[value] = true
1400
+ }
1401
+
1402
+ let seen = {}
1403
+ let out = []
1404
+ for raw in current_values {
1405
+ let value = raw.trim().to_string()
1406
+ if value == "" ||
1407
+ previous_set.get_or_default(value, false) ||
1408
+ seen.get_or_default(value, false) {
1409
+ continue
1410
+ }
1411
+ seen[value] = true
1412
+ out.push(value)
1413
+ }
1414
+ out
1415
+ }
1416
+
1417
+ ///|
1418
+ fn read_mouse_down_from_io_state(io_state : Map[String, Json]) -> Bool {
1419
+ match io_state.get("mouse") {
1420
+ Some(Object(obj)) =>
1421
+ match obj.get("isDown") {
1422
+ Some(value) => json_to_bool_value(value)
1423
+ None =>
1424
+ match obj.get("down") {
1425
+ Some(value) => json_to_bool_value(value)
1426
+ None => false
1427
+ }
1428
+ }
1429
+ Some(value) => json_to_bool_value(value)
1430
+ None =>
1431
+ match io_state.get("mousedown") {
1432
+ Some(value) => json_to_bool_value(value)
1433
+ None => false
1434
+ }
1435
+ }
1436
+ }
1437
+
1438
+ ///|
1439
+ fn read_mouse_targets_from_io_state(
1440
+ io_state : Map[String, Json],
1441
+ ) -> (Bool, Array[String]) {
1442
+ let mut stage = false
1443
+ let targets = []
1444
+ match io_state.get("mouse_targets") {
1445
+ Some(True) => stage = true
1446
+ Some(False) => ()
1447
+ Some(payload) =>
1448
+ match payload {
1449
+ String(_) | Number(_, ..) =>
1450
+ push_string_if_non_empty(targets, payload, false)
1451
+ Array(items) =>
1452
+ for item in items {
1453
+ push_string_if_non_empty(targets, item, false)
1454
+ }
1455
+ Object(obj) => {
1456
+ match obj.get("stage") {
1457
+ Some(value) => stage = json_to_bool_value(value)
1458
+ None => ()
1459
+ }
1460
+ match obj.get("target") {
1461
+ Some(value) => push_string_if_non_empty(targets, value, false)
1462
+ None => ()
1463
+ }
1464
+ match obj.get("targets") {
1465
+ Some(Array(items)) =>
1466
+ for item in items {
1467
+ push_string_if_non_empty(targets, item, false)
1468
+ }
1469
+ _ => ()
1470
+ }
1471
+ match obj.get("sprite") {
1472
+ Some(value) => push_string_if_non_empty(targets, value, false)
1473
+ None => ()
1474
+ }
1475
+ match obj.get("sprites") {
1476
+ Some(Array(items)) =>
1477
+ for item in items {
1478
+ push_string_if_non_empty(targets, item, false)
1479
+ }
1480
+ _ => ()
1481
+ }
1482
+ }
1483
+ _ => ()
1484
+ }
1485
+ _ => ()
1486
+ }
1487
+ (stage, unique_names(targets))
1488
+ }
1489
+
1490
+ ///|
1491
+ fn has_mouse_down_rising_edge(vm : Vm) -> Bool {
1492
+ let current = read_mouse_down_from_io_state(vm.io_state)
1493
+ let previous = read_mouse_down_from_io_state(vm.io_prev_state)
1494
+ current && !previous
1495
+ }
1496
+
1497
+ ///|
1498
+ fn dispatch_io_event_hats(vm : Vm) -> Int {
1499
+ let mut count = 0
1500
+
1501
+ let current_keys = parse_key_state_from_io(vm.io_state)
1502
+ let previous_keys = parse_key_state_from_io(vm.io_prev_state)
1503
+ for key in newly_added_names(current_keys, previous_keys) {
1504
+ count += spawn_hats_for_key(vm, key)
1505
+ }
1506
+
1507
+ if has_mouse_down_rising_edge(vm) {
1508
+ let (stage_clicked, sprite_targets) = read_mouse_targets_from_io_state(
1509
+ vm.io_state,
1510
+ )
1511
+ if stage_clicked {
1512
+ count += spawn_stage_clicked_hats(vm)
1513
+ }
1514
+ for name in sprite_targets {
1515
+ count += spawn_sprite_clicked_hats(vm, name)
1516
+ }
1517
+ }
1518
+
1519
+ let current_backdrops = parse_backdrop_state_from_io(vm.io_state)
1520
+ let previous_backdrops = parse_backdrop_state_from_io(vm.io_prev_state)
1521
+ for name in newly_added_names(current_backdrops, previous_backdrops) {
1522
+ count += spawn_hats_for_backdrop(vm, name, None)
1523
+ }
1524
+ count
1525
+ }
1526
+
1527
+ ///|
1528
+ fn spawn_hats_for_key(vm : Vm, key : String) -> Int {
1529
+ let key = lower_trim(key)
1530
+ let mut count = 0
1531
+ for target_index, target in vm.targets {
1532
+ if target.deleted {
1533
+ continue
1534
+ }
1535
+ for hat in target.key_pressed_hats {
1536
+ if hat.key_option == "any" || hat.key_option == key {
1537
+ spawn_thread(vm, target_index, hat.start_block, None)
1538
+ count += 1
1539
+ }
1540
+ }
1541
+ }
1542
+ count
1543
+ }
1544
+
1545
+ ///|
1546
+ fn spawn_stage_clicked_hats(vm : Vm) -> Int {
1547
+ if vm.stage_index < 0 || vm.stage_index >= vm.targets.length() {
1548
+ return 0
1549
+ }
1550
+ let target_index = vm.stage_index
1551
+ let stage = vm.targets[target_index]
1552
+ if stage.deleted {
1553
+ return 0
1554
+ }
1555
+
1556
+ let mut count = 0
1557
+ for start in stage.stage_clicked_starts {
1558
+ spawn_thread(vm, target_index, start, None)
1559
+ count += 1
1560
+ }
1561
+ count
1562
+ }
1563
+
1564
+ ///|
1565
+ fn spawn_sprite_clicked_hats(vm : Vm, sprite_name : String) -> Int {
1566
+ let mut count = 0
1567
+ for target_index, target in vm.targets {
1568
+ if target.deleted || target.is_stage || target.name != sprite_name {
1569
+ continue
1570
+ }
1571
+ for start in target.sprite_clicked_starts {
1572
+ spawn_thread(vm, target_index, start, None)
1573
+ count += 1
1574
+ }
1575
+ }
1576
+ count
1577
+ }
1578
+
1579
+ ///|
1580
+ fn read_loudness(vm : Vm) -> Double {
1581
+ match vm.io_state.get("loudness") {
1582
+ Some(Number(n, ..)) => n
1583
+ Some(Object(obj)) => object_get_number_or(obj, "value", 0.0)
1584
+ Some(String(raw)) =>
1585
+ match parse_double_or_none(raw) {
1586
+ Some(value) => value
1587
+ None => 0.0
1588
+ }
1589
+ _ => 0.0
1590
+ }
1591
+ }
1592
+
1593
+ ///|
1594
+ fn read_touching_from_io(
1595
+ vm : Vm,
1596
+ target_name : String,
1597
+ option : String,
1598
+ ) -> Bool {
1599
+ let option = lower_trim(option)
1600
+ match vm.io_state.get("touching") {
1601
+ Some(Object(map)) =>
1602
+ match map.get(target_name) {
1603
+ Some(True) => true
1604
+ Some(False) => false
1605
+ Some(String(raw)) => {
1606
+ let value = lower_trim(raw)
1607
+ value == option || value == "any" || value == "_any_"
1608
+ }
1609
+ Some(Array(items)) =>
1610
+ items.any(fn(item) {
1611
+ let value = lower_trim(json_to_string_value(item))
1612
+ value == option || value == "any" || value == "_any_"
1613
+ })
1614
+ Some(Object(obj)) =>
1615
+ match obj.get(option) {
1616
+ Some(value) => json_to_bool_value(value)
1617
+ None =>
1618
+ match obj.get("any") {
1619
+ Some(value) => json_to_bool_value(value)
1620
+ None => false
1621
+ }
1622
+ }
1623
+ _ => false
1624
+ }
1625
+ _ => false
1626
+ }
1627
+ }
1628
+
1629
+ ///|
1630
+ fn predicate_state_key(target_index : Int, hat_id : String) -> String {
1631
+ "\{target_index}:\{hat_id}"
1632
+ }
1633
+
1634
+ ///|
1635
+ fn hat_predicate_value(
1636
+ vm : Vm,
1637
+ target_index : Int,
1638
+ target : TargetState,
1639
+ predicate : PredicateHatStart,
1640
+ ) -> Bool {
1641
+ match target.blocks.get(predicate.hat_id) {
1642
+ Some(hat) =>
1643
+ match predicate.kind {
1644
+ PredicateHatKind::WhenGreaterThan => {
1645
+ let threshold = json_to_number_value(
1646
+ value_from_input(vm, target_index, hat, "VALUE", 0),
1647
+ )
1648
+ let current = if predicate.menu == "timer" {
1649
+ Double::from_int(vm.now_ms - vm.timer_start_ms) / 1000.0
1650
+ } else if predicate.menu == "loudness" {
1651
+ read_loudness(vm)
1652
+ } else {
1653
+ 0.0
1654
+ }
1655
+ current > threshold
1656
+ }
1657
+ PredicateHatKind::WhenTouchingObject =>
1658
+ read_touching_from_io(vm, target.name, predicate.menu)
1659
+ }
1660
+ None => false
1661
+ }
1662
+ }
1663
+
1664
+ ///|
1665
+ fn spawn_predicate_hats(vm : Vm) -> Int {
1666
+ let mut count = 0
1667
+ for target_index, target in vm.targets {
1668
+ if target.deleted {
1669
+ continue
1670
+ }
1671
+ for predicate in target.predicate_hats {
1672
+ let key = predicate_state_key(target_index, predicate.hat_id)
1673
+ let previous = vm.hat_predicates.get_or_default(key, false)
1674
+ let current = hat_predicate_value(vm, target_index, target, predicate)
1675
+ if current && !previous {
1676
+ spawn_thread(vm, target_index, predicate.start_block, None)
1677
+ count += 1
1678
+ }
1679
+ vm.hat_predicates[key] = current
1680
+ }
1681
+ }
1682
+ count
1683
+ }
1684
+
1685
+ ///|
1686
+ fn spawn_hats_for_message(
1687
+ vm : Vm,
1688
+ message : String,
1689
+ parent_waiter : Int?,
1690
+ ) -> Int {
1691
+ let mut count = 0
1692
+ for target_index, target in vm.targets {
1693
+ if target.deleted {
1694
+ continue
1695
+ }
1696
+ for hat in target.broadcast_hats {
1697
+ if hat.message == message {
1698
+ spawn_thread(vm, target_index, hat.start_block, parent_waiter)
1699
+ count += 1
1700
+ }
1701
+ }
1702
+ }
1703
+ count
1704
+ }
1705
+
1706
+ ///|
1707
+ fn spawn_aot_green_flag_hats(vm : Vm) -> Int {
1708
+ let mut count = 0
1709
+ for target_index, target in vm.targets {
1710
+ if target.deleted {
1711
+ continue
1712
+ }
1713
+ match vm.aot_full_green_flag_starts.get(target_index) {
1714
+ Some(start_pcs) =>
1715
+ for start_pc in start_pcs {
1716
+ if spawn_thread_pc(vm, target_index, start_pc, None) {
1717
+ count += 1
1718
+ }
1719
+ }
1720
+ None => ()
1721
+ }
1722
+ }
1723
+ count
1724
+ }
1725
+
1726
+ ///|
1727
+ fn spawn_green_flag_hats(vm : Vm) -> Int {
1728
+ let mut count = 0
1729
+ for target_index, target in vm.targets {
1730
+ if target.deleted {
1731
+ continue
1732
+ }
1733
+ for start in target.green_flag_starts {
1734
+ spawn_thread(vm, target_index, start, None)
1735
+ count += 1
1736
+ }
1737
+ }
1738
+ count
1739
+ }
1740
+
1741
+ ///|
1742
+ fn resolve_thread_input_wait(vm : Vm, thread : Thread) -> Thread {
1743
+ match thread.wait_for_input {
1744
+ Some(key) => {
1745
+ match vm.io_state.get(key) {
1746
+ Some(value) => {
1747
+ if key == "answer" {
1748
+ vm.answer = json_to_string_value(value)
1749
+ }
1750
+ thread.wait_for_input = None
1751
+ }
1752
+ None => ()
1753
+ }
1754
+ thread
1755
+ }
1756
+ None => thread
1757
+ }
1758
+ }
1759
+
1760
+ ///|
1761
+ fn is_thread_blocked(vm : Vm, thread : Thread) -> Bool {
1762
+ let waiting_time = match thread.wait_until_ms {
1763
+ Some(until_ms) if vm.now_ms < until_ms => true
1764
+ Some(_) => false
1765
+ None => false
1766
+ }
1767
+ let waiting_input = match thread.wait_for_input {
1768
+ Some(_) => true
1769
+ None => false
1770
+ }
1771
+ let waiting_children = vm.waiting_children.get_or_default(thread.id, 0) > 0
1772
+ waiting_time || waiting_input || waiting_children
1773
+ }
1774
+
1775
+ ///|
1776
+ fn is_warp_window_active(vm : Vm, thread : Thread) -> Bool {
1777
+ ignore(vm)
1778
+ thread.warp_mode
1779
+ }
1780
+
1781
+ ///|
1782
+ fn has_active_warp_thread(vm : Vm) -> Bool {
1783
+ for thread in vm.threads {
1784
+ if thread.done {
1785
+ continue
1786
+ }
1787
+ if thread.warp_mode {
1788
+ return true
1789
+ }
1790
+ match vm.procedure_frames.get(thread.id) {
1791
+ Some(frames) =>
1792
+ for frame in frames {
1793
+ if frame.warp_mode {
1794
+ return true
1795
+ }
1796
+ }
1797
+ None => ()
1798
+ }
1799
+ }
1800
+ false
1801
+ }
1802
+
1803
+ ///|
1804
+ fn is_current_thread_in_warp_context(vm : Vm) -> Bool {
1805
+ match vm.current_thread_id {
1806
+ Some(thread_id) => {
1807
+ for thread in vm.threads {
1808
+ if thread.id == thread_id {
1809
+ if thread.warp_mode {
1810
+ return true
1811
+ }
1812
+ break
1813
+ }
1814
+ }
1815
+ match vm.procedure_frames.get(thread_id) {
1816
+ Some(frames) =>
1817
+ for frame in frames {
1818
+ if frame.warp_mode {
1819
+ return true
1820
+ }
1821
+ }
1822
+ None => ()
1823
+ }
1824
+ false
1825
+ }
1826
+ None => false
1827
+ }
1828
+ }
1829
+
1830
+ ///|
1831
+ fn request_redraw(vm : Vm) -> Unit {
1832
+ if !vm.redraw_requested {
1833
+ vm.render_revision += 1
1834
+ }
1835
+ vm.redraw_requested = true
1836
+ vm.render_cache_valid = false
1837
+ if is_current_thread_in_warp_context(vm) {
1838
+ vm.redraw_requested_while_warp = true
1839
+ }
1840
+ }
1841
+
1842
+ ///|
1843
+ fn target_block_pc_from_id(target : TargetState, block_id : String?) -> Int? {
1844
+ match block_id {
1845
+ Some(id) => target.block_pc_by_id.get(id)
1846
+ None => None
1847
+ }
1848
+ }
1849
+
1850
+ ///|
1851
+ fn target_block_from_pc(target : TargetState, block_pc : Int) -> ScratchBlock? {
1852
+ if block_pc < 0 || block_pc >= target.blocks_by_pc.length() {
1853
+ None
1854
+ } else {
1855
+ Some(target.blocks_by_pc[block_pc])
1856
+ }
1857
+ }
1858
+
1859
+ ///|
1860
+ fn execute_list_delete(
1861
+ vm : Vm,
1862
+ target_index : Int,
1863
+ block : ScratchBlock,
1864
+ ) -> Unit {
1865
+ match field_value(block, "LIST") {
1866
+ Some((list_name, list_id)) =>
1867
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
1868
+ Some((owner, id, slot)) => {
1869
+ let list = read_list_by_ref(vm, owner, id, slot)
1870
+ let index_value = value_from_input(
1871
+ vm, target_index, block, "INDEX", 0,
1872
+ )
1873
+ let key = json_to_string_value(index_value).trim().to_lower()
1874
+ if key == "all" {
1875
+ list.clear()
1876
+ } else {
1877
+ match
1878
+ normalize_index(index_value, list.length(), next_random_unit(vm)) {
1879
+ Some(index) =>
1880
+ if index >= 0 && index < list.length() {
1881
+ ignore(list.remove(index))
1882
+ }
1883
+ None => ()
1884
+ }
1885
+ }
1886
+ write_list_by_ref(vm, owner, id, slot, list)
1887
+ }
1888
+ None => ()
1889
+ }
1890
+ None => ()
1891
+ }
1892
+ }
1893
+
1894
+ ///|
1895
+ fn execute_list_insert(
1896
+ vm : Vm,
1897
+ target_index : Int,
1898
+ block : ScratchBlock,
1899
+ ) -> Unit {
1900
+ match field_value(block, "LIST") {
1901
+ Some((list_name, list_id)) =>
1902
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
1903
+ Some((owner, id, slot)) => {
1904
+ let list = read_list_by_ref(vm, owner, id, slot)
1905
+ let item = value_from_input(vm, target_index, block, "ITEM", 0)
1906
+ let index_value = value_from_input(
1907
+ vm, target_index, block, "INDEX", 0,
1908
+ )
1909
+ let raw = json_to_string_value(index_value).trim().to_lower()
1910
+ let idx = if raw == "last" {
1911
+ list.length()
1912
+ } else if raw == "random" || raw == "any" {
1913
+ let sampled = (next_random_unit(vm) *
1914
+ Double::from_int(list.length() + 1))
1915
+ .floor()
1916
+ .to_int()
1917
+ sampled.clamp(min=0, max=list.length())
1918
+ } else {
1919
+ let n = json_to_number_value(index_value).to_int() - 1
1920
+ n.clamp(min=0, max=list.length())
1921
+ }
1922
+ list.insert(idx, item)
1923
+ write_list_by_ref(vm, owner, id, slot, list)
1924
+ }
1925
+ None => ()
1926
+ }
1927
+ None => ()
1928
+ }
1929
+ }
1930
+
1931
+ ///|
1932
+ fn execute_list_replace(
1933
+ vm : Vm,
1934
+ target_index : Int,
1935
+ block : ScratchBlock,
1936
+ ) -> Unit {
1937
+ match field_value(block, "LIST") {
1938
+ Some((list_name, list_id)) =>
1939
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
1940
+ Some((owner, id, slot)) => {
1941
+ let list = read_list_by_ref(vm, owner, id, slot)
1942
+ let item = value_from_input(vm, target_index, block, "ITEM", 0)
1943
+ let index_value = value_from_input(
1944
+ vm, target_index, block, "INDEX", 0,
1945
+ )
1946
+ match
1947
+ normalize_index(index_value, list.length(), next_random_unit(vm)) {
1948
+ Some(index) =>
1949
+ if index >= 0 && index < list.length() {
1950
+ list[index] = item
1951
+ }
1952
+ None => ()
1953
+ }
1954
+ write_list_by_ref(vm, owner, id, slot, list)
1955
+ }
1956
+ None => ()
1957
+ }
1958
+ None => ()
1959
+ }
1960
+ }
1961
+
1962
+ ///|
1963
+ fn increment_block_hot_count(
1964
+ vm : Vm,
1965
+ target_index : Int,
1966
+ block_id : String,
1967
+ ) -> Int {
1968
+ let block_hot_key = "\{target_index}:\{block_id}"
1969
+ let block_hot_count = vm.hot_op_counts.get_or_default(block_hot_key, 0) + 1
1970
+ vm.hot_op_counts[block_hot_key] = block_hot_count
1971
+ block_hot_count
1972
+ }
1973
+
1974
+ ///|
1975
+ fn execute_thread_once(vm : Vm, thread : Thread) -> Thread {
1976
+ let mut thread = thread
1977
+ if thread.done {
1978
+ return thread
1979
+ }
1980
+
1981
+ thread = resolve_thread_input_wait(vm, thread)
1982
+ if thread.wait_for_input is Some(_) {
1983
+ return thread
1984
+ }
1985
+
1986
+ match thread.wait_until_ms {
1987
+ Some(until_ms) => {
1988
+ if vm.now_ms < until_ms {
1989
+ return thread
1990
+ }
1991
+ thread.wait_until_ms = None
1992
+ }
1993
+ None => ()
1994
+ }
1995
+
1996
+ if vm.waiting_children.get_or_default(thread.id, 0) > 0 {
1997
+ return thread
1998
+ }
1999
+
2000
+ let block_pc = match thread.pc {
2001
+ Some(pc) => pc
2002
+ None => {
2003
+ thread.done = true
2004
+ return thread
2005
+ }
2006
+ }
2007
+
2008
+ let target_index = thread.target_index
2009
+ if target_index < 0 || target_index >= vm.targets.length() {
2010
+ thread.done = true
2011
+ return thread
2012
+ }
2013
+ if vm.targets[target_index].deleted {
2014
+ thread.done = true
2015
+ return thread
2016
+ }
2017
+ let target = vm.targets[target_index]
2018
+ let block = match target_block_from_pc(target, block_pc) {
2019
+ Some(value) => value
2020
+ None => {
2021
+ thread.pc = None
2022
+ return unwind_control(vm, thread)
2023
+ }
2024
+ }
2025
+ let block_meta = target.block_fast_meta_by_pc[block_pc]
2026
+ let mut next_pc = block_meta.next_pc
2027
+ let mut handled_hot_opcode = false
2028
+
2029
+ vm.current_thread_id = Some(thread.id)
2030
+ match block.opcode_tag {
2031
+ OpcodeTag::MotionChangeXBy => {
2032
+ let value = number_from_input(vm, target_index, block, "DX", 0)
2033
+ move_target_with_pen(
2034
+ vm,
2035
+ target_index,
2036
+ vm.targets[target_index].x + value,
2037
+ vm.targets[target_index].y,
2038
+ )
2039
+ handled_hot_opcode = true
2040
+ }
2041
+ OpcodeTag::MotionChangeYBy => {
2042
+ let value = number_from_input(vm, target_index, block, "DY", 0)
2043
+ move_target_with_pen(
2044
+ vm,
2045
+ target_index,
2046
+ vm.targets[target_index].x,
2047
+ vm.targets[target_index].y + value,
2048
+ )
2049
+ handled_hot_opcode = true
2050
+ }
2051
+ OpcodeTag::MotionSetY => {
2052
+ move_target_with_pen(
2053
+ vm,
2054
+ target_index,
2055
+ vm.targets[target_index].x,
2056
+ number_from_input(vm, target_index, block, "Y", 0),
2057
+ )
2058
+ handled_hot_opcode = true
2059
+ }
2060
+ OpcodeTag::PenPenDown => {
2061
+ vm.targets[target_index].pen_down = true
2062
+ render_draw_pen_point(vm, target_index)
2063
+ request_redraw(vm)
2064
+ handled_hot_opcode = true
2065
+ }
2066
+ OpcodeTag::PenPenUp => {
2067
+ vm.targets[target_index].pen_down = false
2068
+ handled_hot_opcode = true
2069
+ }
2070
+ OpcodeTag::ControlRepeat => {
2071
+ let block_hot_count = increment_block_hot_count(
2072
+ vm,
2073
+ target_index,
2074
+ block.id,
2075
+ )
2076
+ let times = json_to_number_value(
2077
+ value_from_input(vm, target_index, block, "TIMES", 0),
2078
+ )
2079
+ .floor()
2080
+ .to_int()
2081
+ if times > 0 {
2082
+ let substack_pc = block_meta.substack_pc
2083
+ let use_fast_path = times >= 32 || block_hot_count >= 8
2084
+ if use_fast_path &&
2085
+ try_fast_repeat_numeric_variable_loop(
2086
+ vm, target_index, times, substack_pc,
2087
+ ) {
2088
+ next_pc = block_meta.next_pc
2089
+ } else if use_fast_path &&
2090
+ try_fast_repeat_motion_step(vm, target_index, times, substack_pc) {
2091
+ next_pc = block_meta.next_pc
2092
+ } else {
2093
+ thread.stack.push(
2094
+ repeat_frame(times, substack_pc, block_meta.next_pc),
2095
+ )
2096
+ next_pc = substack_pc
2097
+ }
2098
+ }
2099
+ handled_hot_opcode = true
2100
+ }
2101
+ OpcodeTag::ControlRepeatUntil => {
2102
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2103
+ if !condition {
2104
+ thread.stack.push(
2105
+ repeat_frame(1, block_meta.substack_pc, Some(block_pc)),
2106
+ )
2107
+ next_pc = block_meta.substack_pc
2108
+ }
2109
+ handled_hot_opcode = true
2110
+ }
2111
+ OpcodeTag::ControlIf => {
2112
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2113
+ if condition {
2114
+ thread.stack.push(
2115
+ repeat_frame(1, block_meta.substack_pc, block_meta.next_pc),
2116
+ )
2117
+ next_pc = block_meta.substack_pc
2118
+ }
2119
+ handled_hot_opcode = true
2120
+ }
2121
+ OpcodeTag::DataSetVariableTo => {
2122
+ match block_meta.variable {
2123
+ Some(variable_ref) => {
2124
+ let resolved = match variable_ref.id {
2125
+ Some(variable_id) =>
2126
+ resolve_variable_ref_by_id(vm, target_index, variable_id)
2127
+ None =>
2128
+ resolve_variable_ref(
2129
+ vm,
2130
+ target_index,
2131
+ None,
2132
+ Some(variable_ref.name),
2133
+ )
2134
+ }
2135
+ match
2136
+ fast_numeric_value_for_setvariable(vm, target, target_index, block) {
2137
+ Some(value) =>
2138
+ match resolved {
2139
+ Some((owner_index, variable_id, variable_slot)) =>
2140
+ write_variable_by_ref(
2141
+ vm,
2142
+ owner_index,
2143
+ variable_id,
2144
+ variable_slot,
2145
+ json_number(value),
2146
+ )
2147
+ None =>
2148
+ write_variable(
2149
+ vm,
2150
+ target_index,
2151
+ variable_ref.id,
2152
+ Some(variable_ref.name),
2153
+ json_number(value),
2154
+ )
2155
+ }
2156
+ None => {
2157
+ let value = value_from_input(vm, target_index, block, "VALUE", 0)
2158
+ match resolved {
2159
+ Some((owner_index, variable_id, variable_slot)) =>
2160
+ write_variable_by_ref(
2161
+ vm, owner_index, variable_id, variable_slot, value,
2162
+ )
2163
+ None =>
2164
+ write_variable(
2165
+ vm,
2166
+ target_index,
2167
+ variable_ref.id,
2168
+ Some(variable_ref.name),
2169
+ value,
2170
+ )
2171
+ }
2172
+ }
2173
+ }
2174
+ }
2175
+ None => ()
2176
+ }
2177
+ handled_hot_opcode = true
2178
+ }
2179
+ OpcodeTag::DataChangeVariableBy => {
2180
+ match block_meta.variable {
2181
+ Some(variable_ref) => {
2182
+ let delta = number_from_input(vm, target_index, block, "VALUE", 0)
2183
+ let resolved = match variable_ref.id {
2184
+ Some(variable_id) =>
2185
+ resolve_variable_ref_by_id(vm, target_index, variable_id)
2186
+ None =>
2187
+ resolve_variable_ref(
2188
+ vm,
2189
+ target_index,
2190
+ None,
2191
+ Some(variable_ref.name),
2192
+ )
2193
+ }
2194
+ match resolved {
2195
+ Some((owner_index, variable_id, variable_slot)) => {
2196
+ let current = json_to_number_value(
2197
+ read_variable_by_ref(
2198
+ vm, owner_index, variable_id, variable_slot,
2199
+ ),
2200
+ )
2201
+ write_variable_by_ref(
2202
+ vm,
2203
+ owner_index,
2204
+ variable_id,
2205
+ variable_slot,
2206
+ json_number(current + delta),
2207
+ )
2208
+ }
2209
+ None => {
2210
+ let current = json_to_number_value(
2211
+ read_variable(
2212
+ vm,
2213
+ target_index,
2214
+ variable_ref.id,
2215
+ Some(variable_ref.name),
2216
+ ),
2217
+ )
2218
+ write_variable(
2219
+ vm,
2220
+ target_index,
2221
+ variable_ref.id,
2222
+ Some(variable_ref.name),
2223
+ json_number(current + delta),
2224
+ )
2225
+ }
2226
+ }
2227
+ }
2228
+ None => ()
2229
+ }
2230
+ handled_hot_opcode = true
2231
+ }
2232
+ OpcodeTag::Unknown => ()
2233
+ }
2234
+ if handled_hot_opcode {
2235
+ vm.current_thread_id = None
2236
+ if thread.done {
2237
+ return thread
2238
+ }
2239
+ thread.pc = next_pc
2240
+ if thread.pc is None {
2241
+ return unwind_control(vm, thread)
2242
+ }
2243
+ return thread
2244
+ }
2245
+ match block.opcode {
2246
+ "motion_movesteps" => {
2247
+ let steps = json_to_number_value(
2248
+ value_from_input(vm, target_index, block, "STEPS", 0),
2249
+ )
2250
+ let radians = (90.0 - vm.targets[target_index].direction) *
2251
+ @math.PI /
2252
+ 180.0
2253
+ move_target_with_pen(
2254
+ vm,
2255
+ target_index,
2256
+ vm.targets[target_index].x + @math.cos(radians) * steps,
2257
+ vm.targets[target_index].y + @math.sin(radians) * steps,
2258
+ )
2259
+ }
2260
+ "motion_turnright" => {
2261
+ let old_direction = vm.targets[target_index].direction
2262
+ let degrees = json_to_number_value(
2263
+ value_from_input(vm, target_index, block, "DEGREES", 0),
2264
+ )
2265
+ vm.targets[target_index].direction += degrees
2266
+ if old_direction != vm.targets[target_index].direction {
2267
+ request_redraw(vm)
2268
+ }
2269
+ }
2270
+ "motion_turnleft" => {
2271
+ let old_direction = vm.targets[target_index].direction
2272
+ let degrees = json_to_number_value(
2273
+ value_from_input(vm, target_index, block, "DEGREES", 0),
2274
+ )
2275
+ vm.targets[target_index].direction -= degrees
2276
+ if old_direction != vm.targets[target_index].direction {
2277
+ request_redraw(vm)
2278
+ }
2279
+ }
2280
+ "motion_pointindirection" => {
2281
+ let old_direction = vm.targets[target_index].direction
2282
+ vm.targets[target_index].direction = normalized_scratch_direction(
2283
+ json_to_number_value(
2284
+ value_from_input(vm, target_index, block, "DIRECTION", 0),
2285
+ ),
2286
+ )
2287
+ if old_direction != vm.targets[target_index].direction {
2288
+ request_redraw(vm)
2289
+ }
2290
+ }
2291
+ "motion_pointtowards" => {
2292
+ let menu_value = json_to_string_value(
2293
+ value_from_input(vm, target_index, block, "TOWARDS", 0),
2294
+ )
2295
+ match resolve_motion_menu_target(vm, target_index, menu_value) {
2296
+ Some((x, y)) => point_towards_position(vm, target_index, x, y)
2297
+ None => ()
2298
+ }
2299
+ }
2300
+ "motion_changexby" => {
2301
+ let value = number_from_input(vm, target_index, block, "DX", 0)
2302
+ move_target_with_pen(
2303
+ vm,
2304
+ target_index,
2305
+ vm.targets[target_index].x + value,
2306
+ vm.targets[target_index].y,
2307
+ )
2308
+ }
2309
+ "motion_changeyby" => {
2310
+ let value = number_from_input(vm, target_index, block, "DY", 0)
2311
+ move_target_with_pen(
2312
+ vm,
2313
+ target_index,
2314
+ vm.targets[target_index].x,
2315
+ vm.targets[target_index].y + value,
2316
+ )
2317
+ }
2318
+ "motion_setx" =>
2319
+ move_target_with_pen(
2320
+ vm,
2321
+ target_index,
2322
+ number_from_input(vm, target_index, block, "X", 0),
2323
+ vm.targets[target_index].y,
2324
+ )
2325
+ "motion_sety" =>
2326
+ move_target_with_pen(
2327
+ vm,
2328
+ target_index,
2329
+ vm.targets[target_index].x,
2330
+ number_from_input(vm, target_index, block, "Y", 0),
2331
+ )
2332
+ "motion_gotoxy" =>
2333
+ move_target_with_pen(
2334
+ vm,
2335
+ target_index,
2336
+ number_from_input(vm, target_index, block, "X", 0),
2337
+ number_from_input(vm, target_index, block, "Y", 0),
2338
+ )
2339
+ "motion_goto" => {
2340
+ let menu_value = json_to_string_value(
2341
+ value_from_input(vm, target_index, block, "TO", 0),
2342
+ )
2343
+ match resolve_motion_menu_target(vm, target_index, menu_value) {
2344
+ Some((x, y)) => move_target_with_pen(vm, target_index, x, y)
2345
+ None => ()
2346
+ }
2347
+ }
2348
+ "motion_glidesecstoxy" => {
2349
+ let seconds = json_to_number_value(
2350
+ value_from_input(vm, target_index, block, "SECS", 0),
2351
+ )
2352
+ move_target_with_pen(
2353
+ vm,
2354
+ target_index,
2355
+ json_to_number_value(value_from_input(vm, target_index, block, "X", 0)),
2356
+ json_to_number_value(value_from_input(vm, target_index, block, "Y", 0)),
2357
+ )
2358
+ let duration = if seconds < 0.0 { 0.0 } else { seconds }
2359
+ thread.wait_until_ms = Some(vm.now_ms + (duration * 1000.0).to_int())
2360
+ }
2361
+ "motion_glideto" => {
2362
+ let seconds = json_to_number_value(
2363
+ value_from_input(vm, target_index, block, "SECS", 0),
2364
+ )
2365
+ let menu_value = json_to_string_value(
2366
+ value_from_input(vm, target_index, block, "TO", 0),
2367
+ )
2368
+ match resolve_motion_menu_target(vm, target_index, menu_value) {
2369
+ Some((x, y)) => move_target_with_pen(vm, target_index, x, y)
2370
+ None => ()
2371
+ }
2372
+ let duration = if seconds < 0.0 { 0.0 } else { seconds }
2373
+ thread.wait_until_ms = Some(vm.now_ms + (duration * 1000.0).to_int())
2374
+ }
2375
+ "motion_ifonedgebounce" => apply_if_on_edge_bounce(vm, target_index)
2376
+ "motion_setrotationstyle"
2377
+ | "motion_align_scene"
2378
+ | "motion_scroll_right"
2379
+ | "motion_scroll_up" => ()
2380
+ "looks_show" => {
2381
+ let was_visible = vm.targets[target_index].visible
2382
+ vm.targets[target_index].visible = true
2383
+ if !was_visible {
2384
+ request_redraw(vm)
2385
+ }
2386
+ }
2387
+ "looks_hide" => {
2388
+ let was_visible = vm.targets[target_index].visible
2389
+ vm.targets[target_index].visible = false
2390
+ if was_visible {
2391
+ request_redraw(vm)
2392
+ }
2393
+ }
2394
+ "looks_hideallsprites" => {
2395
+ let mut changed = false
2396
+ for i, candidate in vm.targets {
2397
+ if !candidate.deleted && !candidate.is_stage {
2398
+ if vm.targets[i].visible {
2399
+ changed = true
2400
+ }
2401
+ vm.targets[i].visible = false
2402
+ }
2403
+ }
2404
+ if changed {
2405
+ request_redraw(vm)
2406
+ }
2407
+ }
2408
+ "looks_switchcostumeto" => {
2409
+ let before = vm.targets[target_index].current_costume
2410
+ let input_value = value_from_input(vm, target_index, block, "COSTUME", 0)
2411
+ let resolved = match
2412
+ resolve_target_costume_index(vm, target_index, input_value, false) {
2413
+ Some(index) => Some(index)
2414
+ None =>
2415
+ match field_value(block, "COSTUME") {
2416
+ Some((name, _)) =>
2417
+ resolve_target_costume_index(
2418
+ vm,
2419
+ target_index,
2420
+ json_string(name),
2421
+ false,
2422
+ )
2423
+ None => None
2424
+ }
2425
+ }
2426
+ match resolved {
2427
+ Some(index) => vm.targets[target_index].current_costume = index
2428
+ None => ()
2429
+ }
2430
+ if before != vm.targets[target_index].current_costume {
2431
+ request_redraw(vm)
2432
+ }
2433
+ }
2434
+ "looks_nextcostume" => {
2435
+ let target = vm.targets[target_index]
2436
+ let before = target.current_costume
2437
+ vm.targets[target_index].current_costume = normalized_target_costume_index(
2438
+ target,
2439
+ target.current_costume + 1,
2440
+ )
2441
+ if before != vm.targets[target_index].current_costume {
2442
+ request_redraw(vm)
2443
+ }
2444
+ }
2445
+ "looks_switchbackdropto" => {
2446
+ let value = block_backdrop_value(vm, target_index, block)
2447
+ ignore(set_stage_backdrop_from_value(vm, value, None))
2448
+ }
2449
+ "looks_switchbackdroptoandwait" => {
2450
+ let value = block_backdrop_value(vm, target_index, block)
2451
+ let spawned = set_stage_backdrop_from_value(vm, value, Some(thread.id))
2452
+ if spawned > 0 {
2453
+ vm.waiting_children[thread.id] = spawned
2454
+ }
2455
+ }
2456
+ "looks_nextbackdrop" =>
2457
+ if vm.stage_index >= 0 && vm.stage_index < vm.targets.length() {
2458
+ let stage = vm.targets[vm.stage_index]
2459
+ ignore(set_stage_backdrop_index(vm, stage.current_costume + 1, None))
2460
+ }
2461
+ "looks_changeeffectby" => {
2462
+ let effect = input_or_field_string(
2463
+ vm, target_index, block, "EFFECT", "EFFECT",
2464
+ )
2465
+ let amount = json_to_number_value(
2466
+ value_from_input(vm, target_index, block, "CHANGE", 0),
2467
+ )
2468
+ set_target_looks_effect(vm, target_index, effect, amount, true)
2469
+ request_redraw(vm)
2470
+ }
2471
+ "looks_seteffectto" => {
2472
+ let effect = input_or_field_string(
2473
+ vm, target_index, block, "EFFECT", "EFFECT",
2474
+ )
2475
+ let amount = json_to_number_value(
2476
+ value_from_input(vm, target_index, block, "VALUE", 0),
2477
+ )
2478
+ set_target_looks_effect(vm, target_index, effect, amount, false)
2479
+ request_redraw(vm)
2480
+ }
2481
+ "looks_cleargraphiceffects" => {
2482
+ clear_target_looks_effect(vm, target_index)
2483
+ request_redraw(vm)
2484
+ }
2485
+ "looks_changestretchby"
2486
+ | "looks_setstretchto"
2487
+ | "looks_gotofrontback"
2488
+ | "looks_goforwardbackwardlayers" => ()
2489
+ "looks_changesizeby" => {
2490
+ let delta = json_to_number_value(
2491
+ value_from_input(vm, target_index, block, "CHANGE", 0),
2492
+ )
2493
+ vm.targets[target_index].size += delta
2494
+ request_redraw(vm)
2495
+ }
2496
+ "looks_setsizeto" => {
2497
+ let size = json_to_number_value(
2498
+ value_from_input(vm, target_index, block, "SIZE", 0),
2499
+ )
2500
+ vm.targets[target_index].size = size
2501
+ request_redraw(vm)
2502
+ }
2503
+ "looks_say" => {
2504
+ let message = json_to_string_value(
2505
+ value_from_input(vm, target_index, block, "MESSAGE", 0),
2506
+ )
2507
+ push_effect(vm, HostEffect::Say(vm.targets[target_index].name, message))
2508
+ request_redraw(vm)
2509
+ }
2510
+ "looks_think" => {
2511
+ let message = json_to_string_value(
2512
+ value_from_input(vm, target_index, block, "MESSAGE", 0),
2513
+ )
2514
+ push_effect(vm, HostEffect::Think(vm.targets[target_index].name, message))
2515
+ request_redraw(vm)
2516
+ }
2517
+ "looks_sayforsecs" => {
2518
+ let message = json_to_string_value(
2519
+ value_from_input(vm, target_index, block, "MESSAGE", 0),
2520
+ )
2521
+ let seconds = json_to_number_value(
2522
+ value_from_input(vm, target_index, block, "SECS", 0),
2523
+ )
2524
+ push_effect(vm, HostEffect::Say(vm.targets[target_index].name, message))
2525
+ let duration = if seconds < 0.0 { 0.0 } else { seconds }
2526
+ thread.wait_until_ms = Some(vm.now_ms + (duration * 1000.0).to_int())
2527
+ request_redraw(vm)
2528
+ }
2529
+ "looks_thinkforsecs" => {
2530
+ let message = json_to_string_value(
2531
+ value_from_input(vm, target_index, block, "MESSAGE", 0),
2532
+ )
2533
+ let seconds = json_to_number_value(
2534
+ value_from_input(vm, target_index, block, "SECS", 0),
2535
+ )
2536
+ push_effect(vm, HostEffect::Think(vm.targets[target_index].name, message))
2537
+ let duration = if seconds < 0.0 { 0.0 } else { seconds }
2538
+ thread.wait_until_ms = Some(vm.now_ms + (duration * 1000.0).to_int())
2539
+ request_redraw(vm)
2540
+ }
2541
+ "pen_clear" => {
2542
+ vm_clear_pen_pixels(vm)
2543
+ request_redraw(vm)
2544
+ }
2545
+ "pen_stamp" =>
2546
+ if target_index >= 0 &&
2547
+ target_index < vm.targets.length() &&
2548
+ !vm.targets[target_index].is_stage &&
2549
+ !vm.targets[target_index].deleted {
2550
+ render_stamp_sprite_to_pen(vm, target_index)
2551
+ request_redraw(vm)
2552
+ }
2553
+ "pen_penDown" => {
2554
+ vm.targets[target_index].pen_down = true
2555
+ render_draw_pen_point(vm, target_index)
2556
+ request_redraw(vm)
2557
+ }
2558
+ "pen_penUp" => vm.targets[target_index].pen_down = false
2559
+ "pen_setPenColorToColor" => {
2560
+ let color = value_from_input(vm, target_index, block, "COLOR", 0)
2561
+ let (r, g, b) = render_json_to_rgb(color)
2562
+ set_target_pen_color_from_rgb(vm, target_index, r, g, b)
2563
+ }
2564
+ "pen_changePenColorParamBy" => {
2565
+ let param = input_or_field_string(
2566
+ vm, target_index, block, "COLOR_PARAM", "COLOR_PARAM",
2567
+ )
2568
+ let value = json_to_number_value(
2569
+ value_from_input(vm, target_index, block, "VALUE", 0),
2570
+ )
2571
+ set_target_pen_color_param(vm, target_index, param, value, true)
2572
+ }
2573
+ "pen_setPenColorParamTo" => {
2574
+ let param = input_or_field_string(
2575
+ vm, target_index, block, "COLOR_PARAM", "COLOR_PARAM",
2576
+ )
2577
+ let value = json_to_number_value(
2578
+ value_from_input(vm, target_index, block, "VALUE", 0),
2579
+ )
2580
+ set_target_pen_color_param(vm, target_index, param, value, false)
2581
+ }
2582
+ "pen_changePenSizeBy" => {
2583
+ let delta = json_to_number_value(
2584
+ value_from_input(vm, target_index, block, "SIZE", 0),
2585
+ )
2586
+ vm.targets[target_index].pen_size = clamp_double(
2587
+ vm.targets[target_index].pen_size + delta,
2588
+ 1.0,
2589
+ 1200.0,
2590
+ )
2591
+ }
2592
+ "pen_setPenSizeTo" => {
2593
+ let size = json_to_number_value(
2594
+ value_from_input(vm, target_index, block, "SIZE", 0),
2595
+ )
2596
+ vm.targets[target_index].pen_size = clamp_double(size, 1.0, 1200.0)
2597
+ }
2598
+ "pen_setPenHueToNumber" => {
2599
+ let hue = json_to_number_value(
2600
+ value_from_input(vm, target_index, block, "HUE", 0),
2601
+ )
2602
+ set_target_pen_color_param(vm, target_index, "color", hue / 2.0, false)
2603
+ set_target_pen_color_param(vm, target_index, "transparency", 0.0, false)
2604
+ apply_target_legacy_pen_shade(vm, target_index)
2605
+ }
2606
+ "pen_changePenHueBy" => {
2607
+ let hue = json_to_number_value(
2608
+ value_from_input(vm, target_index, block, "HUE", 0),
2609
+ )
2610
+ set_target_pen_color_param(vm, target_index, "color", hue / 2.0, true)
2611
+ apply_target_legacy_pen_shade(vm, target_index)
2612
+ }
2613
+ "pen_setPenShadeToNumber" => {
2614
+ let mut shade = json_to_number_value(
2615
+ value_from_input(vm, target_index, block, "SHADE", 0),
2616
+ )
2617
+ shade = shade.mod(200.0)
2618
+ if shade < 0.0 {
2619
+ shade += 200.0
2620
+ }
2621
+ vm.targets[target_index].pen_legacy_shade = shade
2622
+ apply_target_legacy_pen_shade(vm, target_index)
2623
+ }
2624
+ "pen_changePenShadeBy" => {
2625
+ let delta = json_to_number_value(
2626
+ value_from_input(vm, target_index, block, "SHADE", 0),
2627
+ )
2628
+ let mut shade = vm.targets[target_index].pen_legacy_shade + delta
2629
+ shade = shade.mod(200.0)
2630
+ if shade < 0.0 {
2631
+ shade += 200.0
2632
+ }
2633
+ vm.targets[target_index].pen_legacy_shade = shade
2634
+ apply_target_legacy_pen_shade(vm, target_index)
2635
+ }
2636
+ "music_playDrumForBeats" => {
2637
+ let drum = normalize_music_drum(
2638
+ json_to_number_value(
2639
+ value_from_input(vm, target_index, block, "DRUM", 0),
2640
+ ),
2641
+ )
2642
+ let beats = clamp_music_beats(
2643
+ json_to_number_value(
2644
+ value_from_input(vm, target_index, block, "BEATS", 0),
2645
+ ),
2646
+ )
2647
+ push_effect(
2648
+ vm,
2649
+ HostEffect::PlayDrum(
2650
+ vm.targets[target_index].name,
2651
+ drum,
2652
+ beats,
2653
+ vm.music_tempo,
2654
+ ),
2655
+ )
2656
+ let duration_ms = music_beats_to_ms(vm, beats)
2657
+ if duration_ms > 0 {
2658
+ thread.wait_until_ms = Some(vm.now_ms + duration_ms)
2659
+ }
2660
+ }
2661
+ "music_midiPlayDrumForBeats" => {
2662
+ let drum = normalize_music_drum(
2663
+ json_to_number_value(
2664
+ value_from_input(vm, target_index, block, "DRUM", 0),
2665
+ ),
2666
+ )
2667
+ let beats = clamp_music_beats(
2668
+ json_to_number_value(
2669
+ value_from_input(vm, target_index, block, "BEATS", 0),
2670
+ ),
2671
+ )
2672
+ push_effect(
2673
+ vm,
2674
+ HostEffect::PlayDrum(
2675
+ vm.targets[target_index].name,
2676
+ drum,
2677
+ beats,
2678
+ vm.music_tempo,
2679
+ ),
2680
+ )
2681
+ let duration_ms = music_beats_to_ms(vm, beats)
2682
+ if duration_ms > 0 {
2683
+ thread.wait_until_ms = Some(vm.now_ms + duration_ms)
2684
+ }
2685
+ }
2686
+ "music_restForBeats" => {
2687
+ let beats = clamp_music_beats(
2688
+ json_to_number_value(
2689
+ value_from_input(vm, target_index, block, "BEATS", 0),
2690
+ ),
2691
+ )
2692
+ let duration_ms = music_beats_to_ms(vm, beats)
2693
+ if duration_ms > 0 {
2694
+ thread.wait_until_ms = Some(vm.now_ms + duration_ms)
2695
+ }
2696
+ }
2697
+ "music_playNoteForBeats" => {
2698
+ let note = clamp_double(
2699
+ json_to_number_value(
2700
+ value_from_input(vm, target_index, block, "NOTE", 0),
2701
+ ),
2702
+ 0.0,
2703
+ 130.0,
2704
+ ).to_int()
2705
+ let beats = clamp_music_beats(
2706
+ json_to_number_value(
2707
+ value_from_input(vm, target_index, block, "BEATS", 0),
2708
+ ),
2709
+ )
2710
+ if beats > 0.0 {
2711
+ push_effect(
2712
+ vm,
2713
+ HostEffect::PlayNote(
2714
+ vm.targets[target_index].name,
2715
+ note,
2716
+ beats,
2717
+ vm.targets[target_index].music_instrument + 1,
2718
+ vm.music_tempo,
2719
+ ),
2720
+ )
2721
+ let duration_ms = music_beats_to_ms(vm, beats)
2722
+ if duration_ms > 0 {
2723
+ thread.wait_until_ms = Some(vm.now_ms + duration_ms)
2724
+ }
2725
+ }
2726
+ }
2727
+ "music_setInstrument" => {
2728
+ let raw = json_to_number_value(
2729
+ value_from_input(vm, target_index, block, "INSTRUMENT", 0),
2730
+ )
2731
+ vm.targets[target_index].music_instrument = normalize_music_instrument_index(
2732
+ raw,
2733
+ )
2734
+ }
2735
+ "music_midiSetInstrument" => {
2736
+ let raw = json_to_number_value(
2737
+ value_from_input(vm, target_index, block, "INSTRUMENT", 0),
2738
+ )
2739
+ vm.targets[target_index].music_instrument = normalize_music_instrument_index(
2740
+ raw,
2741
+ )
2742
+ }
2743
+ "music_setTempo" => {
2744
+ let tempo = json_to_number_value(
2745
+ value_from_input(vm, target_index, block, "TEMPO", 0),
2746
+ )
2747
+ vm.music_tempo = clamp_music_tempo(tempo)
2748
+ }
2749
+ "music_changeTempo" => {
2750
+ let delta = json_to_number_value(
2751
+ value_from_input(vm, target_index, block, "TEMPO", 0),
2752
+ )
2753
+ vm.music_tempo = clamp_music_tempo(vm.music_tempo + delta)
2754
+ }
2755
+ "text2speech_setVoice" => {
2756
+ let voice = input_or_field_string(
2757
+ vm, target_index, block, "VOICE", "VOICE",
2758
+ )
2759
+ vm.targets[target_index].tts_voice = normalize_tts_voice(
2760
+ voice,
2761
+ vm.targets[target_index].tts_voice,
2762
+ )
2763
+ }
2764
+ "text2speech_setLanguage" => {
2765
+ let language = input_or_field_string(
2766
+ vm, target_index, block, "LANGUAGE", "LANGUAGE",
2767
+ )
2768
+ if language != "" {
2769
+ vm.tts_language = lower_trim(language)
2770
+ }
2771
+ }
2772
+ "text2speech_speakAndWait" => {
2773
+ let words = json_to_string_value(
2774
+ value_from_input(vm, target_index, block, "WORDS", 0),
2775
+ )
2776
+ if words.trim() != "" {
2777
+ let wait_key = "text2speech_done_\{thread.id}"
2778
+ push_effect(
2779
+ vm,
2780
+ HostEffect::TextToSpeech(
2781
+ vm.targets[target_index].name,
2782
+ words,
2783
+ vm.targets[target_index].tts_voice,
2784
+ vm.tts_language,
2785
+ wait_key,
2786
+ ),
2787
+ )
2788
+ thread.wait_for_input = Some(wait_key)
2789
+ }
2790
+ }
2791
+ "sound_play" | "sound_playuntildone" => {
2792
+ let sound = json_to_string_value(
2793
+ value_from_input(vm, target_index, block, "SOUND_MENU", 0),
2794
+ )
2795
+ push_effect(
2796
+ vm,
2797
+ HostEffect::PlaySound(vm.targets[target_index].name, sound),
2798
+ )
2799
+ }
2800
+ "sound_stopallsounds" => push_effect(vm, HostEffect::StopAllSounds)
2801
+ "sound_setvolumeto" => {
2802
+ let volume = json_to_number_value(
2803
+ value_from_input(vm, target_index, block, "VOLUME", 0),
2804
+ )
2805
+ vm.targets[target_index].volume = clamp_0_100(volume)
2806
+ }
2807
+ "sound_changevolumeby" => {
2808
+ let delta = json_to_number_value(
2809
+ value_from_input(vm, target_index, block, "VOLUME", 0),
2810
+ )
2811
+ let next = vm.targets[target_index].volume + delta
2812
+ vm.targets[target_index].volume = clamp_0_100(next)
2813
+ }
2814
+ "sound_seteffectto" | "sound_changeeffectby" | "sound_cleareffects" => ()
2815
+ "data_setvariableto" =>
2816
+ match field_value(block, "VARIABLE") {
2817
+ Some((name, id)) =>
2818
+ match
2819
+ fast_numeric_value_for_setvariable(vm, target, target_index, block) {
2820
+ Some(value) =>
2821
+ write_variable(
2822
+ vm,
2823
+ target_index,
2824
+ id,
2825
+ Some(name),
2826
+ json_number(value),
2827
+ )
2828
+ None => {
2829
+ let value = value_from_input(vm, target_index, block, "VALUE", 0)
2830
+ write_variable(vm, target_index, id, Some(name), value)
2831
+ }
2832
+ }
2833
+ None => ()
2834
+ }
2835
+ "data_changevariableby" =>
2836
+ match field_value(block, "VARIABLE") {
2837
+ Some((name, id)) => {
2838
+ let delta = number_from_input(vm, target_index, block, "VALUE", 0)
2839
+ let current = json_to_number_value(
2840
+ read_variable(vm, target_index, id, Some(name)),
2841
+ )
2842
+ write_variable(
2843
+ vm,
2844
+ target_index,
2845
+ id,
2846
+ Some(name),
2847
+ json_number(current + delta),
2848
+ )
2849
+ }
2850
+ None => ()
2851
+ }
2852
+ "data_addtolist" =>
2853
+ match field_value(block, "LIST") {
2854
+ Some((list_name, list_id)) =>
2855
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
2856
+ Some((owner, id, slot)) => {
2857
+ let list = read_list_by_ref(vm, owner, id, slot)
2858
+ list.push(value_from_input(vm, target_index, block, "ITEM", 0))
2859
+ write_list_by_ref(vm, owner, id, slot, list)
2860
+ }
2861
+ None => ()
2862
+ }
2863
+ None => ()
2864
+ }
2865
+ "data_deleteoflist" => execute_list_delete(vm, target_index, block)
2866
+ "data_deletealloflist" =>
2867
+ match field_value(block, "LIST") {
2868
+ Some((list_name, list_id)) =>
2869
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
2870
+ Some((owner, id, slot)) =>
2871
+ write_list_by_ref(vm, owner, id, slot, [])
2872
+ None => ()
2873
+ }
2874
+ None => ()
2875
+ }
2876
+ "data_insertatlist" => execute_list_insert(vm, target_index, block)
2877
+ "data_replaceitemoflist" => execute_list_replace(vm, target_index, block)
2878
+ "data_showvariable"
2879
+ | "data_hidevariable"
2880
+ | "data_showlist"
2881
+ | "data_hidelist" => ()
2882
+ "control_wait" => {
2883
+ let duration_raw = json_to_number_value(
2884
+ value_from_input(vm, target_index, block, "DURATION", 0),
2885
+ )
2886
+ let duration = if duration_raw < 0.0 { 0.0 } else { duration_raw }
2887
+ thread.wait_until_ms = Some(vm.now_ms + (duration * 1000.0).to_int())
2888
+ request_redraw(vm)
2889
+ }
2890
+ "control_wait_until" => {
2891
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2892
+ if !condition {
2893
+ next_pc = Some(block_pc)
2894
+ }
2895
+ }
2896
+ "control_repeat" => {
2897
+ let block_hot_count = increment_block_hot_count(
2898
+ vm,
2899
+ target_index,
2900
+ block.id,
2901
+ )
2902
+ let times = json_to_number_value(
2903
+ value_from_input(vm, target_index, block, "TIMES", 0),
2904
+ )
2905
+ .floor()
2906
+ .to_int()
2907
+ if times > 0 {
2908
+ let substack_pc = block_meta.substack_pc
2909
+ let use_fast_path = times >= 32 || block_hot_count >= 8
2910
+ if use_fast_path &&
2911
+ try_fast_repeat_motion_step(vm, target_index, times, substack_pc) {
2912
+ next_pc = block_meta.next_pc
2913
+ } else {
2914
+ thread.stack.push(
2915
+ repeat_frame(times, substack_pc, block_meta.next_pc),
2916
+ )
2917
+ next_pc = substack_pc
2918
+ }
2919
+ }
2920
+ }
2921
+ "control_repeat_until" => {
2922
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2923
+ if !condition {
2924
+ thread.stack.push(
2925
+ repeat_frame(1, block_meta.substack_pc, Some(block_pc)),
2926
+ )
2927
+ next_pc = block_meta.substack_pc
2928
+ }
2929
+ }
2930
+ "control_while" => {
2931
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2932
+ if condition {
2933
+ thread.stack.push(
2934
+ repeat_frame(1, block_meta.substack_pc, Some(block_pc)),
2935
+ )
2936
+ next_pc = block_meta.substack_pc
2937
+ }
2938
+ }
2939
+ "control_for_each" =>
2940
+ match block_meta.variable {
2941
+ Some(variable_ref) => {
2942
+ let limit = json_to_number_value(
2943
+ value_from_input(vm, target_index, block, "VALUE", 0),
2944
+ )
2945
+ let key = block.id
2946
+ let index = thread.loop_counters.get_or_default(key, 0)
2947
+ if Double::from_int(index) < limit {
2948
+ let next_index = index + 1
2949
+ thread.loop_counters[key] = next_index
2950
+ write_variable(
2951
+ vm,
2952
+ target_index,
2953
+ variable_ref.id,
2954
+ Some(variable_ref.name),
2955
+ json_number(Double::from_int(next_index)),
2956
+ )
2957
+ thread.stack.push(
2958
+ repeat_frame(1, block_meta.substack_pc, Some(block_pc)),
2959
+ )
2960
+ next_pc = block_meta.substack_pc
2961
+ } else {
2962
+ thread.loop_counters.remove(key)
2963
+ }
2964
+ }
2965
+ None => ()
2966
+ }
2967
+ "control_forever" => {
2968
+ thread.stack.push(forever_frame(block_meta.substack_pc, Some(block_pc)))
2969
+ next_pc = block_meta.substack_pc
2970
+ }
2971
+ "control_if" => {
2972
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2973
+ if condition {
2974
+ thread.stack.push(
2975
+ repeat_frame(1, block_meta.substack_pc, block_meta.next_pc),
2976
+ )
2977
+ next_pc = block_meta.substack_pc
2978
+ }
2979
+ }
2980
+ "control_if_else" => {
2981
+ let condition = bool_from_input(vm, target_index, block, "CONDITION", 0)
2982
+ let branch_pc = if condition {
2983
+ block_meta.substack_pc
2984
+ } else {
2985
+ block_meta.substack2_pc
2986
+ }
2987
+ thread.stack.push(repeat_frame(1, branch_pc, block_meta.next_pc))
2988
+ next_pc = branch_pc
2989
+ }
2990
+ "control_all_at_once" => {
2991
+ thread.stack.push(
2992
+ repeat_frame(1, block_meta.substack_pc, block_meta.next_pc),
2993
+ )
2994
+ next_pc = block_meta.substack_pc
2995
+ }
2996
+ "control_stop" => {
2997
+ let option = json_to_string_value(
2998
+ value_from_input(vm, target_index, block, "STOP_OPTION", 0),
2999
+ ).to_lower()
3000
+ if option == "all" {
3001
+ clear_threads(vm)
3002
+ push_effect(vm, HostEffect::StopAllSounds)
3003
+ } else if option == "other scripts in sprite" ||
3004
+ option == "other scripts in stage" {
3005
+ kill_other_scripts_for_target(vm, target_index, thread.id)
3006
+ }
3007
+ if option == "this script" ||
3008
+ option == "all" ||
3009
+ option == "other scripts in sprite" ||
3010
+ option == "other scripts in stage" {
3011
+ thread.done = true
3012
+ }
3013
+ }
3014
+ "control_create_clone_of" => {
3015
+ let option = block_clone_option(vm, target_index, block)
3016
+ if option != "" {
3017
+ match find_clone_source_target(vm, target_index, option) {
3018
+ Some(source_index) =>
3019
+ match spawn_clone_target(vm, source_index) {
3020
+ Some(clone_target_index) =>
3021
+ ignore(spawn_clone_start_hats(vm, clone_target_index))
3022
+ None => ()
3023
+ }
3024
+ None => ()
3025
+ }
3026
+ }
3027
+ }
3028
+ "control_delete_this_clone" =>
3029
+ if !vm.targets[target_index].is_original &&
3030
+ !vm.targets[target_index].is_stage {
3031
+ dispose_clone_target(vm, target_index, thread.id)
3032
+ thread.done = true
3033
+ }
3034
+ "control_clear_counter" => vm.control_counter = 0
3035
+ "control_incr_counter" => vm.control_counter += 1
3036
+ "procedures_call" => {
3037
+ let proccode = match block_mutation_string(block, "proccode") {
3038
+ Some(value) => value
3039
+ None => ""
3040
+ }
3041
+ if proccode != "" {
3042
+ match find_procedure_body(target, proccode) {
3043
+ Some(
3044
+ (
3045
+ start_block,
3046
+ param_names,
3047
+ param_ids,
3048
+ param_defaults,
3049
+ procedure_warp_mode,
3050
+ )
3051
+ ) => {
3052
+ let params = {}
3053
+ for i, param_id in param_ids {
3054
+ let name = if i < param_names.length() {
3055
+ param_names[i]
3056
+ } else {
3057
+ param_id
3058
+ }
3059
+ let default_value = if i < param_defaults.length() {
3060
+ param_defaults[i]
3061
+ } else {
3062
+ json_number(0.0)
3063
+ }
3064
+ let value = if block.inputs.contains(param_id) {
3065
+ value_from_input(vm, target_index, block, param_id, 0)
3066
+ } else {
3067
+ default_value
3068
+ }
3069
+ params[name] = value
3070
+ }
3071
+ let next_warp_mode = thread.warp_mode || procedure_warp_mode
3072
+ if next_warp_mode && thread.warp_started_ms == 0 {
3073
+ thread.warp_started_ms = vm.now_ms
3074
+ } else if !next_warp_mode {
3075
+ thread.warp_started_ms = 0
3076
+ }
3077
+ thread.warp_mode = next_warp_mode
3078
+ if next_warp_mode {
3079
+ vm.redraw_requested_while_warp = true
3080
+ }
3081
+ push_procedure_frame(vm, thread.id, {
3082
+ return_pc: target_block_pc_from_id(target, block.next),
3083
+ control_depth: thread.stack.length(),
3084
+ params,
3085
+ proccode,
3086
+ warp_mode: next_warp_mode,
3087
+ })
3088
+ next_pc = target.block_pc_by_id.get(start_block)
3089
+ }
3090
+ None => ()
3091
+ }
3092
+ }
3093
+ }
3094
+ "event_broadcast" => {
3095
+ let message = block_broadcast_name(vm, target_index, block)
3096
+ if message != "" {
3097
+ ignore(spawn_hats_for_message(vm, message, None))
3098
+ push_effect(vm, HostEffect::Broadcast(message))
3099
+ }
3100
+ }
3101
+ "event_broadcastandwait" => {
3102
+ let message = block_broadcast_name(vm, target_index, block)
3103
+ if message != "" {
3104
+ let spawned = spawn_hats_for_message(vm, message, Some(thread.id))
3105
+ if spawned > 0 {
3106
+ vm.waiting_children[thread.id] = spawned
3107
+ }
3108
+ push_effect(vm, HostEffect::Broadcast(message))
3109
+ }
3110
+ }
3111
+ "sensing_askandwait" => {
3112
+ let question = json_to_string_value(
3113
+ value_from_input(vm, target_index, block, "QUESTION", 0),
3114
+ )
3115
+ push_effect(vm, HostEffect::Ask(question))
3116
+ thread.wait_for_input = Some("answer")
3117
+ }
3118
+ "sensing_resettimer" => vm.timer_start_ms = vm.now_ms
3119
+ "sensing_setdragmode"
3120
+ | "procedures_definition"
3121
+ | "procedures_prototype"
3122
+ | "control_start_as_clone"
3123
+ | "event_whenflagclicked"
3124
+ | "event_whenbroadcastreceived"
3125
+ | "event_whenkeypressed"
3126
+ | "event_whenstageclicked"
3127
+ | "event_whenthisspriteclicked"
3128
+ | "event_whenbackdropswitchesto"
3129
+ | "event_whengreaterthan"
3130
+ | "event_whentouchingobject" => ()
3131
+ _ =>
3132
+ push_effect(
3133
+ vm,
3134
+ HostEffect::Log("warn", "unimplemented opcode: \{block.opcode}"),
3135
+ )
3136
+ }
3137
+ vm.current_thread_id = None
3138
+
3139
+ if thread.done {
3140
+ return thread
3141
+ }
3142
+
3143
+ thread.pc = next_pc
3144
+ if thread.pc is None {
3145
+ unwind_control(vm, thread)
3146
+ } else {
3147
+ thread
3148
+ }
3149
+ }
3150
+
3151
+ ///|
3152
+ fn start_vm_runtime(vm : Vm) -> Unit {
3153
+ vm.running = true
3154
+ }
3155
+
3156
+ ///|
3157
+ fn exec_script_tail_runtime(vm : Vm, target_index : Int, start_pc : Int) -> Int {
3158
+ vm.frame_override = None
3159
+ if !spawn_thread_pc(vm, target_index, start_pc, None) {
3160
+ return 0
3161
+ }
3162
+ vm.running = true
3163
+ let frame = step_frame_runtime(vm)
3164
+ vm.frame_override = Some(frame)
3165
+ frame.op_count
3166
+ }
3167
+
3168
+ ///|
3169
+ fn try_exec_host_opcode_fast(vm : Vm, target_index : Int, pc : Int) -> Bool {
3170
+ let block = vm.targets[target_index].blocks_by_pc[pc]
3171
+ match block.opcode {
3172
+ "data_addtolist" =>
3173
+ match field_value(block, "LIST") {
3174
+ Some((list_name, list_id)) =>
3175
+ match with_list_mut(vm, target_index, list_id, Some(list_name)) {
3176
+ Some((owner, id, slot)) => {
3177
+ let list = read_list_by_ref(vm, owner, id, slot)
3178
+ list.push(value_from_input(vm, target_index, block, "ITEM", 0))
3179
+ write_list_by_ref(vm, owner, id, slot, list)
3180
+ true
3181
+ }
3182
+ None => true
3183
+ }
3184
+ None => true
3185
+ }
3186
+ _ => false
3187
+ }
3188
+ }
3189
+
3190
+ ///|
3191
+ fn draw_effect_name_from_id(effect_id : Int) -> String? {
3192
+ match effect_id {
3193
+ 1 => Some("color")
3194
+ 2 => Some("fisheye")
3195
+ 3 => Some("whirl")
3196
+ 4 => Some("pixelate")
3197
+ 5 => Some("mosaic")
3198
+ 6 => Some("brightness")
3199
+ 7 => Some("ghost")
3200
+ _ => None
3201
+ }
3202
+ }
3203
+
3204
+ ///|
3205
+ fn draw_pen_param_name_from_id(param_id : Int) -> String? {
3206
+ match param_id {
3207
+ 1 => Some("color")
3208
+ 2 => Some("saturation")
3209
+ 3 => Some("brightness")
3210
+ 4 => Some("transparency")
3211
+ _ => None
3212
+ }
3213
+ }
3214
+
3215
+ ///|
3216
+ fn finite_or_zero(value : Double) -> Double {
3217
+ if value.is_nan() || value.is_inf() {
3218
+ 0.0
3219
+ } else {
3220
+ value
3221
+ }
3222
+ }
3223
+
3224
+ ///|
3225
+ fn exec_draw_opcode_runtime(
3226
+ vm : Vm,
3227
+ target_index : Int,
3228
+ opcode : String,
3229
+ arg0 : Double,
3230
+ arg1 : Double,
3231
+ extra : Int,
3232
+ ) -> Int {
3233
+ if target_index < 0 || target_index >= vm.targets.length() {
3234
+ return 0
3235
+ }
3236
+ let value0 = finite_or_zero(arg0)
3237
+ let value1 = finite_or_zero(arg1)
3238
+ match opcode {
3239
+ "motion_movesteps" => {
3240
+ let steps = value0
3241
+ let radians = (90.0 - vm.targets[target_index].direction) *
3242
+ @math.PI /
3243
+ 180.0
3244
+ move_target_with_pen(
3245
+ vm,
3246
+ target_index,
3247
+ vm.targets[target_index].x + @math.cos(radians) * steps,
3248
+ vm.targets[target_index].y + @math.sin(radians) * steps,
3249
+ )
3250
+ 1
3251
+ }
3252
+ "motion_turnright" => {
3253
+ let old_direction = vm.targets[target_index].direction
3254
+ vm.targets[target_index].direction += value0
3255
+ if old_direction != vm.targets[target_index].direction {
3256
+ request_redraw(vm)
3257
+ }
3258
+ 1
3259
+ }
3260
+ "motion_turnleft" => {
3261
+ let old_direction = vm.targets[target_index].direction
3262
+ vm.targets[target_index].direction -= value0
3263
+ if old_direction != vm.targets[target_index].direction {
3264
+ request_redraw(vm)
3265
+ }
3266
+ 1
3267
+ }
3268
+ "motion_pointindirection" => {
3269
+ let old_direction = vm.targets[target_index].direction
3270
+ vm.targets[target_index].direction = normalized_scratch_direction(value0)
3271
+ if old_direction != vm.targets[target_index].direction {
3272
+ request_redraw(vm)
3273
+ }
3274
+ 1
3275
+ }
3276
+ "motion_changexby" => {
3277
+ move_target_with_pen(
3278
+ vm,
3279
+ target_index,
3280
+ vm.targets[target_index].x + value0,
3281
+ vm.targets[target_index].y,
3282
+ )
3283
+ 1
3284
+ }
3285
+ "motion_changeyby" => {
3286
+ move_target_with_pen(
3287
+ vm,
3288
+ target_index,
3289
+ vm.targets[target_index].x,
3290
+ vm.targets[target_index].y + value0,
3291
+ )
3292
+ 1
3293
+ }
3294
+ "motion_setx" => {
3295
+ move_target_with_pen(vm, target_index, value0, vm.targets[target_index].y)
3296
+ 1
3297
+ }
3298
+ "motion_sety" => {
3299
+ move_target_with_pen(vm, target_index, vm.targets[target_index].x, value0)
3300
+ 1
3301
+ }
3302
+ "motion_gotoxy" => {
3303
+ move_target_with_pen(vm, target_index, value0, value1)
3304
+ 1
3305
+ }
3306
+ "motion_ifonedgebounce" => {
3307
+ apply_if_on_edge_bounce(vm, target_index)
3308
+ 1
3309
+ }
3310
+ "looks_show" => {
3311
+ let was_visible = vm.targets[target_index].visible
3312
+ vm.targets[target_index].visible = true
3313
+ if !was_visible {
3314
+ request_redraw(vm)
3315
+ }
3316
+ 1
3317
+ }
3318
+ "looks_hide" => {
3319
+ let was_visible = vm.targets[target_index].visible
3320
+ vm.targets[target_index].visible = false
3321
+ if was_visible {
3322
+ request_redraw(vm)
3323
+ }
3324
+ 1
3325
+ }
3326
+ "looks_hideallsprites" => {
3327
+ let mut changed = false
3328
+ for i, candidate in vm.targets {
3329
+ if !candidate.deleted && !candidate.is_stage {
3330
+ if vm.targets[i].visible {
3331
+ changed = true
3332
+ }
3333
+ vm.targets[i].visible = false
3334
+ }
3335
+ }
3336
+ if changed {
3337
+ request_redraw(vm)
3338
+ }
3339
+ 1
3340
+ }
3341
+ "looks_nextcostume" => {
3342
+ let target = vm.targets[target_index]
3343
+ let before = target.current_costume
3344
+ vm.targets[target_index].current_costume = normalized_target_costume_index(
3345
+ target,
3346
+ target.current_costume + 1,
3347
+ )
3348
+ if before != vm.targets[target_index].current_costume {
3349
+ request_redraw(vm)
3350
+ }
3351
+ 1
3352
+ }
3353
+ "looks_changeeffectby" =>
3354
+ match draw_effect_name_from_id(extra) {
3355
+ Some(effect_name) => {
3356
+ set_target_looks_effect(vm, target_index, effect_name, value0, true)
3357
+ request_redraw(vm)
3358
+ 1
3359
+ }
3360
+ None => 0
3361
+ }
3362
+ "looks_seteffectto" =>
3363
+ match draw_effect_name_from_id(extra) {
3364
+ Some(effect_name) => {
3365
+ set_target_looks_effect(vm, target_index, effect_name, value0, false)
3366
+ request_redraw(vm)
3367
+ 1
3368
+ }
3369
+ None => 0
3370
+ }
3371
+ "looks_cleargraphiceffects" => {
3372
+ clear_target_looks_effect(vm, target_index)
3373
+ request_redraw(vm)
3374
+ 1
3375
+ }
3376
+ "looks_changesizeby" => {
3377
+ vm.targets[target_index].size += value0
3378
+ request_redraw(vm)
3379
+ 1
3380
+ }
3381
+ "looks_setsizeto" => {
3382
+ vm.targets[target_index].size = value0
3383
+ request_redraw(vm)
3384
+ 1
3385
+ }
3386
+ "pen_clear" => {
3387
+ vm_clear_pen_pixels(vm)
3388
+ request_redraw(vm)
3389
+ 1
3390
+ }
3391
+ "pen_stamp" =>
3392
+ if target_index >= 0 &&
3393
+ target_index < vm.targets.length() &&
3394
+ !vm.targets[target_index].is_stage &&
3395
+ !vm.targets[target_index].deleted {
3396
+ render_stamp_sprite_to_pen(vm, target_index)
3397
+ request_redraw(vm)
3398
+ 1
3399
+ } else {
3400
+ 0
3401
+ }
3402
+ "pen_penDown" => {
3403
+ vm.targets[target_index].pen_down = true
3404
+ render_draw_pen_point(vm, target_index)
3405
+ request_redraw(vm)
3406
+ 1
3407
+ }
3408
+ "pen_penUp" => {
3409
+ vm.targets[target_index].pen_down = false
3410
+ 1
3411
+ }
3412
+ "pen_changePenColorParamBy" =>
3413
+ match draw_pen_param_name_from_id(extra) {
3414
+ Some(param_name) => {
3415
+ set_target_pen_color_param(vm, target_index, param_name, value0, true)
3416
+ 1
3417
+ }
3418
+ None => 0
3419
+ }
3420
+ "pen_setPenColorParamTo" =>
3421
+ match draw_pen_param_name_from_id(extra) {
3422
+ Some(param_name) => {
3423
+ set_target_pen_color_param(
3424
+ vm, target_index, param_name, value0, false,
3425
+ )
3426
+ 1
3427
+ }
3428
+ None => 0
3429
+ }
3430
+ "pen_changePenSizeBy" => {
3431
+ vm.targets[target_index].pen_size = clamp_double(
3432
+ vm.targets[target_index].pen_size + value0,
3433
+ 1.0,
3434
+ 1200.0,
3435
+ )
3436
+ 1
3437
+ }
3438
+ "pen_setPenSizeTo" => {
3439
+ vm.targets[target_index].pen_size = clamp_double(value0, 1.0, 1200.0)
3440
+ 1
3441
+ }
3442
+ "pen_setPenHueToNumber" => {
3443
+ set_target_pen_color_param(vm, target_index, "color", value0 / 2.0, false)
3444
+ set_target_pen_color_param(vm, target_index, "transparency", 0.0, false)
3445
+ apply_target_legacy_pen_shade(vm, target_index)
3446
+ 1
3447
+ }
3448
+ "pen_changePenHueBy" => {
3449
+ set_target_pen_color_param(vm, target_index, "color", value0 / 2.0, true)
3450
+ apply_target_legacy_pen_shade(vm, target_index)
3451
+ 1
3452
+ }
3453
+ "pen_setPenShadeToNumber" => {
3454
+ let mut shade = value0.mod(200.0)
3455
+ if shade < 0.0 {
3456
+ shade += 200.0
3457
+ }
3458
+ vm.targets[target_index].pen_legacy_shade = shade
3459
+ apply_target_legacy_pen_shade(vm, target_index)
3460
+ 1
3461
+ }
3462
+ "pen_changePenShadeBy" => {
3463
+ let mut shade = (vm.targets[target_index].pen_legacy_shade + value0).mod(
3464
+ 200.0,
3465
+ )
3466
+ if shade < 0.0 {
3467
+ shade += 200.0
3468
+ }
3469
+ vm.targets[target_index].pen_legacy_shade = shade
3470
+ apply_target_legacy_pen_shade(vm, target_index)
3471
+ 1
3472
+ }
3473
+ _ => 0
3474
+ }
3475
+ }
3476
+
3477
+ ///|
3478
+ fn exec_opcode_once_runtime(vm : Vm, target_index : Int, pc : Int) -> Int {
3479
+ if target_index < 0 || target_index >= vm.targets.length() {
3480
+ return 0
3481
+ }
3482
+ if pc < 0 || pc >= vm.targets[target_index].blocks_by_pc.length() {
3483
+ return 0
3484
+ }
3485
+ if try_exec_host_opcode_fast(vm, target_index, pc) {
3486
+ return 1
3487
+ }
3488
+
3489
+ // Execute exactly one opcode in an ephemeral thread context.
3490
+ let thread_id = -1
3491
+ ignore(
3492
+ execute_thread_once(vm, {
3493
+ id: thread_id,
3494
+ target_index,
3495
+ pc: Some(pc),
3496
+ wait_until_ms: None,
3497
+ wait_for_input: None,
3498
+ done: false,
3499
+ warp_mode: false,
3500
+ warp_started_ms: 0,
3501
+ stack: [],
3502
+ loop_counters: {},
3503
+ parent_waiter: None,
3504
+ }),
3505
+ )
3506
+ vm.waiting_children.remove(thread_id)
3507
+ vm.procedure_frames.remove(thread_id)
3508
+ vm.current_thread_id = None
3509
+ if !vm.threads.is_empty() {
3510
+ vm.running = true
3511
+ }
3512
+ 1
3513
+ }
3514
+
3515
+ ///|
3516
+ fn apply_aot_commands_frame(vm : Vm) -> FrameReport {
3517
+ let effect_count_before = vm.effects.length()
3518
+ let mut op_count = 0
3519
+ let mut has_host_tail = false
3520
+ vm.aot_pending = false
3521
+ for command in vm.aot_commands {
3522
+ match command {
3523
+ AotCommand::SetVariable(target_index, variable_id, value) =>
3524
+ if target_index >= 0 && target_index < vm.targets.length() {
3525
+ write_variable(vm, target_index, Some(variable_id), None, value)
3526
+ op_count += 1
3527
+ }
3528
+ AotCommand::ChangeVariable(target_index, variable_id, delta) =>
3529
+ if target_index >= 0 && target_index < vm.targets.length() {
3530
+ let current = read_variable(vm, target_index, Some(variable_id), None)
3531
+ let next_value = json_number(json_to_number_value(current) + delta)
3532
+ write_variable(vm, target_index, Some(variable_id), None, next_value)
3533
+ op_count += 1
3534
+ }
3535
+ AotCommand::HostOpcode(target_index, pc) =>
3536
+ op_count += exec_opcode_once_runtime(vm, target_index, pc)
3537
+ AotCommand::HostTail(target_index, start_pc) =>
3538
+ if spawn_thread_pc(vm, target_index, start_pc, None) {
3539
+ has_host_tail = true
3540
+ }
3541
+ }
3542
+ }
3543
+ if has_host_tail {
3544
+ vm.running = true
3545
+ let host_frame = step_frame_runtime(vm)
3546
+ return {
3547
+ active_threads: host_frame.active_threads,
3548
+ tick_count: host_frame.tick_count,
3549
+ op_count: op_count + host_frame.op_count,
3550
+ emitted_effects: vm.effects.length() - effect_count_before,
3551
+ stop_reason: host_frame.stop_reason,
3552
+ should_render: host_frame.should_render,
3553
+ is_in_warp: host_frame.is_in_warp,
3554
+ }
3555
+ }
3556
+ let active_threads = vm.threads.length()
3557
+ if active_threads <= 0 {
3558
+ vm.running = false
3559
+ vm.redraw_requested = false
3560
+ vm.redraw_requested_while_warp = false
3561
+ } else {
3562
+ vm.running = true
3563
+ }
3564
+ let stop_reason = if active_threads <= 0 { "finished" } else { "timeout" }
3565
+ {
3566
+ active_threads,
3567
+ tick_count: 1,
3568
+ op_count,
3569
+ emitted_effects: vm.effects.length() - effect_count_before,
3570
+ stop_reason,
3571
+ should_render: true,
3572
+ is_in_warp: has_active_warp_thread(vm),
3573
+ }
3574
+ }
3575
+
3576
+ ///|
3577
+ fn green_flag_runtime(vm : Vm) -> Unit {
3578
+ clear_threads(vm)
3579
+ vm.frame_override = None
3580
+ reset_targets_for_green_flag(vm)
3581
+ vm_clear_pen_pixels(vm)
3582
+ for i, target in vm.targets {
3583
+ if target.is_original {
3584
+ vm.targets[i].pen_down = false
3585
+ vm.targets[i].pen_color = 66.66
3586
+ vm.targets[i].pen_saturation = 100.0
3587
+ vm.targets[i].pen_brightness = 100.0
3588
+ vm.targets[i].pen_transparency = 0.0
3589
+ vm.targets[i].pen_size = 1.0
3590
+ vm.targets[i].pen_legacy_shade = 50.0
3591
+ clear_target_looks_effect(vm, i)
3592
+ }
3593
+ }
3594
+ vm.hat_predicates.clear()
3595
+ vm.hot_op_counts.clear()
3596
+ vm.pending_translate_requests.clear()
3597
+ vm.io_prev_state = copy_json_map(vm.io_state)
3598
+ vm.answer = ""
3599
+ vm.timer_start_ms = vm.now_ms
3600
+ vm.run_id += 1
3601
+ vm.redraw_requested = false
3602
+ vm.redraw_requested_while_warp = false
3603
+ vm.running = true
3604
+ if vm.aot_wasm_only {
3605
+ vm.aot_pending = false
3606
+ vm.running = false
3607
+ return
3608
+ }
3609
+ if vm.aot_use_full_exec {
3610
+ let spawned = spawn_aot_green_flag_hats(vm)
3611
+ if spawned == 0 {
3612
+ vm.running = false
3613
+ }
3614
+ return
3615
+ }
3616
+ if !vm.aot_commands.is_empty() {
3617
+ vm.aot_pending = true
3618
+ return
3619
+ }
3620
+ let spawned = spawn_green_flag_hats(vm)
3621
+ if spawned == 0 {
3622
+ vm.running = false
3623
+ }
3624
+ }
3625
+
3626
+ ///|
3627
+ fn broadcast_runtime(vm : Vm, message : String) -> Unit {
3628
+ if message == "" {
3629
+ return
3630
+ }
3631
+ let spawned = spawn_hats_for_message(vm, message, None)
3632
+ if spawned > 0 {
3633
+ vm.running = true
3634
+ }
3635
+ push_effect(vm, HostEffect::Broadcast(message))
3636
+ }
3637
+
3638
+ ///|
3639
+ fn post_io_json_runtime(
3640
+ vm : Vm,
3641
+ device : String,
3642
+ payload_json : String,
3643
+ ) -> Unit {
3644
+ if payload_json.trim().is_empty() {
3645
+ vm.io_state[device] = Json::null()
3646
+ return
3647
+ }
3648
+ let parsed = try? @json.parse(payload_json)
3649
+ match parsed {
3650
+ Ok(payload) => vm.io_state[device] = payload
3651
+ Err(err) =>
3652
+ push_effect(vm, HostEffect::Log("error", "invalid io payload: \{err}"))
3653
+ }
3654
+ }
3655
+
3656
+ ///|
3657
+ fn set_time_runtime(vm : Vm, now_ms : Int) -> Unit {
3658
+ vm.now_ms = now_ms
3659
+ }
3660
+
3661
+ ///|
3662
+ fn step_frame_runtime(vm : Vm) -> FrameReport {
3663
+ match vm.frame_override {
3664
+ Some(report) => {
3665
+ vm.frame_override = None
3666
+ return report
3667
+ }
3668
+ None => ()
3669
+ }
3670
+ if vm.aot_pending {
3671
+ return apply_aot_commands_frame(vm)
3672
+ }
3673
+
3674
+ let spawned_io = dispatch_io_event_hats(vm)
3675
+ let spawned_predicates = spawn_predicate_hats(vm)
3676
+ if spawned_io > 0 || spawned_predicates > 0 {
3677
+ vm.running = true
3678
+ }
3679
+
3680
+ let mut total_ticks = 0
3681
+ let mut total_ops = 0
3682
+ let mut stop_reason = "timeout"
3683
+ let mut should_render = false
3684
+ let tick_timeout = if vm.options.step_timeout_ticks > 0 {
3685
+ vm.options.step_timeout_ticks
3686
+ } else {
3687
+ 2048
3688
+ }
3689
+
3690
+ if vm.running {
3691
+ let mut saw_warp_context = has_active_warp_thread(vm) ||
3692
+ vm.redraw_requested_while_warp
3693
+ while total_ticks < tick_timeout {
3694
+ let thread_count = vm.threads.length()
3695
+ if thread_count <= 0 {
3696
+ stop_reason = "finished"
3697
+ should_render = true
3698
+ break
3699
+ }
3700
+
3701
+ let mut progressed = false
3702
+ let mut warp_active_in_tick = false
3703
+ let mut warp_exit_detected = false
3704
+ let budget = current_step_budget(vm)
3705
+ let warp_window_extra_steps = budget * 8
3706
+ let non_warp_extra_steps = if vm.options.turbo {
3707
+ budget
3708
+ } else {
3709
+ budget / 4
3710
+ }
3711
+
3712
+ for index in 0..<thread_count {
3713
+ let thread = vm.threads[index]
3714
+ if thread.done {
3715
+ continue
3716
+ }
3717
+
3718
+ let mut updated = execute_thread_once(vm, thread)
3719
+ let mut warp_steps = warp_window_extra_steps
3720
+ let mut non_warp_steps = non_warp_extra_steps
3721
+ let mut thread_saw_warp_context = false
3722
+ while true {
3723
+ vm.threads[index] = updated
3724
+ let in_warp_window = is_warp_window_active(vm, updated)
3725
+ if in_warp_window {
3726
+ thread_saw_warp_context = true
3727
+ warp_active_in_tick = true
3728
+ } else if thread_saw_warp_context && !has_active_warp_thread(vm) {
3729
+ stop_reason = "warp-exit"
3730
+ should_render = true
3731
+ vm.redraw_requested = false
3732
+ vm.redraw_requested_while_warp = false
3733
+ warp_exit_detected = true
3734
+ break
3735
+ }
3736
+ let is_blocked = is_thread_blocked(vm, updated)
3737
+ if !is_blocked && !updated.done {
3738
+ total_ops += 1
3739
+ progressed = true
3740
+ if in_warp_window && warp_steps > 0 {
3741
+ warp_steps -= 1
3742
+ updated = execute_thread_once(vm, updated)
3743
+ continue
3744
+ }
3745
+ if !in_warp_window && non_warp_steps > 0 {
3746
+ non_warp_steps -= 1
3747
+ updated = execute_thread_once(vm, updated)
3748
+ continue
3749
+ }
3750
+ } else if updated.done {
3751
+ total_ops += 1
3752
+ progressed = true
3753
+ }
3754
+ break
3755
+ }
3756
+ if warp_exit_detected {
3757
+ break
3758
+ }
3759
+
3760
+ if warp_active_in_tick {
3761
+ saw_warp_context = true
3762
+ }
3763
+ if vm.redraw_requested_while_warp {
3764
+ saw_warp_context = true
3765
+ }
3766
+ if saw_warp_context && !has_active_warp_thread(vm) {
3767
+ stop_reason = "warp-exit"
3768
+ should_render = true
3769
+ vm.redraw_requested = false
3770
+ vm.redraw_requested_while_warp = false
3771
+ warp_exit_detected = true
3772
+ break
3773
+ }
3774
+ }
3775
+
3776
+ total_ticks += 1
3777
+ cleanup_done_threads(vm)
3778
+
3779
+ if warp_exit_detected {
3780
+ break
3781
+ }
3782
+
3783
+ if vm.threads.length() <= 0 {
3784
+ stop_reason = "finished"
3785
+ should_render = true
3786
+ break
3787
+ }
3788
+
3789
+ if warp_active_in_tick {
3790
+ saw_warp_context = true
3791
+ }
3792
+ let warp_active_after_tick = has_active_warp_thread(vm)
3793
+ if vm.redraw_requested_while_warp {
3794
+ saw_warp_context = true
3795
+ }
3796
+ if saw_warp_context && !warp_active_after_tick {
3797
+ stop_reason = "warp-exit"
3798
+ should_render = true
3799
+ vm.redraw_requested = false
3800
+ vm.redraw_requested_while_warp = false
3801
+ break
3802
+ }
3803
+
3804
+ if vm.redraw_requested &&
3805
+ !vm.options.turbo &&
3806
+ !warp_active_in_tick &&
3807
+ !vm.redraw_requested_while_warp &&
3808
+ !warp_active_after_tick {
3809
+ stop_reason = "rerender"
3810
+ should_render = true
3811
+ vm.redraw_requested = false
3812
+ vm.redraw_requested_while_warp = false
3813
+ break
3814
+ }
3815
+
3816
+ if !progressed {
3817
+ stop_reason = "timeout"
3818
+ break
3819
+ }
3820
+ }
3821
+ }
3822
+
3823
+ cleanup_done_threads(vm)
3824
+ if vm.threads.length() <= 0 {
3825
+ vm.running = false
3826
+ if stop_reason != "warp-exit" {
3827
+ stop_reason = "finished"
3828
+ }
3829
+ should_render = true
3830
+ vm.redraw_requested = false
3831
+ vm.redraw_requested_while_warp = false
3832
+ } else if stop_reason != "rerender" &&
3833
+ stop_reason != "warp-exit" &&
3834
+ stop_reason != "finished" {
3835
+ stop_reason = "timeout"
3836
+ should_render = false
3837
+ }
3838
+
3839
+ vm.io_prev_state = copy_json_map(vm.io_state)
3840
+
3841
+ {
3842
+ active_threads: vm.threads.length(),
3843
+ tick_count: total_ticks,
3844
+ op_count: total_ops,
3845
+ emitted_effects: vm.effects.length(),
3846
+ stop_reason,
3847
+ should_render,
3848
+ is_in_warp: has_active_warp_thread(vm),
3849
+ }
3850
+ }