@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,1299 @@
1
+ const adapter = require('./adapter');
2
+ const mutationAdapter = require('./mutation-adapter');
3
+ const xmlEscape = require('../util/xml-escape');
4
+ const MonitorRecord = require('./monitor-record');
5
+ const Clone = require('../util/clone');
6
+ const {Map} = require('immutable');
7
+ const BlocksExecuteCache = require('./blocks-execute-cache');
8
+ const BlocksRuntimeCache = require('./blocks-runtime-cache');
9
+ const log = require('../util/log');
10
+ const Variable = require('./variable');
11
+ const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
12
+
13
+ /**
14
+ * @fileoverview
15
+ * Store and mutate the VM block representation,
16
+ * and handle updates from Scratch Blocks events.
17
+ */
18
+
19
+ /**
20
+ * Create a block container.
21
+ * @param {Runtime} runtime The runtime this block container operates within
22
+ * @param {boolean} optNoGlow Optional flag to indicate that blocks in this container
23
+ * should not request glows. This does not affect glows when clicking on a block to execute it.
24
+ */
25
+ class Blocks {
26
+ constructor (runtime, optNoGlow) {
27
+ this.runtime = runtime;
28
+
29
+ /**
30
+ * All blocks in the workspace.
31
+ * Keys are block IDs, values are metadata about the block.
32
+ * @type {Object.<string, Object>}
33
+ */
34
+ this._blocks = {};
35
+
36
+ /**
37
+ * All top-level scripts in the workspace.
38
+ * A list of block IDs that represent scripts (i.e., first block in script).
39
+ * @type {Array.<String>}
40
+ */
41
+ this._scripts = [];
42
+
43
+ /**
44
+ * Runtime Cache
45
+ * @type {{inputs: {}, procedureParamNames: {}, procedureDefinitions: {}}}
46
+ * @private
47
+ */
48
+ Object.defineProperty(this, '_cache', {writable: true, enumerable: false});
49
+ this._cache = {
50
+ /**
51
+ * Cache block inputs by block id
52
+ * @type {object.<string, !Array.<object>>}
53
+ */
54
+ inputs: {},
55
+ /**
56
+ * Cache procedure Param Names by block id
57
+ * @type {object.<string, ?Array.<string>>}
58
+ */
59
+ procedureParamNames: {},
60
+ /**
61
+ * Cache procedure definitions by block id
62
+ * @type {object.<string, ?string>}
63
+ */
64
+ procedureDefinitions: {},
65
+
66
+ /**
67
+ * A cache for execute to use and store on. Only available to
68
+ * execute.
69
+ * @type {object.<string, object>}
70
+ */
71
+ _executeCached: {},
72
+
73
+ /**
74
+ * A cache of block IDs and targets to start threads on as they are
75
+ * actively monitored.
76
+ * @type {Array<{blockId: string, target: Target}>}
77
+ */
78
+ _monitored: null,
79
+
80
+ /**
81
+ * A cache of hat opcodes to collection of theads to execute.
82
+ * @type {object.<string, object>}
83
+ */
84
+ scripts: {}
85
+ };
86
+
87
+ /**
88
+ * Flag which indicates that blocks in this container should not glow.
89
+ * Blocks will still glow when clicked on, but this flag is used to control
90
+ * whether the blocks in this container can request a glow as part of
91
+ * a running stack. E.g. the flyout block container and the monitor block container
92
+ * should not be able to request a glow, but blocks containers belonging to
93
+ * sprites should.
94
+ * @type {boolean}
95
+ */
96
+ this.forceNoGlow = optNoGlow || false;
97
+ }
98
+
99
+ /**
100
+ * Blockly inputs that represent statements/branch.
101
+ * are prefixed with this string.
102
+ * @const{string}
103
+ */
104
+ static get BRANCH_INPUT_PREFIX () {
105
+ return 'SUBSTACK';
106
+ }
107
+
108
+ /**
109
+ * Provide an object with metadata for the requested block ID.
110
+ * @param {!string} blockId ID of block we have stored.
111
+ * @return {?object} Metadata about the block, if it exists.
112
+ */
113
+ getBlock (blockId) {
114
+ return this._blocks[blockId];
115
+ }
116
+
117
+ /**
118
+ * Get all known top-level blocks that start scripts.
119
+ * @return {Array.<string>} List of block IDs.
120
+ */
121
+ getScripts () {
122
+ return this._scripts;
123
+ }
124
+
125
+ /**
126
+ * Get the next block for a particular block
127
+ * @param {?string} id ID of block to get the next block for
128
+ * @return {?string} ID of next block in the sequence
129
+ */
130
+ getNextBlock (id) {
131
+ const block = this._blocks[id];
132
+ return (typeof block === 'undefined') ? null : block.next;
133
+ }
134
+
135
+ /**
136
+ * Get the branch for a particular C-shaped block.
137
+ * @param {?string} id ID for block to get the branch for.
138
+ * @param {?number} branchNum Which branch to select (e.g. for if-else).
139
+ * @return {?string} ID of block in the branch.
140
+ */
141
+ getBranch (id, branchNum) {
142
+ const block = this._blocks[id];
143
+ if (typeof block === 'undefined') return null;
144
+ if (!branchNum) branchNum = 1;
145
+
146
+ let inputName = Blocks.BRANCH_INPUT_PREFIX;
147
+ if (branchNum > 1) {
148
+ inputName += branchNum;
149
+ }
150
+
151
+ // Empty C-block?
152
+ const input = block.inputs[inputName];
153
+ return (typeof input === 'undefined') ? null : input.block;
154
+ }
155
+
156
+ /**
157
+ * Get the opcode for a particular block
158
+ * @param {?object} block The block to query
159
+ * @return {?string} the opcode corresponding to that block
160
+ */
161
+ getOpcode (block) {
162
+ return (typeof block === 'undefined') ? null : block.opcode;
163
+ }
164
+
165
+ /**
166
+ * Get all fields and their values for a block.
167
+ * @param {?object} block The block to query.
168
+ * @return {?object} All fields and their values.
169
+ */
170
+ getFields (block) {
171
+ return (typeof block === 'undefined') ? null : block.fields;
172
+ }
173
+
174
+ /**
175
+ * Get all non-branch inputs for a block.
176
+ * @param {?object} block the block to query.
177
+ * @return {?Array.<object>} All non-branch inputs and their associated blocks.
178
+ */
179
+ getInputs (block) {
180
+ if (typeof block === 'undefined') return null;
181
+ let inputs = this._cache.inputs[block.id];
182
+ if (typeof inputs !== 'undefined') {
183
+ return inputs;
184
+ }
185
+
186
+ inputs = {};
187
+ for (const input in block.inputs) {
188
+ // Ignore blocks prefixed with branch prefix.
189
+ if (input.substring(0, Blocks.BRANCH_INPUT_PREFIX.length) !==
190
+ Blocks.BRANCH_INPUT_PREFIX) {
191
+ inputs[input] = block.inputs[input];
192
+ }
193
+ }
194
+
195
+ this._cache.inputs[block.id] = inputs;
196
+ return inputs;
197
+ }
198
+
199
+ /**
200
+ * Get mutation data for a block.
201
+ * @param {?object} block The block to query.
202
+ * @return {?object} Mutation for the block.
203
+ */
204
+ getMutation (block) {
205
+ return (typeof block === 'undefined') ? null : block.mutation;
206
+ }
207
+
208
+ /**
209
+ * Get the top-level script for a given block.
210
+ * @param {?string} id ID of block to query.
211
+ * @return {?string} ID of top-level script block.
212
+ */
213
+ getTopLevelScript (id) {
214
+ let block = this._blocks[id];
215
+ if (typeof block === 'undefined') return null;
216
+ while (block.parent !== null) {
217
+ block = this._blocks[block.parent];
218
+ }
219
+ return block.id;
220
+ }
221
+
222
+ /**
223
+ * Get the procedure definition for a given name.
224
+ * @param {?string} name Name of procedure to query.
225
+ * @return {?string} ID of procedure definition.
226
+ */
227
+ getProcedureDefinition (name) {
228
+ const blockID = this._cache.procedureDefinitions[name];
229
+ if (typeof blockID !== 'undefined') {
230
+ return blockID;
231
+ }
232
+
233
+ for (const id in this._blocks) {
234
+ if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
235
+ const block = this._blocks[id];
236
+ if (block.opcode === 'procedures_definition') {
237
+ const internal = this._getCustomBlockInternal(block);
238
+ if (internal && internal.mutation.proccode === name) {
239
+ this._cache.procedureDefinitions[name] = id; // The outer define block id
240
+ return id;
241
+ }
242
+ }
243
+ }
244
+
245
+ this._cache.procedureDefinitions[name] = null;
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Get names and ids of parameters for the given procedure.
251
+ * @param {?string} name Name of procedure to query.
252
+ * @return {?Array.<string>} List of param names for a procedure.
253
+ */
254
+ getProcedureParamNamesAndIds (name) {
255
+ return this.getProcedureParamNamesIdsAndDefaults(name).slice(0, 2);
256
+ }
257
+
258
+ /**
259
+ * Get names, ids, and defaults of parameters for the given procedure.
260
+ * @param {?string} name Name of procedure to query.
261
+ * @return {?Array.<string>} List of param names for a procedure.
262
+ */
263
+ getProcedureParamNamesIdsAndDefaults (name) {
264
+ const cachedNames = this._cache.procedureParamNames[name];
265
+ if (typeof cachedNames !== 'undefined') {
266
+ return cachedNames;
267
+ }
268
+
269
+ for (const id in this._blocks) {
270
+ if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue;
271
+ const block = this._blocks[id];
272
+ if (block.opcode === 'procedures_prototype' &&
273
+ block.mutation.proccode === name) {
274
+ const names = JSON.parse(block.mutation.argumentnames);
275
+ const ids = JSON.parse(block.mutation.argumentids);
276
+ const defaults = JSON.parse(block.mutation.argumentdefaults);
277
+
278
+ this._cache.procedureParamNames[name] = [names, ids, defaults];
279
+ return this._cache.procedureParamNames[name];
280
+ }
281
+ }
282
+
283
+ this._cache.procedureParamNames[name] = null;
284
+ return null;
285
+ }
286
+
287
+ duplicate () {
288
+ const newBlocks = new Blocks(this.runtime, this.forceNoGlow);
289
+ newBlocks._blocks = Clone.simple(this._blocks);
290
+ newBlocks._scripts = Clone.simple(this._scripts);
291
+ return newBlocks;
292
+ }
293
+ // ---------------------------------------------------------------------
294
+
295
+ /**
296
+ * Create event listener for blocks, variables, and comments. Handles validation and
297
+ * serves as a generic adapter between the blocks, variables, and the
298
+ * runtime interface.
299
+ * @param {object} e Blockly "block" or "variable" event
300
+ */
301
+ blocklyListen (e) {
302
+ // Validate event
303
+ if (typeof e !== 'object') return;
304
+ if (typeof e.blockId !== 'string' && typeof e.varId !== 'string' &&
305
+ typeof e.commentId !== 'string') {
306
+ return;
307
+ }
308
+ const stage = this.runtime.getTargetForStage();
309
+ const editingTarget = this.runtime.getEditingTarget();
310
+
311
+ // UI event: clicked scripts toggle in the runtime.
312
+ if (e.element === 'stackclick') {
313
+ this.runtime.toggleScript(e.blockId, {stackClick: true});
314
+ return;
315
+ }
316
+
317
+ // Block create/update/destroy
318
+ switch (e.type) {
319
+ case 'create': {
320
+ const newBlocks = adapter(e);
321
+ // A create event can create many blocks. Add them all.
322
+ for (let i = 0; i < newBlocks.length; i++) {
323
+ this.createBlock(newBlocks[i]);
324
+ }
325
+ break;
326
+ }
327
+ case 'change':
328
+ this.changeBlock({
329
+ id: e.blockId,
330
+ element: e.element,
331
+ name: e.name,
332
+ value: e.newValue
333
+ });
334
+ break;
335
+ case 'move':
336
+ this.moveBlock({
337
+ id: e.blockId,
338
+ oldParent: e.oldParentId,
339
+ oldInput: e.oldInputName,
340
+ newParent: e.newParentId,
341
+ newInput: e.newInputName,
342
+ newCoordinate: e.newCoordinate
343
+ });
344
+ break;
345
+ case 'dragOutside':
346
+ this.runtime.emitBlockDragUpdate(e.isOutside);
347
+ break;
348
+ case 'endDrag':
349
+ this.runtime.emitBlockDragUpdate(false /* areBlocksOverGui */);
350
+
351
+ // Drag blocks onto another sprite
352
+ if (e.isOutside) {
353
+ const newBlocks = adapter(e);
354
+ this.runtime.emitBlockEndDrag(newBlocks, e.blockId);
355
+ }
356
+ break;
357
+ case 'delete':
358
+ // Don't accept delete events for missing blocks,
359
+ // or shadow blocks being obscured.
360
+ if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId) ||
361
+ this._blocks[e.blockId].shadow) {
362
+ return;
363
+ }
364
+ // Inform any runtime to forget about glows on this script.
365
+ if (this._blocks[e.blockId].topLevel) {
366
+ this.runtime.quietGlow(e.blockId);
367
+ }
368
+ this.deleteBlock(e.blockId);
369
+ break;
370
+ case 'var_create':
371
+ // Check if the variable being created is global or local
372
+ // If local, create a local var on the current editing target, as long
373
+ // as there are no conflicts, and the current target is actually a sprite
374
+ // If global or if the editing target is not present or we somehow got
375
+ // into a state where a local var was requested for the stage,
376
+ // create a stage (global) var after checking for name conflicts
377
+ // on all the sprites.
378
+ if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) {
379
+ if (!editingTarget.lookupVariableById(e.varId)) {
380
+ editingTarget.createVariable(e.varId, e.varName, e.varType);
381
+ this.emitProjectChanged();
382
+ }
383
+ } else {
384
+ if (stage.lookupVariableById(e.varId)) {
385
+ // Do not re-create a variable if it already exists
386
+ return;
387
+ }
388
+ // Check for name conflicts in all of the targets
389
+ const allTargets = this.runtime.targets.filter(t => t.isOriginal);
390
+ for (const target of allTargets) {
391
+ if (target.lookupVariableByNameAndType(e.varName, e.varType, true)) {
392
+ return;
393
+ }
394
+ }
395
+ stage.createVariable(e.varId, e.varName, e.varType, e.isCloud);
396
+ this.emitProjectChanged();
397
+ }
398
+ break;
399
+ case 'var_rename':
400
+ if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) {
401
+ // This is a local variable, rename on the current target
402
+ editingTarget.renameVariable(e.varId, e.newName);
403
+ // Update all the blocks on the current target that use
404
+ // this variable
405
+ editingTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName);
406
+ } else {
407
+ // This is a global variable
408
+ stage.renameVariable(e.varId, e.newName);
409
+ // Update all blocks on all targets that use the renamed variable
410
+ const targets = this.runtime.targets;
411
+ for (let i = 0; i < targets.length; i++) {
412
+ const currTarget = targets[i];
413
+ currTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName);
414
+ }
415
+ }
416
+ this.emitProjectChanged();
417
+ break;
418
+ case 'var_delete': {
419
+ const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) ?
420
+ editingTarget : stage;
421
+ target.deleteVariable(e.varId);
422
+ this.emitProjectChanged();
423
+ break;
424
+ }
425
+ case 'comment_create':
426
+ if (this.runtime.getEditingTarget()) {
427
+ const currTarget = this.runtime.getEditingTarget();
428
+ currTarget.createComment(e.commentId, e.blockId, e.text,
429
+ e.xy.x, e.xy.y, e.width, e.height, e.minimized);
430
+
431
+ if (currTarget.comments[e.commentId].x === null &&
432
+ currTarget.comments[e.commentId].y === null) {
433
+ // Block comments imported from 2.0 projects are imported with their
434
+ // x and y coordinates set to null so that scratch-blocks can
435
+ // auto-position them. If we are receiving a create event for these
436
+ // comments, then the auto positioning should have taken place.
437
+ // Update the x and y position of these comments to match the
438
+ // one from the event.
439
+ currTarget.comments[e.commentId].x = e.xy.x;
440
+ currTarget.comments[e.commentId].y = e.xy.y;
441
+ }
442
+ }
443
+ this.emitProjectChanged();
444
+ break;
445
+ case 'comment_change':
446
+ if (this.runtime.getEditingTarget()) {
447
+ const currTarget = this.runtime.getEditingTarget();
448
+ if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
449
+ log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
450
+ return;
451
+ }
452
+ const comment = currTarget.comments[e.commentId];
453
+ const change = e.newContents_;
454
+ if (Object.prototype.hasOwnProperty.call(change, 'minimized')) {
455
+ comment.minimized = change.minimized;
456
+ }
457
+ if (Object.prototype.hasOwnProperty.call(change, 'width') &&
458
+ Object.prototype.hasOwnProperty.call(change, 'height')) {
459
+ comment.width = change.width;
460
+ comment.height = change.height;
461
+ }
462
+ if (Object.prototype.hasOwnProperty.call(change, 'text')) {
463
+ comment.text = change.text;
464
+ }
465
+ this.emitProjectChanged();
466
+ }
467
+ break;
468
+ case 'comment_move':
469
+ if (this.runtime.getEditingTarget()) {
470
+ const currTarget = this.runtime.getEditingTarget();
471
+ if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
472
+ log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`);
473
+ return;
474
+ }
475
+ const comment = currTarget.comments[e.commentId];
476
+ const newCoord = e.newCoordinate_;
477
+ comment.x = newCoord.x;
478
+ comment.y = newCoord.y;
479
+
480
+ this.emitProjectChanged();
481
+ }
482
+ break;
483
+ case 'comment_delete':
484
+ if (this.runtime.getEditingTarget()) {
485
+ const currTarget = this.runtime.getEditingTarget();
486
+ if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) {
487
+ // If we're in this state, we have probably received
488
+ // a delete event from a workspace that we switched from
489
+ // (e.g. a delete event for a comment on sprite a's workspace
490
+ // when switching from sprite a to sprite b)
491
+ return;
492
+ }
493
+ delete currTarget.comments[e.commentId];
494
+ if (e.blockId) {
495
+ const block = currTarget.blocks.getBlock(e.blockId);
496
+ if (!block) {
497
+ log.warn(`Could not find block referenced by comment with id: ${e.commentId}`);
498
+ return;
499
+ }
500
+ delete block.comment;
501
+ }
502
+
503
+ this.emitProjectChanged();
504
+ }
505
+ break;
506
+ }
507
+ }
508
+
509
+ // ---------------------------------------------------------------------
510
+
511
+ /**
512
+ * Reset all runtime caches.
513
+ */
514
+ resetCache () {
515
+ this._cache.inputs = {};
516
+ this._cache.procedureParamNames = {};
517
+ this._cache.procedureDefinitions = {};
518
+ this._cache._executeCached = {};
519
+ this._cache._monitored = null;
520
+ this._cache.scripts = {};
521
+ }
522
+
523
+ /**
524
+ * Emit a project changed event if this is a block container
525
+ * that can affect the project state.
526
+ */
527
+ emitProjectChanged () {
528
+ if (!this.forceNoGlow) {
529
+ this.runtime.emitProjectChanged();
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Block management: create blocks and scripts from a `create` event
535
+ * @param {!object} block Blockly create event to be processed
536
+ */
537
+ createBlock (block) {
538
+ // Does the block already exist?
539
+ // Could happen, e.g., for an unobscured shadow.
540
+ if (Object.prototype.hasOwnProperty.call(this._blocks, block.id)) {
541
+ return;
542
+ }
543
+ // Create new block.
544
+ this._blocks[block.id] = block;
545
+ // Push block id to scripts array.
546
+ // Blocks are added as a top-level stack if they are marked as a top-block
547
+ // (if they were top-level XML in the event).
548
+ if (block.topLevel) {
549
+ this._addScript(block.id);
550
+ }
551
+
552
+ this.resetCache();
553
+
554
+ // A new block was actually added to the block container,
555
+ // emit a project changed event
556
+ this.emitProjectChanged();
557
+ }
558
+
559
+ /**
560
+ * Block management: change block field values
561
+ * @param {!object} args Blockly change event to be processed
562
+ */
563
+ changeBlock (args) {
564
+ // Validate
565
+ if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
566
+ let block = this._blocks[args.id];
567
+ if (typeof block === 'undefined') return;
568
+ switch (args.element) {
569
+ case 'field':
570
+ // TODO when the field of a monitored block changes,
571
+ // update the checkbox in the flyout based on whether
572
+ // a monitor for that current combination of selected parameters exists
573
+ // e.g.
574
+ // 1. check (current [v year])
575
+ // 2. switch dropdown in flyout block to (current [v minute])
576
+ // 3. the checkbox should become unchecked if we're not already
577
+ // monitoring current minute
578
+
579
+
580
+ // Update block value
581
+ if (!block.fields[args.name]) return;
582
+ if (args.name === 'VARIABLE' || args.name === 'LIST' ||
583
+ args.name === 'BROADCAST_OPTION') {
584
+ // Get variable name using the id in args.value.
585
+ const variable = this.runtime.getEditingTarget().lookupVariableById(args.value);
586
+ if (variable) {
587
+ block.fields[args.name].value = variable.name;
588
+ block.fields[args.name].id = args.value;
589
+ }
590
+ } else {
591
+ // Changing the value in a dropdown
592
+ block.fields[args.name].value = args.value;
593
+
594
+ // The selected item in the sensing of block menu needs to change based on the
595
+ // selected target. Set it to the first item in the menu list.
596
+ // TODO: (#1787)
597
+ if (block.opcode === 'sensing_of_object_menu') {
598
+ if (block.fields.OBJECT.value === '_stage_') {
599
+ this._blocks[block.parent].fields.PROPERTY.value = 'backdrop #';
600
+ } else {
601
+ this._blocks[block.parent].fields.PROPERTY.value = 'x position';
602
+ }
603
+ this.runtime.requestBlocksUpdate();
604
+ }
605
+
606
+ const flyoutBlock = block.shadow && block.parent ? this._blocks[block.parent] : block;
607
+ if (flyoutBlock.isMonitored) {
608
+ this.runtime.requestUpdateMonitor(Map({
609
+ id: flyoutBlock.id,
610
+ params: this._getBlockParams(flyoutBlock)
611
+ }));
612
+ }
613
+ }
614
+ break;
615
+ case 'mutation':
616
+ block.mutation = mutationAdapter(args.value);
617
+ break;
618
+ case 'checkbox': {
619
+ // A checkbox usually has a one to one correspondence with the monitor
620
+ // block but in the case of monitored reporters that have arguments,
621
+ // map the old id to a new id, creating a new monitor block if necessary
622
+ if (block.fields && Object.keys(block.fields).length > 0 &&
623
+ block.opcode !== 'data_variable' && block.opcode !== 'data_listcontents') {
624
+
625
+ // This block has an argument which needs to get separated out into
626
+ // multiple monitor blocks with ids based on the selected argument
627
+ const newId = getMonitorIdForBlockWithArgs(block.id, block.fields);
628
+ // Note: we're not just constantly creating a longer and longer id everytime we check
629
+ // the checkbox because we're using the id of the block in the flyout as the base
630
+
631
+ // check if a block with the new id already exists, otherwise create
632
+ let newBlock = this.runtime.monitorBlocks.getBlock(newId);
633
+ if (!newBlock) {
634
+ newBlock = JSON.parse(JSON.stringify(block));
635
+ newBlock.id = newId;
636
+ this.runtime.monitorBlocks.createBlock(newBlock);
637
+ }
638
+
639
+ block = newBlock; // Carry on through the rest of this code with newBlock
640
+ }
641
+
642
+ const wasMonitored = block.isMonitored;
643
+ block.isMonitored = args.value;
644
+
645
+ // Variable blocks may be sprite specific depending on the owner of the variable
646
+ let isSpriteLocalVariable = false;
647
+ if (block.opcode === 'data_variable') {
648
+ isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.VARIABLE.id]);
649
+ } else if (block.opcode === 'data_listcontents') {
650
+ isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.LIST.id]);
651
+ }
652
+
653
+ const isSpriteSpecific = isSpriteLocalVariable ||
654
+ (Object.prototype.hasOwnProperty.call(this.runtime.monitorBlockInfo, block.opcode) &&
655
+ this.runtime.monitorBlockInfo[block.opcode].isSpriteSpecific);
656
+ if (isSpriteSpecific) {
657
+ // If creating a new sprite specific monitor, the only possible target is
658
+ // the current editing one b/c you cannot dynamically create monitors.
659
+ // Also, do not change the targetId if it has already been assigned
660
+ block.targetId = block.targetId || this.runtime.getEditingTarget().id;
661
+ } else {
662
+ block.targetId = null;
663
+ }
664
+
665
+ if (wasMonitored && !block.isMonitored) {
666
+ this.runtime.requestHideMonitor(block.id);
667
+ } else if (!wasMonitored && block.isMonitored) {
668
+ // Tries to show the monitor for specified block. If it doesn't exist, add the monitor.
669
+ if (!this.runtime.requestShowMonitor(block.id)) {
670
+ this.runtime.requestAddMonitor(MonitorRecord({
671
+ id: block.id,
672
+ targetId: block.targetId,
673
+ spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null,
674
+ opcode: block.opcode,
675
+ params: this._getBlockParams(block),
676
+ // @todo(vm#565) for numerical values with decimals, some countries use comma
677
+ value: '',
678
+ mode: block.opcode === 'data_listcontents' ? 'list' : 'default'
679
+ }));
680
+ }
681
+ }
682
+ break;
683
+ }
684
+ }
685
+
686
+ this.emitProjectChanged();
687
+
688
+ this.resetCache();
689
+ }
690
+
691
+ /**
692
+ * Block management: move blocks from parent to parent
693
+ * @param {!object} e Blockly move event to be processed
694
+ */
695
+ moveBlock (e) {
696
+ if (!Object.prototype.hasOwnProperty.call(this._blocks, e.id)) {
697
+ return;
698
+ }
699
+
700
+ const block = this._blocks[e.id];
701
+ // Track whether a change actually occurred
702
+ // ignoring changes like routine re-positioning
703
+ // of a block when loading a workspace
704
+ let didChange = false;
705
+
706
+ // Move coordinate changes.
707
+ if (e.newCoordinate) {
708
+
709
+ didChange = (block.x !== e.newCoordinate.x) || (block.y !== e.newCoordinate.y);
710
+
711
+ block.x = e.newCoordinate.x;
712
+ block.y = e.newCoordinate.y;
713
+ }
714
+
715
+ // Remove from any old parent.
716
+ if (typeof e.oldParent !== 'undefined') {
717
+ const oldParent = this._blocks[e.oldParent];
718
+ if (typeof e.oldInput !== 'undefined' &&
719
+ oldParent.inputs[e.oldInput].block === e.id) {
720
+ // This block was connected to the old parent's input.
721
+ oldParent.inputs[e.oldInput].block = null;
722
+ } else if (oldParent.next === e.id) {
723
+ // This block was connected to the old parent's next connection.
724
+ oldParent.next = null;
725
+ }
726
+ this._blocks[e.id].parent = null;
727
+ didChange = true;
728
+ }
729
+
730
+ // Is this block a top-level block?
731
+ if (typeof e.newParent === 'undefined') {
732
+ this._addScript(e.id);
733
+ } else {
734
+ // Remove script, if one exists.
735
+ this._deleteScript(e.id);
736
+ // Otherwise, try to connect it in its new place.
737
+ if (typeof e.newInput === 'undefined') {
738
+ // Moved to the new parent's next connection.
739
+ this._blocks[e.newParent].next = e.id;
740
+ } else {
741
+ // Moved to the new parent's input.
742
+ // Don't obscure the shadow block.
743
+ let oldShadow = null;
744
+ if (Object.prototype.hasOwnProperty.call(this._blocks[e.newParent].inputs, e.newInput)) {
745
+ oldShadow = this._blocks[e.newParent].inputs[e.newInput].shadow;
746
+ }
747
+
748
+ // If the block being attached is itself a shadow, make sure to set
749
+ // both block and shadow to that blocks ID. This happens when adding
750
+ // inputs to a custom procedure.
751
+ if (this._blocks[e.id].shadow) oldShadow = e.id;
752
+
753
+ this._blocks[e.newParent].inputs[e.newInput] = {
754
+ name: e.newInput,
755
+ block: e.id,
756
+ shadow: oldShadow
757
+ };
758
+ }
759
+ this._blocks[e.id].parent = e.newParent;
760
+ didChange = true;
761
+ }
762
+ this.resetCache();
763
+
764
+ if (didChange) this.emitProjectChanged();
765
+ }
766
+
767
+
768
+ /**
769
+ * Block management: run all blocks.
770
+ * @param {!object} runtime Runtime to run all blocks in.
771
+ */
772
+ runAllMonitored (runtime) {
773
+ if (this._cache._monitored === null) {
774
+ this._cache._monitored = Object.keys(this._blocks)
775
+ .filter(blockId => this.getBlock(blockId).isMonitored)
776
+ .map(blockId => {
777
+ const targetId = this.getBlock(blockId).targetId;
778
+ return {
779
+ blockId,
780
+ target: targetId ? runtime.getTargetById(targetId) : null
781
+ };
782
+ });
783
+ }
784
+
785
+ const monitored = this._cache._monitored;
786
+ for (let i = 0; i < monitored.length; i++) {
787
+ const {blockId, target} = monitored[i];
788
+ runtime.addMonitorScript(blockId, target);
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Block management: delete blocks and their associated scripts. Does nothing if a block
794
+ * with the given ID does not exist.
795
+ * @param {!string} blockId Id of block to delete
796
+ */
797
+ deleteBlock (blockId) {
798
+ // @todo In runtime, stop threads running on this script.
799
+
800
+ // Get block
801
+ const block = this._blocks[blockId];
802
+ if (!block) {
803
+ // No block with the given ID exists
804
+ return;
805
+ }
806
+
807
+ // Delete children
808
+ if (block.next !== null) {
809
+ this.deleteBlock(block.next);
810
+ }
811
+
812
+ // Delete inputs (including branches)
813
+ for (const input in block.inputs) {
814
+ // If it's null, the block in this input moved away.
815
+ if (block.inputs[input].block !== null) {
816
+ this.deleteBlock(block.inputs[input].block);
817
+ }
818
+ // Delete obscured shadow blocks.
819
+ if (block.inputs[input].shadow !== null &&
820
+ block.inputs[input].shadow !== block.inputs[input].block) {
821
+ this.deleteBlock(block.inputs[input].shadow);
822
+ }
823
+ }
824
+
825
+ // Delete any script starting with this block.
826
+ this._deleteScript(blockId);
827
+
828
+ // Delete block itself.
829
+ delete this._blocks[blockId];
830
+
831
+ this.resetCache();
832
+ this.emitProjectChanged();
833
+ }
834
+
835
+ /**
836
+ * Delete all blocks and their associated scripts.
837
+ */
838
+ deleteAllBlocks () {
839
+ const blockIds = Object.keys(this._blocks);
840
+ blockIds.forEach(blockId => this.deleteBlock(blockId));
841
+ }
842
+
843
+ /**
844
+ * Returns a map of all references to variables or lists from blocks
845
+ * in this block container.
846
+ * @param {Array<object>} optBlocks Optional list of blocks to constrain the search to.
847
+ * This is useful for getting variable/list references for a stack of blocks instead
848
+ * of all blocks on the workspace
849
+ * @param {?boolean} optIncludeBroadcast Optional whether to include broadcast fields.
850
+ * @return {object} A map of variable ID to a list of all variable references
851
+ * for that ID. A variable reference contains the field referencing that variable
852
+ * and also the type of the variable being referenced.
853
+ */
854
+ getAllVariableAndListReferences (optBlocks, optIncludeBroadcast) {
855
+ const blocks = optBlocks ? optBlocks : this._blocks;
856
+ const allReferences = Object.create(null);
857
+ for (const blockId in blocks) {
858
+ let varOrListField = null;
859
+ let varType = null;
860
+ if (blocks[blockId].fields.VARIABLE) {
861
+ varOrListField = blocks[blockId].fields.VARIABLE;
862
+ varType = Variable.SCALAR_TYPE;
863
+ } else if (blocks[blockId].fields.LIST) {
864
+ varOrListField = blocks[blockId].fields.LIST;
865
+ varType = Variable.LIST_TYPE;
866
+ } else if (optIncludeBroadcast && blocks[blockId].fields.BROADCAST_OPTION) {
867
+ varOrListField = blocks[blockId].fields.BROADCAST_OPTION;
868
+ varType = Variable.BROADCAST_MESSAGE_TYPE;
869
+ }
870
+ if (varOrListField) {
871
+ const currVarId = varOrListField.id;
872
+ if (allReferences[currVarId]) {
873
+ allReferences[currVarId].push({
874
+ referencingField: varOrListField,
875
+ type: varType
876
+ });
877
+ } else {
878
+ allReferences[currVarId] = [{
879
+ referencingField: varOrListField,
880
+ type: varType
881
+ }];
882
+ }
883
+ }
884
+ }
885
+ return allReferences;
886
+ }
887
+
888
+ /**
889
+ * Keep blocks up to date after a variable gets renamed.
890
+ * @param {string} varId The id of the variable that was renamed
891
+ * @param {string} newName The new name of the variable that was renamed
892
+ */
893
+ updateBlocksAfterVarRename (varId, newName) {
894
+ const blocks = this._blocks;
895
+ for (const blockId in blocks) {
896
+ let varOrListField = null;
897
+ if (blocks[blockId].fields.VARIABLE) {
898
+ varOrListField = blocks[blockId].fields.VARIABLE;
899
+ } else if (blocks[blockId].fields.LIST) {
900
+ varOrListField = blocks[blockId].fields.LIST;
901
+ }
902
+ if (varOrListField) {
903
+ const currFieldId = varOrListField.id;
904
+ if (varId === currFieldId) {
905
+ varOrListField.value = newName;
906
+ }
907
+ }
908
+ }
909
+ }
910
+
911
+ /**
912
+ * Keep blocks up to date after they are shared between targets.
913
+ * @param {boolean} isStage If the new target is a stage.
914
+ */
915
+ updateTargetSpecificBlocks (isStage) {
916
+ const blocks = this._blocks;
917
+ for (const blockId in blocks) {
918
+ if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') {
919
+ blocks[blockId].opcode = 'event_whenstageclicked';
920
+ } else if (!isStage && blocks[blockId].opcode === 'event_whenstageclicked') {
921
+ blocks[blockId].opcode = 'event_whenthisspriteclicked';
922
+ }
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Update blocks after a sound, costume, or backdrop gets renamed.
928
+ * Any block referring to the old name of the asset should get updated
929
+ * to refer to the new name.
930
+ * @param {string} oldName The old name of the asset that was renamed.
931
+ * @param {string} newName The new name of the asset that was renamed.
932
+ * @param {string} assetType String representation of the kind of asset
933
+ * that was renamed. This can be one of 'sprite','costume', 'sound', or
934
+ * 'backdrop'.
935
+ */
936
+ updateAssetName (oldName, newName, assetType) {
937
+ let getAssetField;
938
+ if (assetType === 'costume') {
939
+ getAssetField = this._getCostumeField.bind(this);
940
+ } else if (assetType === 'sound') {
941
+ getAssetField = this._getSoundField.bind(this);
942
+ } else if (assetType === 'backdrop') {
943
+ getAssetField = this._getBackdropField.bind(this);
944
+ } else if (assetType === 'sprite') {
945
+ getAssetField = this._getSpriteField.bind(this);
946
+ } else {
947
+ return;
948
+ }
949
+ const blocks = this._blocks;
950
+ for (const blockId in blocks) {
951
+ const assetField = getAssetField(blockId);
952
+ if (assetField && assetField.value === oldName) {
953
+ assetField.value = newName;
954
+ }
955
+ }
956
+ }
957
+
958
+ /**
959
+ * Update sensing_of blocks after a variable gets renamed.
960
+ * @param {string} oldName The old name of the variable that was renamed.
961
+ * @param {string} newName The new name of the variable that was renamed.
962
+ * @param {string} targetName The name of the target the variable belongs to.
963
+ * @return {boolean} Returns true if any of the blocks were updated.
964
+ */
965
+ updateSensingOfReference (oldName, newName, targetName) {
966
+ const blocks = this._blocks;
967
+ let blockUpdated = false;
968
+ for (const blockId in blocks) {
969
+ const block = blocks[blockId];
970
+ if (block.opcode === 'sensing_of' &&
971
+ block.fields.PROPERTY.value === oldName &&
972
+ // If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored.
973
+ block.inputs.OBJECT.block === block.inputs.OBJECT.shadow) {
974
+ const inputBlock = this.getBlock(block.inputs.OBJECT.block);
975
+ if (inputBlock.fields.OBJECT.value === targetName) {
976
+ block.fields.PROPERTY.value = newName;
977
+ blockUpdated = true;
978
+ }
979
+ }
980
+ }
981
+ if (blockUpdated) this.resetCache();
982
+ return blockUpdated;
983
+ }
984
+
985
+ /**
986
+ * Helper function to retrieve a costume menu field from a block given its id.
987
+ * @param {string} blockId A unique identifier for a block
988
+ * @return {?object} The costume menu field of the block with the given block id.
989
+ * Null if either a block with the given id doesn't exist or if a costume menu field
990
+ * does not exist on the block with the given id.
991
+ */
992
+ _getCostumeField (blockId) {
993
+ const block = this.getBlock(blockId);
994
+ if (block && Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')) {
995
+ return block.fields.COSTUME;
996
+ }
997
+ return null;
998
+ }
999
+
1000
+ /**
1001
+ * Helper function to retrieve a sound menu field from a block given its id.
1002
+ * @param {string} blockId A unique identifier for a block
1003
+ * @return {?object} The sound menu field of the block with the given block id.
1004
+ * Null, if either a block with the given id doesn't exist or if a sound menu field
1005
+ * does not exist on the block with the given id.
1006
+ */
1007
+ _getSoundField (blockId) {
1008
+ const block = this.getBlock(blockId);
1009
+ if (block && Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')) {
1010
+ return block.fields.SOUND_MENU;
1011
+ }
1012
+ return null;
1013
+ }
1014
+
1015
+ /**
1016
+ * Helper function to retrieve a backdrop menu field from a block given its id.
1017
+ * @param {string} blockId A unique identifier for a block
1018
+ * @return {?object} The backdrop menu field of the block with the given block id.
1019
+ * Null, if either a block with the given id doesn't exist or if a backdrop menu field
1020
+ * does not exist on the block with the given id.
1021
+ */
1022
+ _getBackdropField (blockId) {
1023
+ const block = this.getBlock(blockId);
1024
+ if (block && Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')) {
1025
+ return block.fields.BACKDROP;
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ /**
1031
+ * Helper function to retrieve a sprite menu field from a block given its id.
1032
+ * @param {string} blockId A unique identifier for a block
1033
+ * @return {?object} The sprite menu field of the block with the given block id.
1034
+ * Null, if either a block with the given id doesn't exist or if a sprite menu field
1035
+ * does not exist on the block with the given id.
1036
+ */
1037
+ _getSpriteField (blockId) {
1038
+ const block = this.getBlock(blockId);
1039
+ if (!block) {
1040
+ return null;
1041
+ }
1042
+ const spriteMenuNames = ['TOWARDS', 'TO', 'OBJECT', 'VIDEOONMENU2',
1043
+ 'DISTANCETOMENU', 'TOUCHINGOBJECTMENU', 'CLONE_OPTION'];
1044
+ for (let i = 0; i < spriteMenuNames.length; i++) {
1045
+ const menuName = spriteMenuNames[i];
1046
+ if (Object.prototype.hasOwnProperty.call(block.fields, menuName)) {
1047
+ return block.fields[menuName];
1048
+ }
1049
+ }
1050
+ return null;
1051
+ }
1052
+
1053
+ // ---------------------------------------------------------------------
1054
+
1055
+ /**
1056
+ * Encode all of `this._blocks` as an XML string usable
1057
+ * by a Blockly/scratch-blocks workspace.
1058
+ * @param {object<string, Comment>} comments Map of comments referenced by id
1059
+ * @return {string} String of XML representing this object's blocks.
1060
+ */
1061
+ toXML (comments) {
1062
+ return this._scripts.map(script => this.blockToXML(script, comments)).join();
1063
+ }
1064
+
1065
+ /**
1066
+ * Recursively encode an individual block and its children
1067
+ * into a Blockly/scratch-blocks XML string.
1068
+ * @param {!string} blockId ID of block to encode.
1069
+ * @param {object<string, Comment>} comments Map of comments referenced by id
1070
+ * @return {string} String of XML representing this block and any children.
1071
+ */
1072
+ blockToXML (blockId, comments) {
1073
+ const block = this._blocks[blockId];
1074
+ // block should exist, but currently some blocks' next property point
1075
+ // to a blockId for non-existent blocks. Until we track down that behavior,
1076
+ // this early exit allows the project to load.
1077
+ if (!block) return;
1078
+ // Encode properties of this block.
1079
+ const tagName = (block.shadow) ? 'shadow' : 'block';
1080
+ let xmlString =
1081
+ `<${tagName}
1082
+ id="${block.id}"
1083
+ type="${block.opcode}"
1084
+ ${block.topLevel ? `x="${block.x}" y="${block.y}"` : ''}
1085
+ >`;
1086
+ const commentId = block.comment;
1087
+ if (commentId) {
1088
+ if (comments) {
1089
+ if (Object.prototype.hasOwnProperty.call(comments, commentId)) {
1090
+ xmlString += comments[commentId].toXML();
1091
+ } else {
1092
+ log.warn(`Could not find comment with id: ${commentId} in provided comment descriptions.`);
1093
+ }
1094
+ } else {
1095
+ log.warn(`Cannot serialize comment with id: ${commentId}; no comment descriptions provided.`);
1096
+ }
1097
+ }
1098
+ // Add any mutation. Must come before inputs.
1099
+ if (block.mutation) {
1100
+ xmlString += this.mutationToXML(block.mutation);
1101
+ }
1102
+ // Add any inputs on this block.
1103
+ for (const input in block.inputs) {
1104
+ if (!Object.prototype.hasOwnProperty.call(block.inputs, input)) continue;
1105
+ const blockInput = block.inputs[input];
1106
+ // Only encode a value tag if the value input is occupied.
1107
+ if (blockInput.block || blockInput.shadow) {
1108
+ xmlString += `<value name="${blockInput.name}">`;
1109
+ if (blockInput.block) {
1110
+ xmlString += this.blockToXML(blockInput.block, comments);
1111
+ }
1112
+ if (blockInput.shadow && blockInput.shadow !== blockInput.block) {
1113
+ // Obscured shadow.
1114
+ xmlString += this.blockToXML(blockInput.shadow, comments);
1115
+ }
1116
+ xmlString += '</value>';
1117
+ }
1118
+ }
1119
+ // Add any fields on this block.
1120
+ for (const field in block.fields) {
1121
+ if (!Object.prototype.hasOwnProperty.call(block.fields, field)) continue;
1122
+ const blockField = block.fields[field];
1123
+ xmlString += `<field name="${blockField.name}"`;
1124
+ const fieldId = blockField.id;
1125
+ if (fieldId) {
1126
+ xmlString += ` id="${fieldId}"`;
1127
+ }
1128
+ const varType = blockField.variableType;
1129
+ if (typeof varType === 'string') {
1130
+ xmlString += ` variabletype="${varType}"`;
1131
+ }
1132
+ let value = blockField.value;
1133
+ if (typeof value === 'string') {
1134
+ value = xmlEscape(blockField.value);
1135
+ }
1136
+ xmlString += `>${value}</field>`;
1137
+ }
1138
+ // Add blocks connected to the next connection.
1139
+ if (block.next) {
1140
+ xmlString += `<next>${this.blockToXML(block.next, comments)}</next>`;
1141
+ }
1142
+ xmlString += `</${tagName}>`;
1143
+ return xmlString;
1144
+ }
1145
+
1146
+ /**
1147
+ * Recursively encode a mutation object to XML.
1148
+ * @param {!object} mutation Object representing a mutation.
1149
+ * @return {string} XML string representing a mutation.
1150
+ */
1151
+ mutationToXML (mutation) {
1152
+ let mutationString = `<${mutation.tagName}`;
1153
+ for (const prop in mutation) {
1154
+ if (prop === 'children' || prop === 'tagName') continue;
1155
+ let mutationValue = (typeof mutation[prop] === 'string') ?
1156
+ xmlEscape(mutation[prop]) : mutation[prop];
1157
+
1158
+ // Handle dynamic extension blocks
1159
+ if (prop === 'blockInfo') {
1160
+ mutationValue = xmlEscape(JSON.stringify(mutation[prop]));
1161
+ }
1162
+
1163
+ mutationString += ` ${prop}="${mutationValue}"`;
1164
+ }
1165
+ mutationString += '>';
1166
+ for (let i = 0; i < mutation.children.length; i++) {
1167
+ mutationString += this.mutationToXML(mutation.children[i]);
1168
+ }
1169
+ mutationString += `</${mutation.tagName}>`;
1170
+ return mutationString;
1171
+ }
1172
+
1173
+ // ---------------------------------------------------------------------
1174
+ /**
1175
+ * Helper to serialize block fields and input fields for reporting new monitors
1176
+ * @param {!object} block Block to be paramified.
1177
+ * @return {!object} object of param key/values.
1178
+ */
1179
+ _getBlockParams (block) {
1180
+ const params = {};
1181
+ for (const key in block.fields) {
1182
+ params[key] = block.fields[key].value;
1183
+ }
1184
+ for (const inputKey in block.inputs) {
1185
+ const inputBlock = this._blocks[block.inputs[inputKey].block];
1186
+ for (const key in inputBlock.fields) {
1187
+ params[key] = inputBlock.fields[key].value;
1188
+ }
1189
+ }
1190
+ return params;
1191
+ }
1192
+
1193
+ /**
1194
+ * Helper to get the corresponding internal procedure definition block
1195
+ * @param {!object} defineBlock Outer define block.
1196
+ * @return {!object} internal definition block which has the mutation.
1197
+ */
1198
+ _getCustomBlockInternal (defineBlock) {
1199
+ if (defineBlock.inputs && defineBlock.inputs.custom_block) {
1200
+ return this._blocks[defineBlock.inputs.custom_block.block];
1201
+ }
1202
+ }
1203
+
1204
+ /**
1205
+ * Helper to add a stack to `this._scripts`.
1206
+ * @param {?string} topBlockId ID of block that starts the script.
1207
+ */
1208
+ _addScript (topBlockId) {
1209
+ const i = this._scripts.indexOf(topBlockId);
1210
+ if (i > -1) return; // Already in scripts.
1211
+ this._scripts.push(topBlockId);
1212
+ // Update `topLevel` property on the top block.
1213
+ this._blocks[topBlockId].topLevel = true;
1214
+ }
1215
+
1216
+ /**
1217
+ * Helper to remove a script from `this._scripts`.
1218
+ * @param {?string} topBlockId ID of block that starts the script.
1219
+ */
1220
+ _deleteScript (topBlockId) {
1221
+ const i = this._scripts.indexOf(topBlockId);
1222
+ if (i > -1) this._scripts.splice(i, 1);
1223
+ // Update `topLevel` property on the top block.
1224
+ if (this._blocks[topBlockId]) this._blocks[topBlockId].topLevel = false;
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * A private method shared with execute to build an object containing the block
1230
+ * information execute needs and that is reset when other cached Blocks info is
1231
+ * reset.
1232
+ * @param {Blocks} blocks Blocks containing the expected blockId
1233
+ * @param {string} blockId blockId for the desired execute cache
1234
+ * @param {function} CacheType constructor for cached block information
1235
+ * @return {object} execute cache object
1236
+ */
1237
+ BlocksExecuteCache.getCached = function (blocks, blockId, CacheType) {
1238
+ let cached = blocks._cache._executeCached[blockId];
1239
+ if (typeof cached !== 'undefined') {
1240
+ return cached;
1241
+ }
1242
+
1243
+ const block = blocks.getBlock(blockId);
1244
+ if (typeof block === 'undefined') return null;
1245
+
1246
+ if (typeof CacheType === 'undefined') {
1247
+ cached = {
1248
+ id: blockId,
1249
+ opcode: blocks.getOpcode(block),
1250
+ fields: blocks.getFields(block),
1251
+ inputs: blocks.getInputs(block),
1252
+ mutation: blocks.getMutation(block)
1253
+ };
1254
+ } else {
1255
+ cached = new CacheType(blocks, {
1256
+ id: blockId,
1257
+ opcode: blocks.getOpcode(block),
1258
+ fields: blocks.getFields(block),
1259
+ inputs: blocks.getInputs(block),
1260
+ mutation: blocks.getMutation(block)
1261
+ });
1262
+ }
1263
+
1264
+ blocks._cache._executeCached[blockId] = cached;
1265
+ return cached;
1266
+ };
1267
+
1268
+ /**
1269
+ * Cache class constructor for runtime. Used to consider what threads should
1270
+ * start based on hat data.
1271
+ * @type {function}
1272
+ */
1273
+ const RuntimeScriptCache = BlocksRuntimeCache._RuntimeScriptCache;
1274
+
1275
+ /**
1276
+ * Get an array of scripts from a block container prefiltered to match opcode.
1277
+ * @param {Blocks} blocks - Container of blocks
1278
+ * @param {string} opcode - Opcode to filter top blocks by
1279
+ * @returns {Array.<RuntimeScriptCache>} - Array of RuntimeScriptCache cache
1280
+ * objects
1281
+ */
1282
+ BlocksRuntimeCache.getScripts = function (blocks, opcode) {
1283
+ let scripts = blocks._cache.scripts[opcode];
1284
+ if (!scripts) {
1285
+ scripts = blocks._cache.scripts[opcode] = [];
1286
+
1287
+ const allScripts = blocks._scripts;
1288
+ for (let i = 0; i < allScripts.length; i++) {
1289
+ const topBlockId = allScripts[i];
1290
+ const block = blocks.getBlock(topBlockId);
1291
+ if (block.opcode === opcode) {
1292
+ scripts.push(new RuntimeScriptCache(blocks, topBlockId));
1293
+ }
1294
+ }
1295
+ }
1296
+ return scripts;
1297
+ };
1298
+
1299
+ module.exports = Blocks;