@scratch/scratch-vm 11.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.jsdoc.json +20 -0
  2. package/.nvmrc +1 -0
  3. package/CHANGELOG.md +7004 -0
  4. package/LICENSE +12 -0
  5. package/README.md +120 -0
  6. package/TRADEMARK +1 -0
  7. package/commitlint.config.js +4 -0
  8. package/docs/extensions.md +527 -0
  9. package/package.json +103 -0
  10. package/release.config.js +13 -0
  11. package/src/blocks/scratch3_control.js +206 -0
  12. package/src/blocks/scratch3_core_example.js +69 -0
  13. package/src/blocks/scratch3_data.js +257 -0
  14. package/src/blocks/scratch3_event.js +137 -0
  15. package/src/blocks/scratch3_looks.js +612 -0
  16. package/src/blocks/scratch3_motion.js +288 -0
  17. package/src/blocks/scratch3_operators.js +154 -0
  18. package/src/blocks/scratch3_procedures.js +79 -0
  19. package/src/blocks/scratch3_sensing.js +336 -0
  20. package/src/blocks/scratch3_sound.js +350 -0
  21. package/src/dispatch/central-dispatch.js +143 -0
  22. package/src/dispatch/shared-dispatch.js +235 -0
  23. package/src/dispatch/worker-dispatch.js +110 -0
  24. package/src/engine/adapter.js +176 -0
  25. package/src/engine/block-utility.js +242 -0
  26. package/src/engine/blocks-execute-cache.js +19 -0
  27. package/src/engine/blocks-runtime-cache.js +78 -0
  28. package/src/engine/blocks.js +1299 -0
  29. package/src/engine/comment.js +56 -0
  30. package/src/engine/execute.js +580 -0
  31. package/src/engine/monitor-record.js +23 -0
  32. package/src/engine/mutation-adapter.js +48 -0
  33. package/src/engine/profiler.js +390 -0
  34. package/src/engine/runtime.js +2686 -0
  35. package/src/engine/scratch-blocks-constants.js +27 -0
  36. package/src/engine/sequencer.js +361 -0
  37. package/src/engine/stage-layering.js +29 -0
  38. package/src/engine/target.js +799 -0
  39. package/src/engine/thread.js +404 -0
  40. package/src/engine/variable.js +70 -0
  41. package/src/extension-support/argument-type.js +47 -0
  42. package/src/extension-support/block-type.js +50 -0
  43. package/src/extension-support/define-messages.js +18 -0
  44. package/src/extension-support/extension-manager.js +440 -0
  45. package/src/extension-support/extension-metadata.js +64 -0
  46. package/src/extension-support/extension-worker.js +59 -0
  47. package/src/extension-support/reporter-scope.js +18 -0
  48. package/src/extension-support/target-type.js +17 -0
  49. package/src/extensions/scratch3_boost/index.js +2113 -0
  50. package/src/extensions/scratch3_ev3/index.js +1355 -0
  51. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  52. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  53. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  54. package/src/extensions/scratch3_microbit/index.js +984 -0
  55. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  56. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  57. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  58. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  59. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  116. package/src/extensions/scratch3_music/index.js +1333 -0
  117. package/src/extensions/scratch3_music/manifest.js +63 -0
  118. package/src/extensions/scratch3_pen/index.js +770 -0
  119. package/src/extensions/scratch3_speech2text/index.js +700 -0
  120. package/src/extensions/scratch3_text2speech/index.js +766 -0
  121. package/src/extensions/scratch3_translate/index.js +286 -0
  122. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  123. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  124. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  125. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  126. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  127. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  128. package/src/import/load-costume.js +416 -0
  129. package/src/import/load-sound.js +116 -0
  130. package/src/index.js +10 -0
  131. package/src/io/ble.js +256 -0
  132. package/src/io/bt.js +202 -0
  133. package/src/io/clock.js +39 -0
  134. package/src/io/cloud.js +169 -0
  135. package/src/io/keyboard.js +146 -0
  136. package/src/io/mouse.js +145 -0
  137. package/src/io/mouseWheel.js +28 -0
  138. package/src/io/userData.js +24 -0
  139. package/src/io/video.js +210 -0
  140. package/src/playground/benchmark.css +83 -0
  141. package/src/playground/benchmark.js +753 -0
  142. package/src/playground/index.html +111 -0
  143. package/src/playground/suite.css +78 -0
  144. package/src/playground/suite.html +25 -0
  145. package/src/playground/suite.js +544 -0
  146. package/src/playground/video-sensing.html +18 -0
  147. package/src/playground/video-sensing.js +133 -0
  148. package/src/serialization/deserialize-assets.js +178 -0
  149. package/src/serialization/sb2.js +1295 -0
  150. package/src/serialization/sb2_specmap.js +1818 -0
  151. package/src/serialization/sb3.js +1320 -0
  152. package/src/serialization/serialize-assets.js +60 -0
  153. package/src/sprites/rendered-target.js +1115 -0
  154. package/src/sprites/sprite.js +177 -0
  155. package/src/util/base64-util.js +48 -0
  156. package/src/util/cast.js +218 -0
  157. package/src/util/clone.js +17 -0
  158. package/src/util/color.js +204 -0
  159. package/src/util/fetch-with-timeout.js +53 -0
  160. package/src/util/get-monitor-id.js +33 -0
  161. package/src/util/jsonrpc.js +114 -0
  162. package/src/util/log.js +4 -0
  163. package/src/util/math-util.js +122 -0
  164. package/src/util/maybe-format-message.js +18 -0
  165. package/src/util/new-block-ids.js +33 -0
  166. package/src/util/rateLimiter.js +73 -0
  167. package/src/util/scratch-link-websocket.js +133 -0
  168. package/src/util/string-util.js +94 -0
  169. package/src/util/task-queue.js +203 -0
  170. package/src/util/timer.js +103 -0
  171. package/src/util/uid.js +29 -0
  172. package/src/util/variable-util.js +48 -0
  173. package/src/util/xml-escape.js +33 -0
  174. package/src/virtual-machine.js +1586 -0
  175. package/webpack.config.js +134 -0
@@ -0,0 +1,2686 @@
1
+ const EventEmitter = require('events');
2
+ const {OrderedMap} = require('immutable');
3
+ const uuid = require('uuid');
4
+
5
+ const ArgumentType = require('../extension-support/argument-type');
6
+ const Blocks = require('./blocks');
7
+ const BlocksRuntimeCache = require('./blocks-runtime-cache');
8
+ const BlockType = require('../extension-support/block-type');
9
+ const Profiler = require('./profiler');
10
+ const Sequencer = require('./sequencer');
11
+ const execute = require('./execute.js');
12
+ const ScratchBlocksConstants = require('./scratch-blocks-constants');
13
+ const TargetType = require('../extension-support/target-type');
14
+ const Thread = require('./thread');
15
+ const log = require('../util/log');
16
+ const maybeFormatMessage = require('../util/maybe-format-message');
17
+ const StageLayering = require('./stage-layering');
18
+ const Variable = require('./variable');
19
+ const xmlEscape = require('../util/xml-escape');
20
+ const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
21
+ const fetchWithTimeout = require('../util/fetch-with-timeout');
22
+
23
+ // Virtual I/O devices.
24
+ const Clock = require('../io/clock');
25
+ const Cloud = require('../io/cloud');
26
+ const Keyboard = require('../io/keyboard');
27
+ const Mouse = require('../io/mouse');
28
+ const MouseWheel = require('../io/mouseWheel');
29
+ const UserData = require('../io/userData');
30
+ const Video = require('../io/video');
31
+
32
+ const StringUtil = require('../util/string-util');
33
+ const uid = require('../util/uid');
34
+
35
+ const defaultBlockPackages = {
36
+ scratch3_control: require('../blocks/scratch3_control'),
37
+ scratch3_event: require('../blocks/scratch3_event'),
38
+ scratch3_looks: require('../blocks/scratch3_looks'),
39
+ scratch3_motion: require('../blocks/scratch3_motion'),
40
+ scratch3_operators: require('../blocks/scratch3_operators'),
41
+ scratch3_sound: require('../blocks/scratch3_sound'),
42
+ scratch3_sensing: require('../blocks/scratch3_sensing'),
43
+ scratch3_data: require('../blocks/scratch3_data'),
44
+ scratch3_procedures: require('../blocks/scratch3_procedures')
45
+ };
46
+
47
+ const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69'];
48
+
49
+ /**
50
+ * Information used for converting Scratch argument types into scratch-blocks data.
51
+ * @type {object.<ArgumentType, {shadowType: string, fieldType: string}>}
52
+ */
53
+ const ArgumentTypeMap = (() => {
54
+ const map = {};
55
+ map[ArgumentType.ANGLE] = {
56
+ shadow: {
57
+ type: 'math_angle',
58
+ // We specify fieldNames here so that we can pick
59
+ // create and populate a field with the defaultValue
60
+ // specified in the extension.
61
+ // When the `fieldName` property is not specified,
62
+ // the <field></field> will be left out of the XML and
63
+ // the scratch-blocks defaults for that field will be
64
+ // used instead (e.g. default of 0 for number fields)
65
+ fieldName: 'NUM'
66
+ }
67
+ };
68
+ map[ArgumentType.COLOR] = {
69
+ shadow: {
70
+ type: 'colour_picker',
71
+ fieldName: 'COLOUR'
72
+ }
73
+ };
74
+ map[ArgumentType.NUMBER] = {
75
+ shadow: {
76
+ type: 'math_number',
77
+ fieldName: 'NUM'
78
+ }
79
+ };
80
+ map[ArgumentType.STRING] = {
81
+ shadow: {
82
+ type: 'text',
83
+ fieldName: 'TEXT'
84
+ }
85
+ };
86
+ map[ArgumentType.BOOLEAN] = {
87
+ check: 'Boolean'
88
+ };
89
+ map[ArgumentType.MATRIX] = {
90
+ shadow: {
91
+ type: 'matrix',
92
+ fieldName: 'MATRIX'
93
+ }
94
+ };
95
+ map[ArgumentType.NOTE] = {
96
+ shadow: {
97
+ type: 'note',
98
+ fieldName: 'NOTE'
99
+ }
100
+ };
101
+ map[ArgumentType.IMAGE] = {
102
+ // Inline images are weird because they're not actually "arguments".
103
+ // They are more analagous to the label on a block.
104
+ fieldType: 'field_image'
105
+ };
106
+ return map;
107
+ })();
108
+
109
+ /**
110
+ * A pair of functions used to manage the cloud variable limit,
111
+ * to be used when adding (or attempting to add) or removing a cloud variable.
112
+ * @typedef {object} CloudDataManager
113
+ * @property {function} canAddCloudVariable A function to call to check that
114
+ * a cloud variable can be added.
115
+ * @property {function} addCloudVariable A function to call to track a new
116
+ * cloud variable on the runtime.
117
+ * @property {function} removeCloudVariable A function to call when
118
+ * removing an existing cloud variable.
119
+ * @property {function} hasCloudVariables A function to call to check that
120
+ * the runtime has any cloud variables.
121
+ */
122
+
123
+ /**
124
+ * Creates and manages cloud variable limit in a project,
125
+ * and returns two functions to be used to add a new
126
+ * cloud variable (while checking that it can be added)
127
+ * and remove an existing cloud variable.
128
+ * These are to be called whenever attempting to create or delete
129
+ * a cloud variable.
130
+ * @return {CloudDataManager} The functions to be used when adding or removing a
131
+ * cloud variable.
132
+ */
133
+ const cloudDataManager = () => {
134
+ const limit = 10;
135
+ let count = 0;
136
+
137
+ const canAddCloudVariable = () => count < limit;
138
+
139
+ const addCloudVariable = () => {
140
+ count++;
141
+ };
142
+
143
+ const removeCloudVariable = () => {
144
+ count--;
145
+ };
146
+
147
+ const hasCloudVariables = () => count > 0;
148
+
149
+ return {
150
+ canAddCloudVariable,
151
+ addCloudVariable,
152
+ removeCloudVariable,
153
+ hasCloudVariables
154
+ };
155
+ };
156
+
157
+ /**
158
+ * Numeric ID for Runtime._step in Profiler instances.
159
+ * @type {number}
160
+ */
161
+ let stepProfilerId = -1;
162
+
163
+ /**
164
+ * Numeric ID for Sequencer.stepThreads in Profiler instances.
165
+ * @type {number}
166
+ */
167
+ let stepThreadsProfilerId = -1;
168
+
169
+ /**
170
+ * Numeric ID for RenderWebGL.draw in Profiler instances.
171
+ * @type {number}
172
+ */
173
+ let rendererDrawProfilerId = -1;
174
+
175
+ /**
176
+ * Manages targets, scripts, and the sequencer.
177
+ * @constructor
178
+ */
179
+ class Runtime extends EventEmitter {
180
+ constructor () {
181
+ super();
182
+
183
+ /**
184
+ * Target management and storage.
185
+ * @type {Array.<!Target>}
186
+ */
187
+ this.targets = [];
188
+
189
+ /**
190
+ * Targets in reverse order of execution. Shares its order with drawables.
191
+ * @type {Array.<!Target>}
192
+ */
193
+ this.executableTargets = [];
194
+
195
+ /**
196
+ * A list of threads that are currently running in the VM.
197
+ * Threads are added when execution starts and pruned when execution ends.
198
+ * @type {Array.<Thread>}
199
+ */
200
+ this.threads = [];
201
+
202
+ /** @type {!Sequencer} */
203
+ this.sequencer = new Sequencer(this);
204
+
205
+ /**
206
+ * Storage container for flyout blocks.
207
+ * These will execute on `_editingTarget.`
208
+ * @type {!Blocks}
209
+ */
210
+ this.flyoutBlocks = new Blocks(this, true /* force no glow */);
211
+
212
+ /**
213
+ * Storage container for monitor blocks.
214
+ * These will execute on a target maybe
215
+ * @type {!Blocks}
216
+ */
217
+ this.monitorBlocks = new Blocks(this, true /* force no glow */);
218
+
219
+ /**
220
+ * Currently known editing target for the VM.
221
+ * @type {?Target}
222
+ */
223
+ this._editingTarget = null;
224
+
225
+ /**
226
+ * Map to look up a block primitive's implementation function by its opcode.
227
+ * This is a two-step lookup: package name first, then primitive name.
228
+ * @type {Object.<string, Function>}
229
+ */
230
+ this._primitives = {};
231
+
232
+ /**
233
+ * Map to look up all block information by extended opcode.
234
+ * @type {Array.<CategoryInfo>}
235
+ * @private
236
+ */
237
+ this._blockInfo = [];
238
+
239
+ /**
240
+ * Map to look up hat blocks' metadata.
241
+ * Keys are opcode for hat, values are metadata objects.
242
+ * @type {Object.<string, Object>}
243
+ */
244
+ this._hats = {};
245
+
246
+ /**
247
+ * A list of script block IDs that were glowing during the previous frame.
248
+ * @type {!Array.<!string>}
249
+ */
250
+ this._scriptGlowsPreviousFrame = [];
251
+
252
+ /**
253
+ * Number of non-monitor threads running during the previous frame.
254
+ * @type {number}
255
+ */
256
+ this._nonMonitorThreadCount = 0;
257
+
258
+ /**
259
+ * All threads that finished running and were removed from this.threads
260
+ * by behaviour in Sequencer.stepThreads.
261
+ * @type {Array<Thread>}
262
+ */
263
+ this._lastStepDoneThreads = null;
264
+
265
+ /**
266
+ * Currently known number of clones, used to enforce clone limit.
267
+ * @type {number}
268
+ */
269
+ this._cloneCounter = 0;
270
+
271
+ /**
272
+ * Flag to emit a targets update at the end of a step. When target data
273
+ * changes, this flag is set to true.
274
+ * @type {boolean}
275
+ */
276
+ this._refreshTargets = false;
277
+
278
+ /**
279
+ * Map to look up all monitor block information by opcode.
280
+ * @type {object}
281
+ * @private
282
+ */
283
+ this.monitorBlockInfo = {};
284
+
285
+ /**
286
+ * Ordered map of all monitors, which are MonitorReporter objects.
287
+ */
288
+ this._monitorState = OrderedMap({});
289
+
290
+ /**
291
+ * Monitor state from last tick
292
+ */
293
+ this._prevMonitorState = OrderedMap({});
294
+
295
+ /**
296
+ * Whether the project is in "turbo mode."
297
+ * @type {Boolean}
298
+ */
299
+ this.turboMode = false;
300
+
301
+ /**
302
+ * Whether the project is in "compatibility mode" (30 TPS).
303
+ * @type {Boolean}
304
+ */
305
+ this.compatibilityMode = false;
306
+
307
+ /**
308
+ * A reference to the current runtime stepping interval, set
309
+ * by a `setInterval`.
310
+ * @type {!number}
311
+ */
312
+ this._steppingInterval = null;
313
+
314
+ /**
315
+ * Current length of a step.
316
+ * Changes as mode switches, and used by the sequencer to calculate
317
+ * WORK_TIME.
318
+ * @type {!number}
319
+ */
320
+ this.currentStepTime = null;
321
+
322
+ // Set an intial value for this.currentMSecs
323
+ this.updateCurrentMSecs();
324
+
325
+ /**
326
+ * Whether any primitive has requested a redraw.
327
+ * Affects whether `Sequencer.stepThreads` will yield
328
+ * after stepping each thread.
329
+ * Reset on every frame.
330
+ * @type {boolean}
331
+ */
332
+ this.redrawRequested = false;
333
+
334
+ // Register all given block packages.
335
+ this._registerBlockPackages();
336
+
337
+ // Register and initialize "IO devices", containers for processing
338
+ // I/O related data.
339
+ /** @type {Object.<string, Object>} */
340
+ this.ioDevices = {
341
+ clock: new Clock(this),
342
+ cloud: new Cloud(this),
343
+ keyboard: new Keyboard(this),
344
+ mouse: new Mouse(this),
345
+ mouseWheel: new MouseWheel(this),
346
+ userData: new UserData(),
347
+ video: new Video(this)
348
+ };
349
+
350
+ /**
351
+ * A list of extensions, used to manage hardware connection.
352
+ */
353
+ this.peripheralExtensions = {};
354
+
355
+ /**
356
+ * A runtime profiler that records timed events for later playback to
357
+ * diagnose Scratch performance.
358
+ * @type {Profiler}
359
+ */
360
+ this.profiler = null;
361
+
362
+ const newCloudDataManager = cloudDataManager();
363
+
364
+ /**
365
+ * Check wether the runtime has any cloud data.
366
+ * @type {function}
367
+ * @return {boolean} Whether or not the runtime currently has any
368
+ * cloud variables.
369
+ */
370
+ this.hasCloudData = newCloudDataManager.hasCloudVariables;
371
+
372
+ /**
373
+ * A function which checks whether a new cloud variable can be added
374
+ * to the runtime.
375
+ * @type {function}
376
+ * @return {boolean} Whether or not a new cloud variable can be added
377
+ * to the runtime.
378
+ */
379
+ this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable;
380
+
381
+ /**
382
+ * A function that tracks a new cloud variable in the runtime,
383
+ * updating the cloud variable limit. Calling this function will
384
+ * emit a cloud data update event if this is the first cloud variable
385
+ * being added.
386
+ * @type {function}
387
+ */
388
+ this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager);
389
+
390
+ /**
391
+ * A function which updates the runtime's cloud variable limit
392
+ * when removing a cloud variable and emits a cloud update event
393
+ * if the last of the cloud variables is being removed.
394
+ * @type {function}
395
+ */
396
+ this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager);
397
+
398
+ /**
399
+ * A string representing the origin of the current project from outside of the
400
+ * Scratch community, such as CSFirst.
401
+ * @type {?string}
402
+ */
403
+ this.origin = null;
404
+
405
+ this._initScratchLink();
406
+
407
+ this.resetRunId();
408
+ }
409
+
410
+ /**
411
+ * Width of the stage, in pixels.
412
+ * @const {number}
413
+ */
414
+ static get STAGE_WIDTH () {
415
+ return 480;
416
+ }
417
+
418
+ /**
419
+ * Height of the stage, in pixels.
420
+ * @const {number}
421
+ */
422
+ static get STAGE_HEIGHT () {
423
+ return 360;
424
+ }
425
+
426
+ /**
427
+ * Event name for glowing a script.
428
+ * @const {string}
429
+ */
430
+ static get SCRIPT_GLOW_ON () {
431
+ return 'SCRIPT_GLOW_ON';
432
+ }
433
+
434
+ /**
435
+ * Event name for unglowing a script.
436
+ * @const {string}
437
+ */
438
+ static get SCRIPT_GLOW_OFF () {
439
+ return 'SCRIPT_GLOW_OFF';
440
+ }
441
+
442
+ /**
443
+ * Event name for glowing a block.
444
+ * @const {string}
445
+ */
446
+ static get BLOCK_GLOW_ON () {
447
+ return 'BLOCK_GLOW_ON';
448
+ }
449
+
450
+ /**
451
+ * Event name for unglowing a block.
452
+ * @const {string}
453
+ */
454
+ static get BLOCK_GLOW_OFF () {
455
+ return 'BLOCK_GLOW_OFF';
456
+ }
457
+
458
+ /**
459
+ * Event name for a cloud data update
460
+ * to this project.
461
+ * @const {string}
462
+ */
463
+ static get HAS_CLOUD_DATA_UPDATE () {
464
+ return 'HAS_CLOUD_DATA_UPDATE';
465
+ }
466
+
467
+ /**
468
+ * Event name for turning on turbo mode.
469
+ * @const {string}
470
+ */
471
+ static get TURBO_MODE_ON () {
472
+ return 'TURBO_MODE_ON';
473
+ }
474
+
475
+ /**
476
+ * Event name for turning off turbo mode.
477
+ * @const {string}
478
+ */
479
+ static get TURBO_MODE_OFF () {
480
+ return 'TURBO_MODE_OFF';
481
+ }
482
+
483
+ /**
484
+ * Event name when the project is started (threads may not necessarily be
485
+ * running).
486
+ * @const {string}
487
+ */
488
+ static get PROJECT_START () {
489
+ return 'PROJECT_START';
490
+ }
491
+
492
+ /**
493
+ * Event name when threads start running.
494
+ * Used by the UI to indicate running status.
495
+ * @const {string}
496
+ */
497
+ static get PROJECT_RUN_START () {
498
+ return 'PROJECT_RUN_START';
499
+ }
500
+
501
+ /**
502
+ * Event name when threads stop running
503
+ * Used by the UI to indicate not-running status.
504
+ * @const {string}
505
+ */
506
+ static get PROJECT_RUN_STOP () {
507
+ return 'PROJECT_RUN_STOP';
508
+ }
509
+
510
+ /**
511
+ * Event name for project being stopped or restarted by the user.
512
+ * Used by blocks that need to reset state.
513
+ * @const {string}
514
+ */
515
+ static get PROJECT_STOP_ALL () {
516
+ return 'PROJECT_STOP_ALL';
517
+ }
518
+
519
+ /**
520
+ * Event name for target being stopped by a stop for target call.
521
+ * Used by blocks that need to stop individual targets.
522
+ * @const {string}
523
+ */
524
+ static get STOP_FOR_TARGET () {
525
+ return 'STOP_FOR_TARGET';
526
+ }
527
+
528
+ /**
529
+ * Event name for visual value report.
530
+ * @const {string}
531
+ */
532
+ static get VISUAL_REPORT () {
533
+ return 'VISUAL_REPORT';
534
+ }
535
+
536
+ /**
537
+ * Event name for project loaded report.
538
+ * @const {string}
539
+ */
540
+ static get PROJECT_LOADED () {
541
+ return 'PROJECT_LOADED';
542
+ }
543
+
544
+ /**
545
+ * Event name for report that a change was made that can be saved
546
+ * @const {string}
547
+ */
548
+ static get PROJECT_CHANGED () {
549
+ return 'PROJECT_CHANGED';
550
+ }
551
+
552
+ /**
553
+ * Event name for report that a change was made to an extension in the toolbox.
554
+ * @const {string}
555
+ */
556
+ static get TOOLBOX_EXTENSIONS_NEED_UPDATE () {
557
+ return 'TOOLBOX_EXTENSIONS_NEED_UPDATE';
558
+ }
559
+
560
+ /**
561
+ * Event name for targets update report.
562
+ * @const {string}
563
+ */
564
+ static get TARGETS_UPDATE () {
565
+ return 'TARGETS_UPDATE';
566
+ }
567
+
568
+ /**
569
+ * Event name for monitors update.
570
+ * @const {string}
571
+ */
572
+ static get MONITORS_UPDATE () {
573
+ return 'MONITORS_UPDATE';
574
+ }
575
+
576
+ /**
577
+ * Event name for block drag update.
578
+ * @const {string}
579
+ */
580
+ static get BLOCK_DRAG_UPDATE () {
581
+ return 'BLOCK_DRAG_UPDATE';
582
+ }
583
+
584
+ /**
585
+ * Event name for block drag end.
586
+ * @const {string}
587
+ */
588
+ static get BLOCK_DRAG_END () {
589
+ return 'BLOCK_DRAG_END';
590
+ }
591
+
592
+ /**
593
+ * Event name for reporting that an extension was added.
594
+ * @const {string}
595
+ */
596
+ static get EXTENSION_ADDED () {
597
+ return 'EXTENSION_ADDED';
598
+ }
599
+
600
+ /**
601
+ * Event name for reporting that an extension as asked for a custom field to be added
602
+ * @const {string}
603
+ */
604
+ static get EXTENSION_FIELD_ADDED () {
605
+ return 'EXTENSION_FIELD_ADDED';
606
+ }
607
+
608
+ /**
609
+ * Event name for updating the available set of peripheral devices.
610
+ * This causes the peripheral connection modal to update a list of
611
+ * available peripherals.
612
+ * @const {string}
613
+ */
614
+ static get PERIPHERAL_LIST_UPDATE () {
615
+ return 'PERIPHERAL_LIST_UPDATE';
616
+ }
617
+
618
+ /**
619
+ * Event name for when the user picks a bluetooth device to connect to
620
+ * via Companion Device Manager (CDM)
621
+ * @const {string}
622
+ */
623
+ static get USER_PICKED_PERIPHERAL () {
624
+ return 'USER_PICKED_PERIPHERAL';
625
+ }
626
+
627
+ /**
628
+ * Event name for reporting that a peripheral has connected.
629
+ * This causes the status button in the blocks menu to indicate 'connected'.
630
+ * @const {string}
631
+ */
632
+ static get PERIPHERAL_CONNECTED () {
633
+ return 'PERIPHERAL_CONNECTED';
634
+ }
635
+
636
+ /**
637
+ * Event name for reporting that a peripheral has been intentionally disconnected.
638
+ * This causes the status button in the blocks menu to indicate 'disconnected'.
639
+ * @const {string}
640
+ */
641
+ static get PERIPHERAL_DISCONNECTED () {
642
+ return 'PERIPHERAL_DISCONNECTED';
643
+ }
644
+
645
+ /**
646
+ * Event name for reporting that a peripheral has encountered a request error.
647
+ * This causes the peripheral connection modal to switch to an error state.
648
+ * @const {string}
649
+ */
650
+ static get PERIPHERAL_REQUEST_ERROR () {
651
+ return 'PERIPHERAL_REQUEST_ERROR';
652
+ }
653
+
654
+ /**
655
+ * Event name for reporting that a peripheral connection has been lost.
656
+ * This causes a 'peripheral connection lost' error alert to display.
657
+ * @const {string}
658
+ */
659
+ static get PERIPHERAL_CONNECTION_LOST_ERROR () {
660
+ return 'PERIPHERAL_CONNECTION_LOST_ERROR';
661
+ }
662
+
663
+ /**
664
+ * Event name for reporting that a peripheral has not been discovered.
665
+ * This causes the peripheral connection modal to show a timeout state.
666
+ * @const {string}
667
+ */
668
+ static get PERIPHERAL_SCAN_TIMEOUT () {
669
+ return 'PERIPHERAL_SCAN_TIMEOUT';
670
+ }
671
+
672
+ /**
673
+ * Event name to indicate that the microphone is being used to stream audio.
674
+ * @const {string}
675
+ */
676
+ static get MIC_LISTENING () {
677
+ return 'MIC_LISTENING';
678
+ }
679
+
680
+ /**
681
+ * Event name for reporting that blocksInfo was updated.
682
+ * @const {string}
683
+ */
684
+ static get BLOCKSINFO_UPDATE () {
685
+ return 'BLOCKSINFO_UPDATE';
686
+ }
687
+
688
+ /**
689
+ * Event name when the runtime tick loop has been started.
690
+ * @const {string}
691
+ */
692
+ static get RUNTIME_STARTED () {
693
+ return 'RUNTIME_STARTED';
694
+ }
695
+
696
+ /**
697
+ * Event name when the runtime dispose has been called.
698
+ * @const {string}
699
+ */
700
+ static get RUNTIME_DISPOSED () {
701
+ return 'RUNTIME_DISPOSED';
702
+ }
703
+
704
+ /**
705
+ * Event name for reporting that a block was updated and needs to be rerendered.
706
+ * @const {string}
707
+ */
708
+ static get BLOCKS_NEED_UPDATE () {
709
+ return 'BLOCKS_NEED_UPDATE';
710
+ }
711
+
712
+ /**
713
+ * How rapidly we try to step threads by default, in ms.
714
+ */
715
+ static get THREAD_STEP_INTERVAL () {
716
+ return 1000 / 60;
717
+ }
718
+
719
+ /**
720
+ * In compatibility mode, how rapidly we try to step threads, in ms.
721
+ */
722
+ static get THREAD_STEP_INTERVAL_COMPATIBILITY () {
723
+ return 1000 / 30;
724
+ }
725
+
726
+ /**
727
+ * How many clones can be created at a time.
728
+ * @const {number}
729
+ */
730
+ static get MAX_CLONES () {
731
+ return 300;
732
+ }
733
+
734
+ // -----------------------------------------------------------------------------
735
+ // -----------------------------------------------------------------------------
736
+
737
+ // Helper function for initializing the addCloudVariable function
738
+ _initializeAddCloudVariable (newCloudDataManager) {
739
+ // The addCloudVariable function
740
+ return (() => {
741
+ const hadCloudVarsBefore = this.hasCloudData();
742
+ newCloudDataManager.addCloudVariable();
743
+ if (!hadCloudVarsBefore && this.hasCloudData()) {
744
+ this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, true);
745
+ }
746
+ });
747
+ }
748
+
749
+ // Helper function for initializing the removeCloudVariable function
750
+ _initializeRemoveCloudVariable (newCloudDataManager) {
751
+ return (() => {
752
+ const hadCloudVarsBefore = this.hasCloudData();
753
+ newCloudDataManager.removeCloudVariable();
754
+ if (hadCloudVarsBefore && !this.hasCloudData()) {
755
+ this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false);
756
+ }
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Register default block packages with this runtime.
762
+ * @todo Prefix opcodes with package name.
763
+ * @private
764
+ */
765
+ _registerBlockPackages () {
766
+ for (const packageName in defaultBlockPackages) {
767
+ if (Object.prototype.hasOwnProperty.call(defaultBlockPackages, packageName)) {
768
+ // @todo pass a different runtime depending on package privilege?
769
+ const packageObject = new (defaultBlockPackages[packageName])(this);
770
+ // Collect primitives from package.
771
+ if (packageObject.getPrimitives) {
772
+ const packagePrimitives = packageObject.getPrimitives();
773
+ for (const op in packagePrimitives) {
774
+ if (Object.prototype.hasOwnProperty.call(packagePrimitives, op)) {
775
+ this._primitives[op] =
776
+ packagePrimitives[op].bind(packageObject);
777
+ }
778
+ }
779
+ }
780
+ // Collect hat metadata from package.
781
+ if (packageObject.getHats) {
782
+ const packageHats = packageObject.getHats();
783
+ for (const hatName in packageHats) {
784
+ if (Object.prototype.hasOwnProperty.call(packageHats, hatName)) {
785
+ this._hats[hatName] = packageHats[hatName];
786
+ }
787
+ }
788
+ }
789
+ // Collect monitored from package.
790
+ if (packageObject.getMonitored) {
791
+ this.monitorBlockInfo = Object.assign({}, this.monitorBlockInfo, packageObject.getMonitored());
792
+ }
793
+ }
794
+ }
795
+ }
796
+
797
+ getMonitorState () {
798
+ return this._monitorState;
799
+ }
800
+
801
+ /**
802
+ * Generate an extension-specific menu ID.
803
+ * @param {string} menuName - the name of the menu.
804
+ * @param {string} extensionId - the ID of the extension hosting the menu.
805
+ * @returns {string} - the constructed ID.
806
+ * @private
807
+ */
808
+ _makeExtensionMenuId (menuName, extensionId) {
809
+ return `${extensionId}_menu_${xmlEscape(menuName)}`;
810
+ }
811
+
812
+ /**
813
+ * Create a context ("args") object for use with `formatMessage` on messages which might be target-specific.
814
+ * @param {Target} [target] - the target to use as context. If a target is not provided, default to the current
815
+ * editing target or the stage.
816
+ */
817
+ makeMessageContextForTarget (target) {
818
+ const context = {};
819
+ target = target || this.getEditingTarget() || this.getTargetForStage();
820
+ if (target) {
821
+ context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE);
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Register the primitives provided by an extension.
827
+ * @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.)
828
+ * @private
829
+ */
830
+ _registerExtensionPrimitives (extensionInfo) {
831
+ const categoryInfo = {
832
+ id: extensionInfo.id,
833
+ name: maybeFormatMessage(extensionInfo.name),
834
+ showStatusButton: extensionInfo.showStatusButton,
835
+ blockIconURI: extensionInfo.blockIconURI,
836
+ menuIconURI: extensionInfo.menuIconURI
837
+ };
838
+
839
+ if (extensionInfo.color1) {
840
+ categoryInfo.color1 = extensionInfo.color1;
841
+ categoryInfo.color2 = extensionInfo.color2;
842
+ categoryInfo.color3 = extensionInfo.color3;
843
+ } else {
844
+ categoryInfo.color1 = defaultExtensionColors[0];
845
+ categoryInfo.color2 = defaultExtensionColors[1];
846
+ categoryInfo.color3 = defaultExtensionColors[2];
847
+ }
848
+
849
+ this._blockInfo.push(categoryInfo);
850
+
851
+ this._fillExtensionCategory(categoryInfo, extensionInfo);
852
+
853
+ for (const fieldTypeName in categoryInfo.customFieldTypes) {
854
+ if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) {
855
+ const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName];
856
+
857
+ // Emit events for custom field types from extension
858
+ this.emit(Runtime.EXTENSION_FIELD_ADDED, {
859
+ name: `field_${fieldTypeInfo.extendedName}`,
860
+ implementation: fieldTypeInfo.fieldImplementation
861
+ });
862
+ }
863
+ }
864
+
865
+ this.emit(Runtime.EXTENSION_ADDED, categoryInfo);
866
+ }
867
+
868
+ /**
869
+ * Reregister the primitives for an extension
870
+ * @param {ExtensionMetadata} extensionInfo - new info (results of running getInfo) for an extension
871
+ * @private
872
+ */
873
+ _refreshExtensionPrimitives (extensionInfo) {
874
+ const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id);
875
+ if (categoryInfo) {
876
+ categoryInfo.name = maybeFormatMessage(extensionInfo.name);
877
+ this._fillExtensionCategory(categoryInfo, extensionInfo);
878
+
879
+ this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Read extension information, convert menus, blocks and custom field types
885
+ * and store the results in the provided category object.
886
+ * @param {CategoryInfo} categoryInfo - the category to be filled
887
+ * @param {ExtensionMetadata} extensionInfo - the extension metadata to read
888
+ * @private
889
+ */
890
+ _fillExtensionCategory (categoryInfo, extensionInfo) {
891
+ categoryInfo.blocks = [];
892
+ categoryInfo.customFieldTypes = {};
893
+ categoryInfo.menus = [];
894
+ categoryInfo.menuInfo = {};
895
+
896
+ for (const menuName in extensionInfo.menus) {
897
+ if (Object.prototype.hasOwnProperty.call(extensionInfo.menus, menuName)) {
898
+ const menuInfo = extensionInfo.menus[menuName];
899
+ const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo);
900
+ categoryInfo.menus.push(convertedMenu);
901
+ categoryInfo.menuInfo[menuName] = menuInfo;
902
+ }
903
+ }
904
+ for (const fieldTypeName in extensionInfo.customFieldTypes) {
905
+ if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) {
906
+ const fieldType = extensionInfo.customFieldTypes[fieldTypeName];
907
+ const fieldTypeInfo = this._buildCustomFieldInfo(
908
+ fieldTypeName,
909
+ fieldType,
910
+ extensionInfo.id,
911
+ categoryInfo
912
+ );
913
+
914
+ categoryInfo.customFieldTypes[fieldTypeName] = fieldTypeInfo;
915
+ }
916
+ }
917
+
918
+ for (const blockInfo of extensionInfo.blocks) {
919
+ try {
920
+ const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
921
+ categoryInfo.blocks.push(convertedBlock);
922
+ if (convertedBlock.json) {
923
+ const opcode = convertedBlock.json.type;
924
+ if (blockInfo.blockType !== BlockType.EVENT) {
925
+ this._primitives[opcode] = convertedBlock.info.func;
926
+ }
927
+ if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) {
928
+ this._hats[opcode] = {
929
+ edgeActivated: blockInfo.isEdgeActivated,
930
+ restartExistingThreads: blockInfo.shouldRestartExistingThreads
931
+ };
932
+ }
933
+ }
934
+ } catch (e) {
935
+ log.error('Error parsing block: ', {block: blockInfo, error: e});
936
+ }
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Convert the given extension menu items into the scratch-blocks style of list of pairs.
942
+ * If the menu is dynamic (e.g. the passed in argument is a function), return the input unmodified.
943
+ * @param {object} menuItems - an array of menu items or a function to retrieve such an array
944
+ * @returns {object} - an array of 2 element arrays or the original input function
945
+ * @private
946
+ */
947
+ _convertMenuItems (menuItems) {
948
+ if (typeof menuItems !== 'function') {
949
+ const extensionMessageContext = this.makeMessageContextForTarget();
950
+ return menuItems.map(item => {
951
+ const formattedItem = maybeFormatMessage(item, extensionMessageContext);
952
+ switch (typeof formattedItem) {
953
+ case 'string':
954
+ return [formattedItem, formattedItem];
955
+ case 'object':
956
+ return [maybeFormatMessage(item.text, extensionMessageContext), item.value];
957
+ default:
958
+ throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`);
959
+ }
960
+ });
961
+ }
962
+ return menuItems;
963
+ }
964
+
965
+ /**
966
+ * Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block.
967
+ * @param {string} menuName - the name of the menu
968
+ * @param {object} menuInfo - a description of this menu and its items
969
+ * @property {*} items - an array of menu items or a function to retrieve such an array
970
+ * @property {boolean} [acceptReporters] - if true, allow dropping reporters onto this menu
971
+ * @param {CategoryInfo} categoryInfo - the category for this block
972
+ * @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
973
+ * @private
974
+ */
975
+ _buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) {
976
+ const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
977
+ const menuItems = this._convertMenuItems(menuInfo.items);
978
+ return {
979
+ json: {
980
+ message0: '%1',
981
+ type: menuId,
982
+ inputsInline: true,
983
+ output: 'String',
984
+ colour: categoryInfo.color1,
985
+ colourSecondary: categoryInfo.color2,
986
+ colourTertiary: categoryInfo.color3,
987
+ outputShape: menuInfo.acceptReporters ?
988
+ ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE,
989
+ args0: [
990
+ {
991
+ type: 'field_dropdown',
992
+ name: menuName,
993
+ options: menuItems
994
+ }
995
+ ]
996
+ }
997
+ };
998
+ }
999
+
1000
+ _buildCustomFieldInfo (fieldName, fieldInfo, extensionId, categoryInfo) {
1001
+ const extendedName = `${extensionId}_${fieldName}`;
1002
+ return {
1003
+ fieldName: fieldName,
1004
+ extendedName: extendedName,
1005
+ argumentTypeInfo: {
1006
+ shadow: {
1007
+ type: extendedName,
1008
+ fieldName: `field_${extendedName}`
1009
+ }
1010
+ },
1011
+ scratchBlocksDefinition: this._buildCustomFieldTypeForScratchBlocks(
1012
+ extendedName,
1013
+ fieldInfo.output,
1014
+ fieldInfo.outputShape,
1015
+ categoryInfo
1016
+ ),
1017
+ fieldImplementation: fieldInfo.implementation
1018
+ };
1019
+ }
1020
+
1021
+ /**
1022
+ * Build the scratch-blocks JSON needed for a fieldType.
1023
+ * Custom field types need to be namespaced to the extension so that extensions can't interfere with each other
1024
+ * @param {string} fieldName - The name of the field
1025
+ * @param {string} output - The output of the field
1026
+ * @param {number} outputShape - Shape of the field (from ScratchBlocksConstants)
1027
+ * @param {object} categoryInfo - The category the field belongs to (Used to set its colors)
1028
+ * @returns {object} - Object to be inserted into scratch-blocks
1029
+ */
1030
+ _buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) {
1031
+ return {
1032
+ json: {
1033
+ type: fieldName,
1034
+ message0: '%1',
1035
+ inputsInline: true,
1036
+ output: output,
1037
+ colour: categoryInfo.color1,
1038
+ colourSecondary: categoryInfo.color2,
1039
+ colourTertiary: categoryInfo.color3,
1040
+ outputShape: outputShape,
1041
+ args0: [
1042
+ {
1043
+ name: `field_${fieldName}`,
1044
+ type: `field_${fieldName}`
1045
+ }
1046
+ ]
1047
+ }
1048
+ };
1049
+ }
1050
+
1051
+ /**
1052
+ * Convert ExtensionBlockMetadata into data ready for scratch-blocks.
1053
+ * @param {ExtensionBlockMetadata} blockInfo - the block info to convert
1054
+ * @param {CategoryInfo} categoryInfo - the category for this block
1055
+ * @returns {ConvertedBlockInfo} - the converted & original block information
1056
+ * @private
1057
+ */
1058
+ _convertForScratchBlocks (blockInfo, categoryInfo) {
1059
+ if (blockInfo === '---') {
1060
+ return this._convertSeparatorForScratchBlocks(blockInfo);
1061
+ }
1062
+
1063
+ if (blockInfo.blockType === BlockType.BUTTON) {
1064
+ return this._convertButtonForScratchBlocks(blockInfo);
1065
+ }
1066
+
1067
+ return this._convertBlockForScratchBlocks(blockInfo, categoryInfo);
1068
+ }
1069
+
1070
+ /**
1071
+ * Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function.
1072
+ * @param {ExtensionBlockMetadata} blockInfo - the block to convert
1073
+ * @param {CategoryInfo} categoryInfo - the category for this block
1074
+ * @returns {ConvertedBlockInfo} - the converted & original block information
1075
+ * @private
1076
+ */
1077
+ _convertBlockForScratchBlocks (blockInfo, categoryInfo) {
1078
+ const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`;
1079
+
1080
+ const blockJSON = {
1081
+ type: extendedOpcode,
1082
+ inputsInline: true,
1083
+ category: categoryInfo.name,
1084
+ colour: categoryInfo.color1,
1085
+ colourSecondary: categoryInfo.color2,
1086
+ colourTertiary: categoryInfo.color3
1087
+ };
1088
+ const context = {
1089
+ // TODO: store this somewhere so that we can map args appropriately after translation.
1090
+ // This maps an arg name to its relative position in the original (usually English) block text.
1091
+ // When displaying a block in another language we'll need to run a `replace` action similar to the one
1092
+ // below, but each `[ARG]` will need to be replaced with the number in this map.
1093
+ argsMap: {},
1094
+ blockJSON,
1095
+ categoryInfo,
1096
+ blockInfo,
1097
+ inputList: []
1098
+ };
1099
+
1100
+ // If an icon for the extension exists, prepend it to each block, with a vertical separator.
1101
+ // We can overspecify an icon for each block, but if no icon exists on a block, fall back to
1102
+ // the category block icon.
1103
+ const iconURI = blockInfo.blockIconURI || categoryInfo.blockIconURI;
1104
+
1105
+ if (iconURI) {
1106
+ blockJSON.extensions = ['scratch_extension'];
1107
+ blockJSON.message0 = '%1 %2';
1108
+ const iconJSON = {
1109
+ type: 'field_image',
1110
+ src: iconURI,
1111
+ width: 40,
1112
+ height: 40
1113
+ };
1114
+ const separatorJSON = {
1115
+ type: 'field_vertical_separator'
1116
+ };
1117
+ blockJSON.args0 = [
1118
+ iconJSON,
1119
+ separatorJSON
1120
+ ];
1121
+ }
1122
+
1123
+ switch (blockInfo.blockType) {
1124
+ case BlockType.COMMAND:
1125
+ blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
1126
+ blockJSON.previousStatement = null; // null = available connection; undefined = hat
1127
+ if (!blockInfo.isTerminal) {
1128
+ blockJSON.nextStatement = null; // null = available connection; undefined = terminal
1129
+ }
1130
+ break;
1131
+ case BlockType.REPORTER:
1132
+ blockJSON.output = 'String'; // TODO: distinguish number & string here?
1133
+ blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_ROUND;
1134
+ break;
1135
+ case BlockType.BOOLEAN:
1136
+ blockJSON.output = 'Boolean';
1137
+ blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL;
1138
+ break;
1139
+ case BlockType.HAT:
1140
+ case BlockType.EVENT:
1141
+ if (!Object.prototype.hasOwnProperty.call(blockInfo, 'isEdgeActivated')) {
1142
+ // if absent, this property defaults to true
1143
+ blockInfo.isEdgeActivated = true;
1144
+ }
1145
+ blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
1146
+ blockJSON.nextStatement = null; // null = available connection; undefined = terminal
1147
+ break;
1148
+ case BlockType.CONDITIONAL:
1149
+ case BlockType.LOOP:
1150
+ blockInfo.branchCount = blockInfo.branchCount || 1;
1151
+ blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
1152
+ blockJSON.previousStatement = null; // null = available connection; undefined = hat
1153
+ if (!blockInfo.isTerminal) {
1154
+ blockJSON.nextStatement = null; // null = available connection; undefined = terminal
1155
+ }
1156
+ break;
1157
+ }
1158
+
1159
+ const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text];
1160
+ let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum]
1161
+ let inBranchNum = 0; // how many branches have we placed into the JSON so far?
1162
+ let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}`
1163
+ const convertPlaceholders = this._convertPlaceholders.bind(this, context);
1164
+ const extensionMessageContext = this.makeMessageContextForTarget();
1165
+
1166
+ // alternate between a block "arm" with text on it and an open slot for a substack
1167
+ while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) {
1168
+ if (inTextNum < blockText.length) {
1169
+ context.outLineNum = outLineNum;
1170
+ const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext);
1171
+ const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders);
1172
+ if (blockJSON[`message${outLineNum}`]) {
1173
+ blockJSON[`message${outLineNum}`] += convertedText;
1174
+ } else {
1175
+ blockJSON[`message${outLineNum}`] = convertedText;
1176
+ }
1177
+ ++inTextNum;
1178
+ ++outLineNum;
1179
+ }
1180
+ if (inBranchNum < blockInfo.branchCount) {
1181
+ blockJSON[`message${outLineNum}`] = '%1';
1182
+ blockJSON[`args${outLineNum}`] = [{
1183
+ type: 'input_statement',
1184
+ name: `SUBSTACK${inBranchNum > 0 ? inBranchNum + 1 : ''}`
1185
+ }];
1186
+ ++inBranchNum;
1187
+ ++outLineNum;
1188
+ }
1189
+ }
1190
+
1191
+ if (blockInfo.blockType === BlockType.REPORTER) {
1192
+ if (!blockInfo.disableMonitor && context.inputList.length === 0) {
1193
+ blockJSON.checkboxInFlyout = true;
1194
+ }
1195
+ } else if (blockInfo.blockType === BlockType.LOOP) {
1196
+ // Add icon to the bottom right of a loop block
1197
+ blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT';
1198
+ blockJSON[`message${outLineNum}`] = '%1';
1199
+ blockJSON[`args${outLineNum}`] = [{
1200
+ type: 'field_image',
1201
+ src: './static/blocks-media/repeat.svg', // TODO: use a constant or make this configurable?
1202
+ width: 24,
1203
+ height: 24,
1204
+ alt: '*', // TODO remove this since we don't use collapsed blocks in scratch
1205
+ flip_rtl: true
1206
+ }];
1207
+ ++outLineNum;
1208
+ }
1209
+
1210
+ const mutation = blockInfo.isDynamic ? `<mutation blockInfo="${xmlEscape(JSON.stringify(blockInfo))}"/>` : '';
1211
+ const inputs = context.inputList.join('');
1212
+ const blockXML = `<block type="${extendedOpcode}">${mutation}${inputs}</block>`;
1213
+
1214
+ return {
1215
+ info: context.blockInfo,
1216
+ json: context.blockJSON,
1217
+ xml: blockXML
1218
+ };
1219
+ }
1220
+
1221
+ /**
1222
+ * Generate a separator between blocks categories or sub-categories.
1223
+ * @param {ExtensionBlockMetadata} blockInfo - the block to convert
1224
+ * @param {CategoryInfo} categoryInfo - the category for this block
1225
+ * @returns {ConvertedBlockInfo} - the converted & original block information
1226
+ * @private
1227
+ */
1228
+ _convertSeparatorForScratchBlocks (blockInfo) {
1229
+ return {
1230
+ info: blockInfo,
1231
+ xml: '<sep gap="36"/>'
1232
+ };
1233
+ }
1234
+
1235
+ /**
1236
+ * Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field.
1237
+ * @param {ExtensionBlockMetadata} buttonInfo - the button to convert
1238
+ * @property {string} func - the callback name
1239
+ * @param {CategoryInfo} categoryInfo - the category for this button
1240
+ * @returns {ConvertedBlockInfo} - the converted & original button information
1241
+ * @private
1242
+ */
1243
+ _convertButtonForScratchBlocks (buttonInfo) {
1244
+ // for now we only support these pre-defined callbacks handled in scratch-blocks
1245
+ const supportedCallbackKeys = ['MAKE_A_LIST', 'MAKE_A_PROCEDURE', 'MAKE_A_VARIABLE'];
1246
+ if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) {
1247
+ log.error(`Custom button callbacks not supported yet: ${buttonInfo.func}`);
1248
+ }
1249
+
1250
+ const extensionMessageContext = this.makeMessageContextForTarget();
1251
+ const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext);
1252
+ return {
1253
+ info: buttonInfo,
1254
+ xml: `<button text="${buttonText}" callbackKey="${buttonInfo.func}"></button>`
1255
+ };
1256
+ }
1257
+
1258
+ /**
1259
+ * Helper for _convertPlaceholdes which handles inline images which are a specialized case of block "arguments".
1260
+ * @param {object} argInfo Metadata about the inline image as specified by the extension
1261
+ * @return {object} JSON blob for a scratch-blocks image field.
1262
+ * @private
1263
+ */
1264
+ _constructInlineImageJson (argInfo) {
1265
+ if (!argInfo.dataURI) {
1266
+ log.warn('Missing data URI in extension block with argument type IMAGE');
1267
+ }
1268
+ return {
1269
+ type: 'field_image',
1270
+ src: argInfo.dataURI || '',
1271
+ // TODO these probably shouldn't be hardcoded...?
1272
+ width: 24,
1273
+ height: 24,
1274
+ // Whether or not the inline image should be flipped horizontally
1275
+ // in RTL languages. Defaults to false, indicating that the
1276
+ // image will not be flipped.
1277
+ flip_rtl: argInfo.flipRTL || false
1278
+ };
1279
+ }
1280
+
1281
+ /**
1282
+ * Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback
1283
+ * from string#replace. In addition to the return value the JSON and XML items in the context will be filled.
1284
+ * @param {object} context - information shared with _convertForScratchBlocks about the block, etc.
1285
+ * @param {string} match - the overall string matched by the placeholder regex, including brackets: '[FOO]'.
1286
+ * @param {string} placeholder - the name of the placeholder being matched: 'FOO'.
1287
+ * @return {string} scratch-blocks placeholder for the argument: '%1'.
1288
+ * @private
1289
+ */
1290
+ _convertPlaceholders (context, match, placeholder) {
1291
+ // Sanitize the placeholder to ensure valid XML
1292
+ placeholder = placeholder.replace(/[<"&]/, '_');
1293
+
1294
+ // Determine whether the argument type is one of the known standard field types
1295
+ const argInfo = context.blockInfo.arguments[placeholder] || {};
1296
+ let argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
1297
+
1298
+ // Field type not a standard field type, see if extension has registered custom field type
1299
+ if (!ArgumentTypeMap[argInfo.type] && context.categoryInfo.customFieldTypes[argInfo.type]) {
1300
+ argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo;
1301
+ }
1302
+
1303
+ // Start to construct the scratch-blocks style JSON defining how the block should be
1304
+ // laid out
1305
+ let argJSON;
1306
+
1307
+ // Most field types are inputs (slots on the block that can have other blocks plugged into them)
1308
+ // check if this is not one of those cases. E.g. an inline image on a block.
1309
+ if (argTypeInfo.fieldType === 'field_image') {
1310
+ argJSON = this._constructInlineImageJson(argInfo);
1311
+ } else {
1312
+ // Construct input value
1313
+
1314
+ // Layout a block argument (e.g. an input slot on the block)
1315
+ argJSON = {
1316
+ type: 'input_value',
1317
+ name: placeholder
1318
+ };
1319
+
1320
+ const defaultValue =
1321
+ typeof argInfo.defaultValue === 'undefined' ? '' :
1322
+ xmlEscape(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString());
1323
+
1324
+ if (argTypeInfo.check) {
1325
+ // Right now the only type of 'check' we have specifies that the
1326
+ // input slot on the block accepts Boolean reporters, so it should be
1327
+ // shaped like a hexagon
1328
+ argJSON.check = argTypeInfo.check;
1329
+ }
1330
+
1331
+ let valueName;
1332
+ let shadowType;
1333
+ let fieldName;
1334
+ if (argInfo.menu) {
1335
+ const menuInfo = context.categoryInfo.menuInfo[argInfo.menu];
1336
+ if (menuInfo.acceptReporters) {
1337
+ valueName = placeholder;
1338
+ shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id);
1339
+ fieldName = argInfo.menu;
1340
+ } else {
1341
+ argJSON.type = 'field_dropdown';
1342
+ argJSON.options = this._convertMenuItems(menuInfo.items);
1343
+ valueName = null;
1344
+ shadowType = null;
1345
+ fieldName = placeholder;
1346
+ }
1347
+ } else {
1348
+ valueName = placeholder;
1349
+ shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null;
1350
+ fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null;
1351
+ }
1352
+
1353
+ // <value> is the ScratchBlocks name for a block input.
1354
+ if (valueName) {
1355
+ context.inputList.push(`<value name="${placeholder}">`);
1356
+ }
1357
+
1358
+ // The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
1359
+ // Boolean inputs don't need to specify a shadow in the XML.
1360
+ if (shadowType) {
1361
+ context.inputList.push(`<shadow type="${shadowType}">`);
1362
+ }
1363
+
1364
+ // A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
1365
+ // Leave out the field if defaultValue or fieldName are not specified
1366
+ if (defaultValue && fieldName) {
1367
+ context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
1368
+ }
1369
+
1370
+ if (shadowType) {
1371
+ context.inputList.push('</shadow>');
1372
+ }
1373
+
1374
+ if (valueName) {
1375
+ context.inputList.push('</value>');
1376
+ }
1377
+ }
1378
+
1379
+ const argsName = `args${context.outLineNum}`;
1380
+ const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
1381
+ if (argJSON) blockArgs.push(argJSON);
1382
+ const argNum = blockArgs.length;
1383
+ context.argsMap[placeholder] = argNum;
1384
+
1385
+ return `%${argNum}`;
1386
+ }
1387
+
1388
+ /**
1389
+ * @returns {Array.<object>} scratch-blocks XML for each category of extension blocks, in category order.
1390
+ * @param {?Target} [target] - the active editing target (optional)
1391
+ * @property {string} id - the category / extension ID
1392
+ * @property {string} xml - the XML text for this category, starting with `<category>` and ending with `</category>`
1393
+ */
1394
+ getBlocksXML (target) {
1395
+ return this._blockInfo.map(categoryInfo => {
1396
+ const {name, color1, color2} = categoryInfo;
1397
+ // Filter out blocks that aren't supposed to be shown on this target, as determined by the block info's
1398
+ // `hideFromPalette` and `filter` properties.
1399
+ const paletteBlocks = categoryInfo.blocks.filter(block => {
1400
+ let blockFilterIncludesTarget = true;
1401
+ // If an editing target is not passed, include all blocks
1402
+ // If the block info doesn't include a `filter` property, always include it
1403
+ if (target && block.info.filter) {
1404
+ blockFilterIncludesTarget = block.info.filter.includes(
1405
+ target.isStage ? TargetType.STAGE : TargetType.SPRITE
1406
+ );
1407
+ }
1408
+ // If the block info's `hideFromPalette` is true, then filter out this block
1409
+ return blockFilterIncludesTarget && !block.info.hideFromPalette;
1410
+ });
1411
+
1412
+ const colorXML = `colour="${color1}" secondaryColour="${color2}"`;
1413
+
1414
+ // Use a menu icon if there is one. Otherwise, use the block icon. If there's no icon,
1415
+ // the category menu will show its default colored circle.
1416
+ let menuIconURI = '';
1417
+ if (categoryInfo.menuIconURI) {
1418
+ menuIconURI = categoryInfo.menuIconURI;
1419
+ } else if (categoryInfo.blockIconURI) {
1420
+ menuIconURI = categoryInfo.blockIconURI;
1421
+ }
1422
+ const menuIconXML = menuIconURI ?
1423
+ `iconURI="${menuIconURI}"` : '';
1424
+
1425
+ let statusButtonXML = '';
1426
+ if (categoryInfo.showStatusButton) {
1427
+ statusButtonXML = 'showStatusButton="true"';
1428
+ }
1429
+
1430
+ return {
1431
+ id: categoryInfo.id,
1432
+ xml: `<category name="${name}" id="${categoryInfo.id}" ${statusButtonXML} ${colorXML} ${menuIconXML}>${
1433
+ paletteBlocks.map(block => block.xml).join('')}</category>`
1434
+ };
1435
+ });
1436
+ }
1437
+
1438
+ /**
1439
+ * @returns {Array.<string>} - an array containing the scratch-blocks JSON information for each dynamic block.
1440
+ */
1441
+ getBlocksJSON () {
1442
+ return this._blockInfo.reduce(
1443
+ (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
1444
+ }
1445
+
1446
+ /**
1447
+ * One-time initialization for Scratch Link support.
1448
+ */
1449
+ _initScratchLink () {
1450
+ // Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin.
1451
+ // note that `if (self?....)` will throw if `self` is undefined, so check for that first!
1452
+ if (typeof self !== 'undefined' &&
1453
+ typeof document !== 'undefined' &&
1454
+ document.getElementById &&
1455
+ self.origin &&
1456
+ self.origin !== 'null' && // note this is a string comparison, not a null check
1457
+ self.navigator &&
1458
+ self.navigator.userAgent &&
1459
+ !(
1460
+ self.navigator.userAgent.includes('Node.js') ||
1461
+ self.navigator.userAgent.includes('jsdom')
1462
+ )
1463
+ ) {
1464
+ // Create a script tag for the Scratch Link browser extension, unless one already exists
1465
+ const scriptElement = document.getElementById('scratch-link-extension-script');
1466
+ if (!scriptElement) {
1467
+ const script = document.createElement('script');
1468
+ script.id = 'scratch-link-extension-script';
1469
+ document.body.appendChild(script);
1470
+
1471
+ // Tell the browser extension to inject its script.
1472
+ // If the extension isn't present or isn't active, this will do nothing.
1473
+ self.postMessage('inject-scratch-link-script', self.origin);
1474
+ }
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * Get a scratch link socket.
1480
+ * @param {string} type Either BLE or BT
1481
+ * @returns {ScratchLinkSocket} The scratch link socket.
1482
+ */
1483
+ getScratchLinkSocket (type) {
1484
+ const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory;
1485
+ return factory(type);
1486
+ }
1487
+
1488
+ /**
1489
+ * Configure how ScratchLink sockets are created. Factory must consume a "type" parameter
1490
+ * either BT or BLE.
1491
+ * @param {Function} factory The new factory for creating ScratchLink sockets.
1492
+ */
1493
+ configureScratchLinkSocketFactory (factory) {
1494
+ this._linkSocketFactory = factory;
1495
+ }
1496
+
1497
+ /**
1498
+ * The default scratch link socket creator, using websockets to the installed device manager.
1499
+ * @param {string} type Either BLE or BT
1500
+ * @returns {ScratchLinkSocket} The new scratch link socket (a WebSocket object)
1501
+ */
1502
+ _defaultScratchLinkSocketFactory (type) {
1503
+ const Scratch = self.Scratch;
1504
+ const ScratchLinkSafariSocket = Scratch && Scratch.ScratchLinkSafariSocket;
1505
+ // detect this every time in case the user turns on the extension after loading the page
1506
+ const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible();
1507
+ return useSafariSocket ? new ScratchLinkSafariSocket(type) : new ScratchLinkWebSocket(type);
1508
+ }
1509
+
1510
+ /**
1511
+ * Register an extension that communications with a hardware peripheral by id,
1512
+ * to have access to it and its peripheral functions in the future.
1513
+ * @param {string} extensionId - the id of the extension.
1514
+ * @param {object} extension - the extension to register.
1515
+ */
1516
+ registerPeripheralExtension (extensionId, extension) {
1517
+ this.peripheralExtensions[extensionId] = extension;
1518
+ }
1519
+
1520
+ /**
1521
+ * Tell the specified extension to scan for a peripheral.
1522
+ * @param {string} extensionId - the id of the extension.
1523
+ */
1524
+ scanForPeripheral (extensionId) {
1525
+ if (this.peripheralExtensions[extensionId]) {
1526
+ this.peripheralExtensions[extensionId].scan();
1527
+ }
1528
+ }
1529
+
1530
+ /**
1531
+ * Connect to the extension's specified peripheral.
1532
+ * @param {string} extensionId - the id of the extension.
1533
+ * @param {number} peripheralId - the id of the peripheral.
1534
+ */
1535
+ connectPeripheral (extensionId, peripheralId) {
1536
+ if (this.peripheralExtensions[extensionId]) {
1537
+ this.peripheralExtensions[extensionId].connect(peripheralId);
1538
+ }
1539
+ }
1540
+
1541
+ /**
1542
+ * Disconnect from the extension's connected peripheral.
1543
+ * @param {string} extensionId - the id of the extension.
1544
+ */
1545
+ disconnectPeripheral (extensionId) {
1546
+ if (this.peripheralExtensions[extensionId]) {
1547
+ this.peripheralExtensions[extensionId].disconnect();
1548
+ }
1549
+ }
1550
+
1551
+ /**
1552
+ * Returns whether the extension has a currently connected peripheral.
1553
+ * @param {string} extensionId - the id of the extension.
1554
+ * @return {boolean} - whether the extension has a connected peripheral.
1555
+ */
1556
+ getPeripheralIsConnected (extensionId) {
1557
+ let isConnected = false;
1558
+ if (this.peripheralExtensions[extensionId]) {
1559
+ isConnected = this.peripheralExtensions[extensionId].isConnected();
1560
+ }
1561
+ return isConnected;
1562
+ }
1563
+
1564
+ /**
1565
+ * Emit an event to indicate that the microphone is being used to stream audio.
1566
+ * @param {boolean} listening - true if the microphone is currently listening.
1567
+ */
1568
+ emitMicListening (listening) {
1569
+ this.emit(Runtime.MIC_LISTENING, listening);
1570
+ }
1571
+
1572
+ /**
1573
+ * Retrieve the function associated with the given opcode.
1574
+ * @param {!string} opcode The opcode to look up.
1575
+ * @return {Function} The function which implements the opcode.
1576
+ */
1577
+ getOpcodeFunction (opcode) {
1578
+ return this._primitives[opcode];
1579
+ }
1580
+
1581
+ /**
1582
+ * Return whether an opcode represents a hat block.
1583
+ * @param {!string} opcode The opcode to look up.
1584
+ * @return {boolean} True if the op is known to be a hat.
1585
+ */
1586
+ getIsHat (opcode) {
1587
+ return Object.prototype.hasOwnProperty.call(this._hats, opcode);
1588
+ }
1589
+
1590
+ /**
1591
+ * Return whether an opcode represents an edge-activated hat block.
1592
+ * @param {!string} opcode The opcode to look up.
1593
+ * @return {boolean} True if the op is known to be a edge-activated hat.
1594
+ */
1595
+ getIsEdgeActivatedHat (opcode) {
1596
+ return Object.prototype.hasOwnProperty.call(this._hats, opcode) &&
1597
+ this._hats[opcode].edgeActivated;
1598
+ }
1599
+
1600
+
1601
+ /**
1602
+ * Attach the audio engine
1603
+ * @param {!AudioEngine} audioEngine The audio engine to attach
1604
+ */
1605
+ attachAudioEngine (audioEngine) {
1606
+ this.audioEngine = audioEngine;
1607
+ }
1608
+
1609
+ /**
1610
+ * Attach the renderer
1611
+ * @param {!RenderWebGL} renderer The renderer to attach
1612
+ */
1613
+ attachRenderer (renderer) {
1614
+ this.renderer = renderer;
1615
+ this.renderer.setLayerGroupOrdering(StageLayering.LAYER_GROUPS);
1616
+ }
1617
+
1618
+ /**
1619
+ * Set the bitmap adapter for the VM/runtime, which converts scratch 2
1620
+ * bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2)
1621
+ * @param {!function} bitmapAdapter The adapter to attach
1622
+ */
1623
+ attachV2BitmapAdapter (bitmapAdapter) {
1624
+ this.v2BitmapAdapter = bitmapAdapter;
1625
+ }
1626
+
1627
+ /**
1628
+ * Attach the storage module
1629
+ * @param {!ScratchStorage} storage The storage module to attach
1630
+ */
1631
+ attachStorage (storage) {
1632
+ this.storage = storage;
1633
+ fetchWithTimeout.setFetch(storage.scratchFetch.scratchFetch);
1634
+ this.resetRunId();
1635
+ }
1636
+
1637
+ // -----------------------------------------------------------------------------
1638
+ // -----------------------------------------------------------------------------
1639
+
1640
+ /**
1641
+ * Create a thread and push it to the list of threads.
1642
+ * @param {!string} id ID of block that starts the stack.
1643
+ * @param {!Target} target Target to run thread on.
1644
+ * @param {?object} opts optional arguments
1645
+ * @param {?boolean} opts.stackClick true if the script was activated by clicking on the stack
1646
+ * @param {?boolean} opts.updateMonitor true if the script should update a monitor value
1647
+ * @return {!Thread} The newly created thread.
1648
+ */
1649
+ _pushThread (id, target, opts) {
1650
+ const thread = new Thread(id);
1651
+ thread.target = target;
1652
+ thread.stackClick = Boolean(opts && opts.stackClick);
1653
+ thread.updateMonitor = Boolean(opts && opts.updateMonitor);
1654
+ thread.blockContainer = thread.updateMonitor ?
1655
+ this.monitorBlocks :
1656
+ target.blocks;
1657
+
1658
+ thread.pushStack(id);
1659
+ this.threads.push(thread);
1660
+ return thread;
1661
+ }
1662
+
1663
+ /**
1664
+ * Stop a thread: stop running it immediately, and remove it from the thread list later.
1665
+ * @param {!Thread} thread Thread object to remove from actives
1666
+ */
1667
+ _stopThread (thread) {
1668
+ // Mark the thread for later removal
1669
+ thread.isKilled = true;
1670
+ // Inform sequencer to stop executing that thread.
1671
+ this.sequencer.retireThread(thread);
1672
+ }
1673
+
1674
+ /**
1675
+ * Restart a thread in place, maintaining its position in the list of threads.
1676
+ * This is used by `startHats` to and is necessary to ensure 2.0-like execution order.
1677
+ * Test project: https://scratch.mit.edu/projects/130183108/
1678
+ * @param {!Thread} thread Thread object to restart.
1679
+ * @return {Thread} The restarted thread.
1680
+ */
1681
+ _restartThread (thread) {
1682
+ const newThread = new Thread(thread.topBlock);
1683
+ newThread.target = thread.target;
1684
+ newThread.stackClick = thread.stackClick;
1685
+ newThread.updateMonitor = thread.updateMonitor;
1686
+ newThread.blockContainer = thread.blockContainer;
1687
+ newThread.pushStack(thread.topBlock);
1688
+ const i = this.threads.indexOf(thread);
1689
+ if (i > -1) {
1690
+ this.threads[i] = newThread;
1691
+ return newThread;
1692
+ }
1693
+ this.threads.push(thread);
1694
+ return thread;
1695
+ }
1696
+
1697
+ /**
1698
+ * Return whether a thread is currently active/running.
1699
+ * @param {?Thread} thread Thread object to check.
1700
+ * @return {boolean} True if the thread is active/running.
1701
+ */
1702
+ isActiveThread (thread) {
1703
+ return (
1704
+ (
1705
+ thread.stack.length > 0 &&
1706
+ thread.status !== Thread.STATUS_DONE) &&
1707
+ this.threads.indexOf(thread) > -1);
1708
+ }
1709
+
1710
+ /**
1711
+ * Return whether a thread is waiting for more information or done.
1712
+ * @param {?Thread} thread Thread object to check.
1713
+ * @return {boolean} True if the thread is waiting
1714
+ */
1715
+ isWaitingThread (thread) {
1716
+ return (
1717
+ thread.status === Thread.STATUS_PROMISE_WAIT ||
1718
+ thread.status === Thread.STATUS_YIELD_TICK ||
1719
+ !this.isActiveThread(thread)
1720
+ );
1721
+ }
1722
+
1723
+ /**
1724
+ * Toggle a script.
1725
+ * @param {!string} topBlockId ID of block that starts the script.
1726
+ * @param {?object} opts optional arguments to toggle script
1727
+ * @param {?string} opts.target target ID for target to run script on. If not supplied, uses editing target.
1728
+ * @param {?boolean} opts.stackClick true if the user activated the stack by clicking, false if not. This
1729
+ * determines whether we show a visual report when turning on the script.
1730
+ */
1731
+ toggleScript (topBlockId, opts) {
1732
+ opts = Object.assign({
1733
+ target: this._editingTarget,
1734
+ stackClick: false
1735
+ }, opts);
1736
+ // Remove any existing thread.
1737
+ for (let i = 0; i < this.threads.length; i++) {
1738
+ // Toggling a script that's already running turns it off
1739
+ if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) {
1740
+ const blockContainer = opts.target.blocks;
1741
+ const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId));
1742
+
1743
+ if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) {
1744
+ // Allow edge activated hat thread stack click to coexist with
1745
+ // edge activated hat thread that runs every frame
1746
+ continue;
1747
+ }
1748
+ this._stopThread(this.threads[i]);
1749
+ return;
1750
+ }
1751
+ }
1752
+ // Otherwise add it.
1753
+ this._pushThread(topBlockId, opts.target, opts);
1754
+ }
1755
+
1756
+ /**
1757
+ * Enqueue a script that when finished will update the monitor for the block.
1758
+ * @param {!string} topBlockId ID of block that starts the script.
1759
+ * @param {?Target} optTarget target Target to run script on. If not supplied, uses editing target.
1760
+ */
1761
+ addMonitorScript (topBlockId, optTarget) {
1762
+ if (!optTarget) optTarget = this._editingTarget;
1763
+ for (let i = 0; i < this.threads.length; i++) {
1764
+ // Don't re-add the script if it's already running
1765
+ if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE &&
1766
+ this.threads[i].updateMonitor) {
1767
+ return;
1768
+ }
1769
+ }
1770
+ // Otherwise add it.
1771
+ this._pushThread(topBlockId, optTarget, {updateMonitor: true});
1772
+ }
1773
+
1774
+ /**
1775
+ * Run a function `f` for all scripts in a workspace.
1776
+ * `f` will be called with two parameters:
1777
+ * - the top block ID of the script.
1778
+ * - the target that owns the script.
1779
+ * @param {!Function} f Function to call for each script.
1780
+ * @param {Target=} optTarget Optionally, a target to restrict to.
1781
+ */
1782
+ allScriptsDo (f, optTarget) {
1783
+ let targets = this.executableTargets;
1784
+ if (optTarget) {
1785
+ targets = [optTarget];
1786
+ }
1787
+ for (let t = targets.length - 1; t >= 0; t--) {
1788
+ const target = targets[t];
1789
+ const scripts = target.blocks.getScripts();
1790
+ for (let j = 0; j < scripts.length; j++) {
1791
+ const topBlockId = scripts[j];
1792
+ f(topBlockId, target);
1793
+ }
1794
+ }
1795
+ }
1796
+
1797
+ allScriptsByOpcodeDo (opcode, f, optTarget) {
1798
+ let targets = this.executableTargets;
1799
+ if (optTarget) {
1800
+ targets = [optTarget];
1801
+ }
1802
+ for (let t = targets.length - 1; t >= 0; t--) {
1803
+ const target = targets[t];
1804
+ const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode);
1805
+ for (let j = 0; j < scripts.length; j++) {
1806
+ f(scripts[j], target);
1807
+ }
1808
+ }
1809
+ }
1810
+
1811
+ /**
1812
+ * Start all relevant hats.
1813
+ * @param {!string} requestedHatOpcode Opcode of hats to start.
1814
+ * @param {object=} optMatchFields Optionally, fields to match on the hat.
1815
+ * @param {Target=} optTarget Optionally, a target to restrict to.
1816
+ * @return {Array.<Thread>} List of threads started by this function.
1817
+ */
1818
+ startHats (requestedHatOpcode,
1819
+ optMatchFields, optTarget) {
1820
+ if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) {
1821
+ // No known hat with this opcode.
1822
+ return;
1823
+ }
1824
+ const instance = this;
1825
+ const newThreads = [];
1826
+ // Look up metadata for the relevant hat.
1827
+ const hatMeta = instance._hats[requestedHatOpcode];
1828
+
1829
+ for (const opts in optMatchFields) {
1830
+ if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) continue;
1831
+ optMatchFields[opts] = optMatchFields[opts].toUpperCase();
1832
+ }
1833
+
1834
+ // Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
1835
+ this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => {
1836
+ const {
1837
+ blockId: topBlockId,
1838
+ fieldsOfInputs: hatFields
1839
+ } = script;
1840
+
1841
+ // Match any requested fields.
1842
+ // For example: ensures that broadcasts match.
1843
+ // This needs to happen before the block is evaluated
1844
+ // (i.e., before the predicate can be run) because "broadcast and wait"
1845
+ // needs to have a precise collection of started threads.
1846
+ for (const matchField in optMatchFields) {
1847
+ if (hatFields[matchField].value !== optMatchFields[matchField]) {
1848
+ // Field mismatch.
1849
+ return;
1850
+ }
1851
+ }
1852
+
1853
+ if (hatMeta.restartExistingThreads) {
1854
+ // If `restartExistingThreads` is true, we should stop
1855
+ // any existing threads starting with the top block.
1856
+ for (let i = 0; i < this.threads.length; i++) {
1857
+ if (this.threads[i].target === target &&
1858
+ this.threads[i].topBlock === topBlockId &&
1859
+ // stack click threads and hat threads can coexist
1860
+ !this.threads[i].stackClick) {
1861
+ newThreads.push(this._restartThread(this.threads[i]));
1862
+ return;
1863
+ }
1864
+ }
1865
+ } else {
1866
+ // If `restartExistingThreads` is false, we should
1867
+ // give up if any threads with the top block are running.
1868
+ for (let j = 0; j < this.threads.length; j++) {
1869
+ if (this.threads[j].target === target &&
1870
+ this.threads[j].topBlock === topBlockId &&
1871
+ // stack click threads and hat threads can coexist
1872
+ !this.threads[j].stackClick &&
1873
+ this.threads[j].status !== Thread.STATUS_DONE) {
1874
+ // Some thread is already running.
1875
+ return;
1876
+ }
1877
+ }
1878
+ }
1879
+ // Start the thread with this top block.
1880
+ newThreads.push(this._pushThread(topBlockId, target));
1881
+ }, optTarget);
1882
+ // For compatibility with Scratch 2, edge triggered hats need to be processed before
1883
+ // threads are stepped. See ScratchRuntime.as for original implementation
1884
+ newThreads.forEach(thread => {
1885
+ execute(this.sequencer, thread);
1886
+ thread.goToNextBlock();
1887
+ });
1888
+ return newThreads;
1889
+ }
1890
+
1891
+
1892
+ /**
1893
+ * Dispose all targets. Return to clean state.
1894
+ */
1895
+ dispose () {
1896
+ this.stopAll();
1897
+ // Deleting each target's variable's monitors.
1898
+ this.targets.forEach(target => {
1899
+ if (target.isOriginal) target.deleteMonitors();
1900
+ });
1901
+
1902
+ this.targets.map(this.disposeTarget, this);
1903
+ this._monitorState = OrderedMap({});
1904
+ this.emit(Runtime.RUNTIME_DISPOSED);
1905
+ this.ioDevices.clock.resetProjectTimer();
1906
+ // @todo clear out extensions? turboMode? etc.
1907
+
1908
+ // *********** Cloud *******************
1909
+
1910
+ // If the runtime currently has cloud data,
1911
+ // emit a has cloud data update event resetting
1912
+ // it to false
1913
+ if (this.hasCloudData()) {
1914
+ this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false);
1915
+ }
1916
+
1917
+ this.ioDevices.cloud.clear();
1918
+
1919
+ // Reset runtime cloud data info
1920
+ const newCloudDataManager = cloudDataManager();
1921
+ this.hasCloudData = newCloudDataManager.hasCloudVariables;
1922
+ this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable;
1923
+ this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager);
1924
+ this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager);
1925
+ }
1926
+
1927
+ /**
1928
+ * Add a target to the runtime. This tracks the sprite pane
1929
+ * ordering of the target. The target still needs to be put
1930
+ * into the correct execution order after calling this function.
1931
+ * @param {Target} target target to add
1932
+ */
1933
+ addTarget (target) {
1934
+ this.targets.push(target);
1935
+ this.executableTargets.push(target);
1936
+ }
1937
+
1938
+ /**
1939
+ * Move a target in the execution order by a relative amount.
1940
+ *
1941
+ * A positve number will make the target execute earlier. A negative number
1942
+ * will make the target execute later in the order.
1943
+ *
1944
+ * @param {Target} executableTarget target to move
1945
+ * @param {number} delta number of positions to move target by
1946
+ * @returns {number} new position in execution order
1947
+ */
1948
+ moveExecutable (executableTarget, delta) {
1949
+ const oldIndex = this.executableTargets.indexOf(executableTarget);
1950
+ this.executableTargets.splice(oldIndex, 1);
1951
+ let newIndex = oldIndex + delta;
1952
+ if (newIndex > this.executableTargets.length) {
1953
+ newIndex = this.executableTargets.length;
1954
+ }
1955
+ if (newIndex <= 0) {
1956
+ if (this.executableTargets.length > 0 && this.executableTargets[0].isStage) {
1957
+ newIndex = 1;
1958
+ } else {
1959
+ newIndex = 0;
1960
+ }
1961
+ }
1962
+ this.executableTargets.splice(newIndex, 0, executableTarget);
1963
+ return newIndex;
1964
+ }
1965
+
1966
+ /**
1967
+ * Set a target to execute at a specific position in the execution order.
1968
+ *
1969
+ * Infinity will set the target to execute first. 0 will set the target to
1970
+ * execute last (before the stage).
1971
+ *
1972
+ * @param {Target} executableTarget target to move
1973
+ * @param {number} newIndex position in execution order to place the target
1974
+ * @returns {number} new position in the execution order
1975
+ */
1976
+ setExecutablePosition (executableTarget, newIndex) {
1977
+ const oldIndex = this.executableTargets.indexOf(executableTarget);
1978
+ return this.moveExecutable(executableTarget, newIndex - oldIndex);
1979
+ }
1980
+
1981
+ /**
1982
+ * Remove a target from the execution set.
1983
+ * @param {Target} executableTarget target to remove
1984
+ */
1985
+ removeExecutable (executableTarget) {
1986
+ const oldIndex = this.executableTargets.indexOf(executableTarget);
1987
+ if (oldIndex > -1) {
1988
+ this.executableTargets.splice(oldIndex, 1);
1989
+ }
1990
+ }
1991
+
1992
+ /**
1993
+ * Dispose of a target.
1994
+ * @param {!Target} disposingTarget Target to dispose of.
1995
+ */
1996
+ disposeTarget (disposingTarget) {
1997
+ this.targets = this.targets.filter(target => {
1998
+ if (disposingTarget !== target) return true;
1999
+ // Allow target to do dispose actions.
2000
+ target.dispose();
2001
+ // Remove from list of targets.
2002
+ return false;
2003
+ });
2004
+ }
2005
+
2006
+ /**
2007
+ * Stop any threads acting on the target.
2008
+ * @param {!Target} target Target to stop threads for.
2009
+ * @param {Thread=} optThreadException Optional thread to skip.
2010
+ */
2011
+ stopForTarget (target, optThreadException) {
2012
+ // Emit stop event to allow blocks to clean up any state.
2013
+ this.emit(Runtime.STOP_FOR_TARGET, target, optThreadException);
2014
+
2015
+ // Stop any threads on the target.
2016
+ for (let i = 0; i < this.threads.length; i++) {
2017
+ if (this.threads[i] === optThreadException) {
2018
+ continue;
2019
+ }
2020
+ if (this.threads[i].target === target) {
2021
+ this._stopThread(this.threads[i]);
2022
+ }
2023
+ }
2024
+ }
2025
+
2026
+ /**
2027
+ * Reset the Run ID. Call this any time the project logically starts, stops, or changes identity.
2028
+ */
2029
+ resetRunId () {
2030
+ if (!this.storage) {
2031
+ // see also: attachStorage
2032
+ return;
2033
+ }
2034
+
2035
+ const newRunId = uuid.v1();
2036
+ this.storage.scratchFetch.setMetadata(this.storage.scratchFetch.RequestMetadata.RunId, newRunId);
2037
+ }
2038
+
2039
+ /**
2040
+ * Start all threads that start with the green flag.
2041
+ */
2042
+ greenFlag () {
2043
+ this.stopAll();
2044
+ this.emit(Runtime.PROJECT_START);
2045
+ this.ioDevices.clock.resetProjectTimer();
2046
+ this.targets.forEach(target => target.clearEdgeActivatedValues());
2047
+ // Inform all targets of the green flag.
2048
+ for (let i = 0; i < this.targets.length; i++) {
2049
+ this.targets[i].onGreenFlag();
2050
+ }
2051
+ this.startHats('event_whenflagclicked');
2052
+ }
2053
+
2054
+ /**
2055
+ * Stop "everything."
2056
+ */
2057
+ stopAll () {
2058
+ // Emit stop event to allow blocks to clean up any state.
2059
+ this.emit(Runtime.PROJECT_STOP_ALL);
2060
+
2061
+ // Dispose all clones.
2062
+ const newTargets = [];
2063
+ for (let i = 0; i < this.targets.length; i++) {
2064
+ this.targets[i].onStopAll();
2065
+ if (Object.prototype.hasOwnProperty.call(this.targets[i], 'isOriginal') &&
2066
+ !this.targets[i].isOriginal) {
2067
+ this.targets[i].dispose();
2068
+ } else {
2069
+ newTargets.push(this.targets[i]);
2070
+ }
2071
+ }
2072
+ this.targets = newTargets;
2073
+ // Dispose of the active thread.
2074
+ if (this.sequencer.activeThread !== null) {
2075
+ this._stopThread(this.sequencer.activeThread);
2076
+ }
2077
+ // Remove all remaining threads from executing in the next tick.
2078
+ this.threads = [];
2079
+
2080
+ this.resetRunId();
2081
+ }
2082
+
2083
+ /**
2084
+ * Repeatedly run `sequencer.stepThreads` and filter out
2085
+ * inactive threads after each iteration.
2086
+ */
2087
+ _step () {
2088
+ if (this.profiler !== null) {
2089
+ if (stepProfilerId === -1) {
2090
+ stepProfilerId = this.profiler.idByName('Runtime._step');
2091
+ }
2092
+ this.profiler.start(stepProfilerId);
2093
+ }
2094
+
2095
+ // Clean up threads that were told to stop during or since the last step
2096
+ this.threads = this.threads.filter(thread => !thread.isKilled);
2097
+
2098
+ // Find all edge-activated hats, and add them to threads to be evaluated.
2099
+ for (const hatType in this._hats) {
2100
+ if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) continue;
2101
+ const hat = this._hats[hatType];
2102
+ if (hat.edgeActivated) {
2103
+ this.startHats(hatType);
2104
+ }
2105
+ }
2106
+ this.redrawRequested = false;
2107
+ this._pushMonitors();
2108
+ if (this.profiler !== null) {
2109
+ if (stepThreadsProfilerId === -1) {
2110
+ stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads');
2111
+ }
2112
+ this.profiler.start(stepThreadsProfilerId);
2113
+ }
2114
+ const doneThreads = this.sequencer.stepThreads();
2115
+ if (this.profiler !== null) {
2116
+ this.profiler.stop();
2117
+ }
2118
+ this._updateGlows(doneThreads);
2119
+ // Add done threads so that even if a thread finishes within 1 frame, the green
2120
+ // flag will still indicate that a script ran.
2121
+ this._emitProjectRunStatus(
2122
+ this.threads.length + doneThreads.length -
2123
+ this._getMonitorThreadCount([...this.threads, ...doneThreads]));
2124
+ // Store threads that completed this iteration for testing and other
2125
+ // internal purposes.
2126
+ this._lastStepDoneThreads = doneThreads;
2127
+ if (this.renderer) {
2128
+ // @todo: Only render when this.redrawRequested or clones rendered.
2129
+ if (this.profiler !== null) {
2130
+ if (rendererDrawProfilerId === -1) {
2131
+ rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw');
2132
+ }
2133
+ this.profiler.start(rendererDrawProfilerId);
2134
+ }
2135
+ this.renderer.draw();
2136
+ if (this.profiler !== null) {
2137
+ this.profiler.stop();
2138
+ }
2139
+ }
2140
+
2141
+ if (this._refreshTargets) {
2142
+ this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */);
2143
+ this._refreshTargets = false;
2144
+ }
2145
+
2146
+ if (!this._prevMonitorState.equals(this._monitorState)) {
2147
+ this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
2148
+ this._prevMonitorState = this._monitorState;
2149
+ }
2150
+
2151
+ if (this.profiler !== null) {
2152
+ this.profiler.stop();
2153
+ this.profiler.reportFrames();
2154
+ }
2155
+ }
2156
+
2157
+ /**
2158
+ * Get the number of threads in the given array that are monitor threads (threads
2159
+ * that update monitor values, and don't count as running a script).
2160
+ * @param {!Array.<Thread>} threads The set of threads to look through.
2161
+ * @return {number} The number of monitor threads in threads.
2162
+ */
2163
+ _getMonitorThreadCount (threads) {
2164
+ let count = 0;
2165
+ threads.forEach(thread => {
2166
+ if (thread.updateMonitor) count++;
2167
+ });
2168
+ return count;
2169
+ }
2170
+
2171
+ /**
2172
+ * Queue monitor blocks to sequencer to be run.
2173
+ */
2174
+ _pushMonitors () {
2175
+ this.monitorBlocks.runAllMonitored(this);
2176
+ }
2177
+
2178
+ /**
2179
+ * Set the current editing target known by the runtime.
2180
+ * @param {!Target} editingTarget New editing target.
2181
+ */
2182
+ setEditingTarget (editingTarget) {
2183
+ const oldEditingTarget = this._editingTarget;
2184
+ this._editingTarget = editingTarget;
2185
+ // Script glows must be cleared.
2186
+ this._scriptGlowsPreviousFrame = [];
2187
+ this._updateGlows();
2188
+
2189
+ if (oldEditingTarget !== this._editingTarget) {
2190
+ this.requestToolboxExtensionsUpdate();
2191
+ }
2192
+ }
2193
+
2194
+ /**
2195
+ * Set whether we are in 30 TPS compatibility mode.
2196
+ * @param {boolean} compatibilityModeOn True iff in compatibility mode.
2197
+ */
2198
+ setCompatibilityMode (compatibilityModeOn) {
2199
+ this.compatibilityMode = compatibilityModeOn;
2200
+ if (this._steppingInterval) {
2201
+ clearInterval(this._steppingInterval);
2202
+ this._steppingInterval = null;
2203
+ this.start();
2204
+ }
2205
+ }
2206
+
2207
+ /**
2208
+ * Emit glows/glow clears for scripts after a single tick.
2209
+ * Looks at `this.threads` and notices which have turned on/off new glows.
2210
+ * @param {Array.<Thread>=} optExtraThreads Optional list of inactive threads.
2211
+ */
2212
+ _updateGlows (optExtraThreads) {
2213
+ const searchThreads = [];
2214
+ searchThreads.push(...this.threads);
2215
+ if (optExtraThreads) {
2216
+ searchThreads.push(...optExtraThreads);
2217
+ }
2218
+ // Set of scripts that request a glow this frame.
2219
+ const requestedGlowsThisFrame = [];
2220
+ // Final set of scripts glowing during this frame.
2221
+ const finalScriptGlows = [];
2222
+ // Find all scripts that should be glowing.
2223
+ for (let i = 0; i < searchThreads.length; i++) {
2224
+ const thread = searchThreads[i];
2225
+ const target = thread.target;
2226
+ if (target === this._editingTarget) {
2227
+ const blockForThread = thread.blockGlowInFrame;
2228
+ if (thread.requestScriptGlowInFrame || thread.stackClick) {
2229
+ let script = target.blocks.getTopLevelScript(blockForThread);
2230
+ if (!script) {
2231
+ // Attempt to find in flyout blocks.
2232
+ script = this.flyoutBlocks.getTopLevelScript(
2233
+ blockForThread
2234
+ );
2235
+ }
2236
+ if (script) {
2237
+ requestedGlowsThisFrame.push(script);
2238
+ }
2239
+ }
2240
+ }
2241
+ }
2242
+ // Compare to previous frame.
2243
+ for (let j = 0; j < this._scriptGlowsPreviousFrame.length; j++) {
2244
+ const previousFrameGlow = this._scriptGlowsPreviousFrame[j];
2245
+ if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) {
2246
+ // Glow turned off.
2247
+ this.glowScript(previousFrameGlow, false);
2248
+ } else {
2249
+ // Still glowing.
2250
+ finalScriptGlows.push(previousFrameGlow);
2251
+ }
2252
+ }
2253
+ for (let k = 0; k < requestedGlowsThisFrame.length; k++) {
2254
+ const currentFrameGlow = requestedGlowsThisFrame[k];
2255
+ if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) {
2256
+ // Glow turned on.
2257
+ this.glowScript(currentFrameGlow, true);
2258
+ finalScriptGlows.push(currentFrameGlow);
2259
+ }
2260
+ }
2261
+ this._scriptGlowsPreviousFrame = finalScriptGlows;
2262
+ }
2263
+
2264
+ /**
2265
+ * Emit run start/stop after each tick. Emits when `this.threads.length` goes
2266
+ * between non-zero and zero
2267
+ *
2268
+ * @param {number} nonMonitorThreadCount The new nonMonitorThreadCount
2269
+ */
2270
+ _emitProjectRunStatus (nonMonitorThreadCount) {
2271
+ if (this._nonMonitorThreadCount === 0 && nonMonitorThreadCount > 0) {
2272
+ this.emit(Runtime.PROJECT_RUN_START);
2273
+ }
2274
+ if (this._nonMonitorThreadCount > 0 && nonMonitorThreadCount === 0) {
2275
+ this.emit(Runtime.PROJECT_RUN_STOP);
2276
+ }
2277
+ this._nonMonitorThreadCount = nonMonitorThreadCount;
2278
+ }
2279
+
2280
+ /**
2281
+ * "Quiet" a script's glow: stop the VM from generating glow/unglow events
2282
+ * about that script. Use when a script has just been deleted, but we may
2283
+ * still be tracking glow data about it.
2284
+ * @param {!string} scriptBlockId Id of top-level block in script to quiet.
2285
+ */
2286
+ quietGlow (scriptBlockId) {
2287
+ const index = this._scriptGlowsPreviousFrame.indexOf(scriptBlockId);
2288
+ if (index > -1) {
2289
+ this._scriptGlowsPreviousFrame.splice(index, 1);
2290
+ }
2291
+ }
2292
+
2293
+ /**
2294
+ * Emit feedback for block glowing (used in the sequencer).
2295
+ * @param {?string} blockId ID for the block to update glow
2296
+ * @param {boolean} isGlowing True to turn on glow; false to turn off.
2297
+ */
2298
+ glowBlock (blockId, isGlowing) {
2299
+ if (isGlowing) {
2300
+ this.emit(Runtime.BLOCK_GLOW_ON, {id: blockId});
2301
+ } else {
2302
+ this.emit(Runtime.BLOCK_GLOW_OFF, {id: blockId});
2303
+ }
2304
+ }
2305
+
2306
+ /**
2307
+ * Emit feedback for script glowing.
2308
+ * @param {?string} topBlockId ID for the top block to update glow
2309
+ * @param {boolean} isGlowing True to turn on glow; false to turn off.
2310
+ */
2311
+ glowScript (topBlockId, isGlowing) {
2312
+ if (isGlowing) {
2313
+ this.emit(Runtime.SCRIPT_GLOW_ON, {id: topBlockId});
2314
+ } else {
2315
+ this.emit(Runtime.SCRIPT_GLOW_OFF, {id: topBlockId});
2316
+ }
2317
+ }
2318
+
2319
+ /**
2320
+ * Emit whether blocks are being dragged over gui
2321
+ * @param {boolean} areBlocksOverGui True if blocks are dragged out of blocks workspace, false otherwise
2322
+ */
2323
+ emitBlockDragUpdate (areBlocksOverGui) {
2324
+ this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui);
2325
+ }
2326
+
2327
+ /**
2328
+ * Emit event to indicate that the block drag has ended with the blocks outside the blocks workspace
2329
+ * @param {Array.<object>} blocks The set of blocks dragged to the GUI
2330
+ * @param {string} topBlockId The original id of the top block being dragged
2331
+ */
2332
+ emitBlockEndDrag (blocks, topBlockId) {
2333
+ this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId);
2334
+ }
2335
+
2336
+ /**
2337
+ * Emit value for reporter to show in the blocks.
2338
+ * @param {string} blockId ID for the block.
2339
+ * @param {string} value Value to show associated with the block.
2340
+ */
2341
+ visualReport (blockId, value) {
2342
+ this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)});
2343
+ }
2344
+
2345
+ /**
2346
+ * Add a monitor to the state. If the monitor already exists in the state,
2347
+ * updates those properties that are defined in the given monitor record.
2348
+ * @param {!MonitorRecord} monitor Monitor to add.
2349
+ */
2350
+ requestAddMonitor (monitor) {
2351
+ const id = monitor.get('id');
2352
+ if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state
2353
+ // if the monitor did not exist in the state, add it
2354
+ this._monitorState = this._monitorState.set(id, monitor);
2355
+ }
2356
+ }
2357
+
2358
+ /**
2359
+ * Update a monitor in the state and report success/failure of update.
2360
+ * @param {!Map} monitor Monitor values to update. Values on the monitor with overwrite
2361
+ * values on the old monitor with the same ID. If a value isn't defined on the new monitor,
2362
+ * the old monitor will keep its old value.
2363
+ * @return {boolean} true if monitor exists in the state and was updated, false if it did not exist.
2364
+ */
2365
+ requestUpdateMonitor (monitor) {
2366
+ const id = monitor.get('id');
2367
+ if (this._monitorState.has(id)) {
2368
+ this._monitorState =
2369
+ // Use mergeWith here to prevent undefined values from overwriting existing ones
2370
+ this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => {
2371
+ if (typeof next === 'undefined' || next === null) {
2372
+ return prev;
2373
+ }
2374
+ return next;
2375
+ }, monitor));
2376
+ return true;
2377
+ }
2378
+ return false;
2379
+ }
2380
+
2381
+ /**
2382
+ * Removes a monitor from the state. Does nothing if the monitor already does
2383
+ * not exist in the state.
2384
+ * @param {!string} monitorId ID of the monitor to remove.
2385
+ */
2386
+ requestRemoveMonitor (monitorId) {
2387
+ this._monitorState = this._monitorState.delete(monitorId);
2388
+ }
2389
+
2390
+ /**
2391
+ * Hides a monitor and returns success/failure of action.
2392
+ * @param {!string} monitorId ID of the monitor to hide.
2393
+ * @return {boolean} true if monitor exists and was updated, false otherwise
2394
+ */
2395
+ requestHideMonitor (monitorId) {
2396
+ return this.requestUpdateMonitor(new Map([
2397
+ ['id', monitorId],
2398
+ ['visible', false]
2399
+ ]));
2400
+ }
2401
+
2402
+ /**
2403
+ * Shows a monitor and returns success/failure of action.
2404
+ * not exist in the state.
2405
+ * @param {!string} monitorId ID of the monitor to show.
2406
+ * @return {boolean} true if monitor exists and was updated, false otherwise
2407
+ */
2408
+ requestShowMonitor (monitorId) {
2409
+ return this.requestUpdateMonitor(new Map([
2410
+ ['id', monitorId],
2411
+ ['visible', true]
2412
+ ]));
2413
+ }
2414
+
2415
+ /**
2416
+ * Removes all monitors with the given target ID from the state. Does nothing if
2417
+ * the monitor already does not exist in the state.
2418
+ * @param {!string} targetId Remove all monitors with given target ID.
2419
+ */
2420
+ requestRemoveMonitorByTargetId (targetId) {
2421
+ this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId);
2422
+ }
2423
+
2424
+ /**
2425
+ * Get a target by its id.
2426
+ * @param {string} targetId Id of target to find.
2427
+ * @return {?Target} The target, if found.
2428
+ */
2429
+ getTargetById (targetId) {
2430
+ for (let i = 0; i < this.targets.length; i++) {
2431
+ const target = this.targets[i];
2432
+ if (target.id === targetId) {
2433
+ return target;
2434
+ }
2435
+ }
2436
+ }
2437
+
2438
+ /**
2439
+ * Get the first original (non-clone-block-created) sprite given a name.
2440
+ * @param {string} spriteName Name of sprite to look for.
2441
+ * @return {?Target} Target representing a sprite of the given name.
2442
+ */
2443
+ getSpriteTargetByName (spriteName) {
2444
+ for (let i = 0; i < this.targets.length; i++) {
2445
+ const target = this.targets[i];
2446
+ if (target.isStage) {
2447
+ continue;
2448
+ }
2449
+ if (target.sprite && target.sprite.name === spriteName) {
2450
+ return target;
2451
+ }
2452
+ }
2453
+ }
2454
+
2455
+ /**
2456
+ * Get a target by its drawable id.
2457
+ * @param {number} drawableID drawable id of target to find
2458
+ * @return {?Target} The target, if found
2459
+ */
2460
+ getTargetByDrawableId (drawableID) {
2461
+ for (let i = 0; i < this.targets.length; i++) {
2462
+ const target = this.targets[i];
2463
+ if (target.drawableID === drawableID) return target;
2464
+ }
2465
+ }
2466
+
2467
+ /**
2468
+ * Update the clone counter to track how many clones are created.
2469
+ * @param {number} changeAmount How many clones have been created/destroyed.
2470
+ */
2471
+ changeCloneCounter (changeAmount) {
2472
+ this._cloneCounter += changeAmount;
2473
+ }
2474
+
2475
+ /**
2476
+ * Return whether there are clones available.
2477
+ * @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
2478
+ */
2479
+ clonesAvailable () {
2480
+ return this._cloneCounter < Runtime.MAX_CLONES;
2481
+ }
2482
+
2483
+ /**
2484
+ * Handle that the project has loaded in the Virtual Machine.
2485
+ */
2486
+ handleProjectLoaded () {
2487
+ this.emit(Runtime.PROJECT_LOADED);
2488
+ this.resetRunId();
2489
+ }
2490
+
2491
+ /**
2492
+ * Report that the project has changed in a way that would affect serialization
2493
+ */
2494
+ emitProjectChanged () {
2495
+ this.emit(Runtime.PROJECT_CHANGED);
2496
+ }
2497
+
2498
+ /**
2499
+ * Report that a new target has been created, possibly by cloning an existing target.
2500
+ * @param {Target} newTarget - the newly created target.
2501
+ * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
2502
+ * @fires Runtime#targetWasCreated
2503
+ */
2504
+ fireTargetWasCreated (newTarget, sourceTarget) {
2505
+ this.emit('targetWasCreated', newTarget, sourceTarget);
2506
+ }
2507
+
2508
+ /**
2509
+ * Report that a clone target is being removed.
2510
+ * @param {Target} target - the target being removed
2511
+ * @fires Runtime#targetWasRemoved
2512
+ */
2513
+ fireTargetWasRemoved (target) {
2514
+ this.emit('targetWasRemoved', target);
2515
+ }
2516
+
2517
+ /**
2518
+ * Get a target representing the Scratch stage, if one exists.
2519
+ * @return {?Target} The target, if found.
2520
+ */
2521
+ getTargetForStage () {
2522
+ for (let i = 0; i < this.targets.length; i++) {
2523
+ const target = this.targets[i];
2524
+ if (target.isStage) {
2525
+ return target;
2526
+ }
2527
+ }
2528
+ }
2529
+
2530
+ /**
2531
+ * Get the editing target.
2532
+ * @return {?Target} The editing target.
2533
+ */
2534
+ getEditingTarget () {
2535
+ return this._editingTarget;
2536
+ }
2537
+
2538
+ getAllVarNamesOfType (varType) {
2539
+ let varNames = [];
2540
+ for (const target of this.targets) {
2541
+ const targetVarNames = target.getAllVariableNamesInScopeByType(varType, true);
2542
+ varNames = varNames.concat(targetVarNames);
2543
+ }
2544
+ return varNames;
2545
+ }
2546
+
2547
+ /**
2548
+ * Get the label or label function for an opcode
2549
+ * @param {string} extendedOpcode - the opcode you want a label for
2550
+ * @return {object} - object with label and category
2551
+ * @property {string} category - the category for this opcode
2552
+ * @property {Function} [labelFn] - function to generate the label for this opcode
2553
+ * @property {string} [label] - the label for this opcode if `labelFn` is absent
2554
+ */
2555
+ getLabelForOpcode (extendedOpcode) {
2556
+ const [category, opcode] = StringUtil.splitFirst(extendedOpcode, '_');
2557
+ if (!(category && opcode)) return;
2558
+
2559
+ const categoryInfo = this._blockInfo.find(ci => ci.id === category);
2560
+ if (!categoryInfo) return;
2561
+
2562
+ const block = categoryInfo.blocks.find(b => b.info.opcode === opcode);
2563
+ if (!block) return;
2564
+
2565
+ // TODO: we may want to format the label in a locale-specific way.
2566
+ return {
2567
+ category: 'extension', // This assumes that all extensions have the same monitor color.
2568
+ label: `${categoryInfo.name}: ${block.info.text}`
2569
+ };
2570
+ }
2571
+
2572
+ /**
2573
+ * Create a new global variable avoiding conflicts with other variable names.
2574
+ * @param {string} variableName The desired variable name for the new global variable.
2575
+ * This can be turned into a fresh name as necessary.
2576
+ * @param {string} optVarId An optional ID to use for the variable. A new one will be generated
2577
+ * if a falsey value for this parameter is provided.
2578
+ * @param {string} optVarType The type of the variable to create. Defaults to Variable.SCALAR_TYPE.
2579
+ * @return {Variable} The new variable that was created.
2580
+ */
2581
+ createNewGlobalVariable (variableName, optVarId, optVarType) {
2582
+ const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE;
2583
+ const allVariableNames = this.getAllVarNamesOfType(varType);
2584
+ const newName = StringUtil.unusedName(variableName, allVariableNames);
2585
+ const variable = new Variable(optVarId || uid(), newName, varType);
2586
+ const stage = this.getTargetForStage();
2587
+ stage.variables[variable.id] = variable;
2588
+ return variable;
2589
+ }
2590
+
2591
+ /**
2592
+ * Tell the runtime to request a redraw.
2593
+ * Use after a clone/sprite has completed some visible operation on the stage.
2594
+ */
2595
+ requestRedraw () {
2596
+ this.redrawRequested = true;
2597
+ }
2598
+
2599
+ /**
2600
+ * Emit a targets update at the end of the step if the provided target is
2601
+ * the original sprite
2602
+ * @param {!Target} target Target requesting the targets update
2603
+ */
2604
+ requestTargetsUpdate (target) {
2605
+ if (!target.isOriginal) return;
2606
+ this._refreshTargets = true;
2607
+ }
2608
+
2609
+ /**
2610
+ * Emit an event that indicates that the blocks on the workspace need updating.
2611
+ */
2612
+ requestBlocksUpdate () {
2613
+ this.emit(Runtime.BLOCKS_NEED_UPDATE);
2614
+ }
2615
+
2616
+ /**
2617
+ * Emit an event that indicates that the toolbox extension blocks need updating.
2618
+ */
2619
+ requestToolboxExtensionsUpdate () {
2620
+ this.emit(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE);
2621
+ }
2622
+
2623
+ /**
2624
+ * Set up timers to repeatedly step in a browser.
2625
+ */
2626
+ start () {
2627
+ // Do not start if we are already running
2628
+ if (this._steppingInterval) return;
2629
+
2630
+ let interval = Runtime.THREAD_STEP_INTERVAL;
2631
+ if (this.compatibilityMode) {
2632
+ interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
2633
+ }
2634
+ this.currentStepTime = interval;
2635
+ this._steppingInterval = setInterval(() => {
2636
+ this._step();
2637
+ }, interval);
2638
+ this.emit(Runtime.RUNTIME_STARTED);
2639
+ }
2640
+
2641
+ /**
2642
+ * Quit the Runtime, clearing any handles which might keep the process alive.
2643
+ * Do not use the runtime after calling this method. This method is meant for test shutdown.
2644
+ */
2645
+ quit () {
2646
+ clearInterval(this._steppingInterval);
2647
+ this._steppingInterval = null;
2648
+ }
2649
+
2650
+ /**
2651
+ * Turn on profiling.
2652
+ * @param {Profiler/FrameCallback} onFrame A callback handle passed a
2653
+ * profiling frame when the profiler reports its collected data.
2654
+ */
2655
+ enableProfiling (onFrame) {
2656
+ if (Profiler.available()) {
2657
+ this.profiler = new Profiler(onFrame);
2658
+ }
2659
+ }
2660
+
2661
+ /**
2662
+ * Turn off profiling.
2663
+ */
2664
+ disableProfiling () {
2665
+ this.profiler = null;
2666
+ }
2667
+
2668
+ /**
2669
+ * Update a millisecond timestamp value that is saved on the Runtime.
2670
+ * This value is helpful in certain instances for compatibility with Scratch 2,
2671
+ * which sometimes uses a `currentMSecs` timestamp value in Interpreter.as
2672
+ */
2673
+ updateCurrentMSecs () {
2674
+ this.currentMSecs = Date.now();
2675
+ }
2676
+ }
2677
+
2678
+ /**
2679
+ * Event fired after a new target has been created, possibly by cloning an existing target.
2680
+ *
2681
+ * @event Runtime#targetWasCreated
2682
+ * @param {Target} newTarget - the newly created target.
2683
+ * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
2684
+ */
2685
+
2686
+ module.exports = Runtime;