@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,1586 @@
1
+ let _TextEncoder;
2
+ if (typeof TextEncoder === 'undefined') {
3
+ _TextEncoder = require('text-encoding').TextEncoder;
4
+ } else {
5
+ _TextEncoder = TextEncoder;
6
+ }
7
+ const EventEmitter = require('events');
8
+ const JSZip = require('jszip');
9
+
10
+ const Buffer = require('buffer').Buffer;
11
+ const centralDispatch = require('./dispatch/central-dispatch');
12
+ const ExtensionManager = require('./extension-support/extension-manager');
13
+ const log = require('./util/log');
14
+ const MathUtil = require('./util/math-util');
15
+ const Runtime = require('./engine/runtime');
16
+ const StringUtil = require('./util/string-util');
17
+ const formatMessage = require('format-message');
18
+
19
+ const Variable = require('./engine/variable');
20
+ const newBlockIds = require('./util/new-block-ids');
21
+
22
+ const {loadCostume} = require('./import/load-costume.js');
23
+ const {loadSound} = require('./import/load-sound.js');
24
+ const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets');
25
+ require('canvas-toBlob');
26
+
27
+ const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_'];
28
+
29
+ const CORE_EXTENSIONS = [
30
+ // 'motion',
31
+ // 'looks',
32
+ // 'sound',
33
+ // 'events',
34
+ // 'control',
35
+ // 'sensing',
36
+ // 'operators',
37
+ // 'variables',
38
+ // 'myBlocks'
39
+ ];
40
+
41
+ /**
42
+ * Handles connections between blocks, stage, and extensions.
43
+ * @constructor
44
+ */
45
+ class VirtualMachine extends EventEmitter {
46
+ constructor () {
47
+ super();
48
+
49
+ /**
50
+ * VM runtime, to store blocks, I/O devices, sprites/targets, etc.
51
+ * @type {!Runtime}
52
+ */
53
+ this.runtime = new Runtime();
54
+ centralDispatch.setService('runtime', this.runtime).catch(e => {
55
+ log.error(`Failed to register runtime service: ${JSON.stringify(e)}`);
56
+ });
57
+
58
+ /**
59
+ * The "currently editing"/selected target ID for the VM.
60
+ * Block events from any Blockly workspace are routed to this target.
61
+ * @type {Target}
62
+ */
63
+ this.editingTarget = null;
64
+
65
+ /**
66
+ * The currently dragging target, for redirecting IO data.
67
+ * @type {Target}
68
+ */
69
+ this._dragTarget = null;
70
+
71
+ // Runtime emits are passed along as VM emits.
72
+ this.runtime.on(Runtime.SCRIPT_GLOW_ON, glowData => {
73
+ this.emit(Runtime.SCRIPT_GLOW_ON, glowData);
74
+ });
75
+ this.runtime.on(Runtime.SCRIPT_GLOW_OFF, glowData => {
76
+ this.emit(Runtime.SCRIPT_GLOW_OFF, glowData);
77
+ });
78
+ this.runtime.on(Runtime.BLOCK_GLOW_ON, glowData => {
79
+ this.emit(Runtime.BLOCK_GLOW_ON, glowData);
80
+ });
81
+ this.runtime.on(Runtime.BLOCK_GLOW_OFF, glowData => {
82
+ this.emit(Runtime.BLOCK_GLOW_OFF, glowData);
83
+ });
84
+ this.runtime.on(Runtime.PROJECT_START, () => {
85
+ this.emit(Runtime.PROJECT_START);
86
+ });
87
+ this.runtime.on(Runtime.PROJECT_RUN_START, () => {
88
+ this.emit(Runtime.PROJECT_RUN_START);
89
+ });
90
+ this.runtime.on(Runtime.PROJECT_RUN_STOP, () => {
91
+ this.emit(Runtime.PROJECT_RUN_STOP);
92
+ });
93
+ this.runtime.on(Runtime.PROJECT_CHANGED, () => {
94
+ this.emit(Runtime.PROJECT_CHANGED);
95
+ });
96
+ this.runtime.on(Runtime.VISUAL_REPORT, visualReport => {
97
+ this.emit(Runtime.VISUAL_REPORT, visualReport);
98
+ });
99
+ this.runtime.on(Runtime.TARGETS_UPDATE, emitProjectChanged => {
100
+ this.emitTargetsUpdate(emitProjectChanged);
101
+ });
102
+ this.runtime.on(Runtime.MONITORS_UPDATE, monitorList => {
103
+ this.emit(Runtime.MONITORS_UPDATE, monitorList);
104
+ });
105
+ this.runtime.on(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui => {
106
+ this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui);
107
+ });
108
+ this.runtime.on(Runtime.BLOCK_DRAG_END, (blocks, topBlockId) => {
109
+ this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId);
110
+ });
111
+ this.runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
112
+ this.emit(Runtime.EXTENSION_ADDED, categoryInfo);
113
+ });
114
+ this.runtime.on(Runtime.EXTENSION_FIELD_ADDED, (fieldName, fieldImplementation) => {
115
+ this.emit(Runtime.EXTENSION_FIELD_ADDED, fieldName, fieldImplementation);
116
+ });
117
+ this.runtime.on(Runtime.BLOCKSINFO_UPDATE, categoryInfo => {
118
+ this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
119
+ });
120
+ this.runtime.on(Runtime.BLOCKS_NEED_UPDATE, () => {
121
+ this.emitWorkspaceUpdate();
122
+ });
123
+ this.runtime.on(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE, () => {
124
+ this.extensionManager.refreshBlocks();
125
+ });
126
+ this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => {
127
+ this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info);
128
+ });
129
+ this.runtime.on(Runtime.USER_PICKED_PERIPHERAL, info => {
130
+ this.emit(Runtime.USER_PICKED_PERIPHERAL, info);
131
+ });
132
+ this.runtime.on(Runtime.PERIPHERAL_CONNECTED, () =>
133
+ this.emit(Runtime.PERIPHERAL_CONNECTED)
134
+ );
135
+ this.runtime.on(Runtime.PERIPHERAL_REQUEST_ERROR, () =>
136
+ this.emit(Runtime.PERIPHERAL_REQUEST_ERROR)
137
+ );
138
+ this.runtime.on(Runtime.PERIPHERAL_DISCONNECTED, () =>
139
+ this.emit(Runtime.PERIPHERAL_DISCONNECTED)
140
+ );
141
+ this.runtime.on(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data =>
142
+ this.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data)
143
+ );
144
+ this.runtime.on(Runtime.PERIPHERAL_SCAN_TIMEOUT, () =>
145
+ this.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT)
146
+ );
147
+ this.runtime.on(Runtime.MIC_LISTENING, listening => {
148
+ this.emit(Runtime.MIC_LISTENING, listening);
149
+ });
150
+ this.runtime.on(Runtime.RUNTIME_STARTED, () => {
151
+ this.emit(Runtime.RUNTIME_STARTED);
152
+ });
153
+ this.runtime.on(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData => {
154
+ this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData);
155
+ });
156
+
157
+ this.extensionManager = new ExtensionManager(this.runtime);
158
+
159
+ // Load core extensions
160
+ for (const id of CORE_EXTENSIONS) {
161
+ this.extensionManager.loadExtensionIdSync(id);
162
+ }
163
+
164
+ this.blockListener = this.blockListener.bind(this);
165
+ this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
166
+ this.monitorBlockListener = this.monitorBlockListener.bind(this);
167
+ this.variableListener = this.variableListener.bind(this);
168
+ }
169
+
170
+ /**
171
+ * Start running the VM - do this before anything else.
172
+ */
173
+ start () {
174
+ this.runtime.start();
175
+ }
176
+
177
+ /**
178
+ * Quit the VM, clearing any handles which might keep the process alive.
179
+ * Do not use the runtime after calling this method. This method is meant for test shutdown.
180
+ */
181
+ quit () {
182
+ this.runtime.quit();
183
+ }
184
+
185
+ /**
186
+ * "Green flag" handler - start all threads starting with a green flag.
187
+ */
188
+ greenFlag () {
189
+ this.runtime.greenFlag();
190
+ }
191
+
192
+ /**
193
+ * Set whether the VM is in "turbo mode."
194
+ * When true, loops don't yield to redraw.
195
+ * @param {boolean} turboModeOn Whether turbo mode should be set.
196
+ */
197
+ setTurboMode (turboModeOn) {
198
+ this.runtime.turboMode = !!turboModeOn;
199
+ if (this.runtime.turboMode) {
200
+ this.emit(Runtime.TURBO_MODE_ON);
201
+ } else {
202
+ this.emit(Runtime.TURBO_MODE_OFF);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Set whether the VM is in 2.0 "compatibility mode."
208
+ * When true, ticks go at 2.0 speed (30 TPS).
209
+ * @param {boolean} compatibilityModeOn Whether compatibility mode is set.
210
+ */
211
+ setCompatibilityMode (compatibilityModeOn) {
212
+ this.runtime.setCompatibilityMode(!!compatibilityModeOn);
213
+ }
214
+
215
+ /**
216
+ * Stop all threads and running activities.
217
+ */
218
+ stopAll () {
219
+ this.runtime.stopAll();
220
+ }
221
+
222
+ /**
223
+ * Clear out current running project data.
224
+ */
225
+ clear () {
226
+ this.runtime.dispose();
227
+ this.editingTarget = null;
228
+ this.emitTargetsUpdate(false /* Don't emit project change */);
229
+ }
230
+
231
+ /**
232
+ * Get data for playground. Data comes back in an emitted event.
233
+ */
234
+ getPlaygroundData () {
235
+ const instance = this;
236
+ // Only send back thread data for the current editingTarget.
237
+ const threadData = this.runtime.threads.filter(thread => thread.target === instance.editingTarget);
238
+ // Remove the target key, since it's a circular reference.
239
+ const filteredThreadData = JSON.stringify(threadData, (key, value) => {
240
+ if (key === 'target' || key === 'blockContainer') return;
241
+ return value;
242
+ }, 2);
243
+ this.emit('playgroundData', {
244
+ blocks: this.editingTarget.blocks,
245
+ threads: filteredThreadData
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Post I/O data to the virtual devices.
251
+ * @param {?string} device Name of virtual I/O device.
252
+ * @param {object} data Any data object to post to the I/O device.
253
+ */
254
+ postIOData (device, data) {
255
+ if (this.runtime.ioDevices[device]) {
256
+ this.runtime.ioDevices[device].postData(data);
257
+ }
258
+ }
259
+
260
+ setVideoProvider (videoProvider) {
261
+ this.runtime.ioDevices.video.setProvider(videoProvider);
262
+ }
263
+
264
+ setCloudProvider (cloudProvider) {
265
+ this.runtime.ioDevices.cloud.setProvider(cloudProvider);
266
+ }
267
+
268
+ /**
269
+ * Tell the specified extension to scan for a peripheral.
270
+ * @param {string} extensionId - the id of the extension.
271
+ */
272
+ scanForPeripheral (extensionId) {
273
+ this.runtime.scanForPeripheral(extensionId);
274
+ }
275
+
276
+ /**
277
+ * Connect to the extension's specified peripheral.
278
+ * @param {string} extensionId - the id of the extension.
279
+ * @param {number} peripheralId - the id of the peripheral.
280
+ */
281
+ connectPeripheral (extensionId, peripheralId) {
282
+ this.runtime.connectPeripheral(extensionId, peripheralId);
283
+ }
284
+
285
+ /**
286
+ * Disconnect from the extension's connected peripheral.
287
+ * @param {string} extensionId - the id of the extension.
288
+ */
289
+ disconnectPeripheral (extensionId) {
290
+ this.runtime.disconnectPeripheral(extensionId);
291
+ }
292
+
293
+ /**
294
+ * Returns whether the extension has a currently connected peripheral.
295
+ * @param {string} extensionId - the id of the extension.
296
+ * @return {boolean} - whether the extension has a connected peripheral.
297
+ */
298
+ getPeripheralIsConnected (extensionId) {
299
+ return this.runtime.getPeripheralIsConnected(extensionId);
300
+ }
301
+
302
+ /**
303
+ * Load a Scratch project from a .sb, .sb2, .sb3 or json string.
304
+ * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
305
+ * @return {!Promise} Promise that resolves after targets are installed.
306
+ */
307
+ loadProject (input) {
308
+ if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
309
+ !ArrayBuffer.isView(input)) {
310
+ // If the input is an object and not any ArrayBuffer
311
+ // or an ArrayBuffer view (this includes all typed arrays and DataViews)
312
+ // turn the object into a JSON string, because we suspect
313
+ // this is a project.json as an object
314
+ // validate expects a string or buffer as input
315
+ // TODO not sure if we need to check that it also isn't a data view
316
+ input = JSON.stringify(input);
317
+ }
318
+
319
+ const validationPromise = new Promise((resolve, reject) => {
320
+ const validate = require('scratch-parser');
321
+ // The second argument of false below indicates to the validator that the
322
+ // input should be parsed/validated as an entire project (and not a single sprite)
323
+ validate(input, false, (error, res) => {
324
+ if (error) return reject(error);
325
+ resolve(res);
326
+ });
327
+ })
328
+ .catch(error => {
329
+ const {SB1File, ValidationError} = require('scratch-sb1-converter');
330
+
331
+ try {
332
+ const sb1 = new SB1File(input);
333
+ const json = sb1.json;
334
+ json.projectVersion = 2;
335
+ return Promise.resolve([json, sb1.zip]);
336
+ } catch (sb1Error) {
337
+ if (sb1Error instanceof ValidationError) {
338
+ // The input does not validate as a Scratch 1 file.
339
+ } else {
340
+ // The project appears to be a Scratch 1 file but it
341
+ // could not be successfully translated into a Scratch 2
342
+ // project.
343
+ return Promise.reject(sb1Error);
344
+ }
345
+ }
346
+ // Throw original error since the input does not appear to be
347
+ // an SB1File.
348
+ return Promise.reject(error);
349
+ });
350
+
351
+ return validationPromise
352
+ .then(validatedInput => this.deserializeProject(validatedInput[0], validatedInput[1]))
353
+ .then(() => this.runtime.handleProjectLoaded())
354
+ .catch(error => {
355
+ // Intentionally rejecting here (want errors to be handled by caller)
356
+ if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
357
+ return Promise.reject(JSON.stringify(error));
358
+ }
359
+ return Promise.reject(error);
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Load a project from the Scratch web site, by ID.
365
+ * @param {string} id - the ID of the project to download, as a string.
366
+ */
367
+ downloadProjectId (id) {
368
+ const storage = this.runtime.storage;
369
+ if (!storage) {
370
+ log.error('No storage module present; cannot load project: ', id);
371
+ return;
372
+ }
373
+ const vm = this;
374
+ const promise = storage.load(storage.AssetType.Project, id);
375
+ promise.then(projectAsset => {
376
+ if (!projectAsset) {
377
+ log.error(`Failed to fetch project with id: ${id}`);
378
+ return null;
379
+ }
380
+ return vm.loadProject(projectAsset.data);
381
+ });
382
+ }
383
+
384
+ /**
385
+ * @returns {string} Project in a Scratch 3.0 JSON representation.
386
+ */
387
+ saveProjectSb3 () {
388
+ const soundDescs = serializeSounds(this.runtime);
389
+ const costumeDescs = serializeCostumes(this.runtime);
390
+ const projectJson = this.toJSON();
391
+
392
+ // TODO want to eventually move zip creation out of here, and perhaps
393
+ // into scratch-storage
394
+ const zip = new JSZip();
395
+
396
+ // Put everything in a zip file
397
+ zip.file('project.json', projectJson);
398
+ this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
399
+
400
+ return zip.generateAsync({
401
+ type: 'blob',
402
+ mimeType: 'application/x.scratch.sb3',
403
+ compression: 'DEFLATE',
404
+ compressionOptions: {
405
+ level: 6 // Tradeoff between best speed (1) and best compression (9)
406
+ }
407
+ });
408
+ }
409
+
410
+ /*
411
+ * @type {Array<object>} Array of all costumes and sounds currently in the runtime
412
+ */
413
+ get assets () {
414
+ return this.runtime.targets.reduce((acc, target) => (
415
+ acc
416
+ .concat(target.sprite.sounds.map(sound => sound.asset))
417
+ .concat(target.sprite.costumes.map(costume => costume.asset))
418
+ ), []);
419
+ }
420
+
421
+ _addFileDescsToZip (fileDescs, zip) {
422
+ for (let i = 0; i < fileDescs.length; i++) {
423
+ const currFileDesc = fileDescs[i];
424
+ zip.file(currFileDesc.fileName, currFileDesc.fileContent);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Exports a sprite in the sprite3 format.
430
+ * @param {string} targetId ID of the target to export
431
+ * @param {string=} optZipType Optional type that the resulting
432
+ * zip should be outputted in. Options are: base64, binarystring,
433
+ * array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to
434
+ * blob if argument not provided.
435
+ * See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option
436
+ * for more information about these options.
437
+ * @return {object} A generated zip of the sprite and its assets in the format
438
+ * specified by optZipType or blob by default.
439
+ */
440
+ exportSprite (targetId, optZipType) {
441
+ const soundDescs = serializeSounds(this.runtime, targetId);
442
+ const costumeDescs = serializeCostumes(this.runtime, targetId);
443
+ const spriteJson = this.toJSON(targetId);
444
+
445
+ const zip = new JSZip();
446
+ zip.file('sprite.json', spriteJson);
447
+ this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
448
+
449
+ return zip.generateAsync({
450
+ type: typeof optZipType === 'string' ? optZipType : 'blob',
451
+ mimeType: 'application/x.scratch.sprite3',
452
+ compression: 'DEFLATE',
453
+ compressionOptions: {
454
+ level: 6
455
+ }
456
+ });
457
+ }
458
+
459
+ /**
460
+ * Export project or sprite as a Scratch 3.0 JSON representation.
461
+ * @param {string=} optTargetId - Optional id of a sprite to serialize
462
+ * @return {string} Serialized state of the runtime.
463
+ */
464
+ toJSON (optTargetId) {
465
+ const sb3 = require('./serialization/sb3');
466
+ return StringUtil.stringify(sb3.serialize(this.runtime, optTargetId));
467
+ }
468
+
469
+ // TODO do we still need this function? Keeping it here so as not to introduce
470
+ // a breaking change.
471
+ /**
472
+ * Load a project from a Scratch JSON representation.
473
+ * @param {string} json JSON string representing a project.
474
+ * @returns {Promise} Promise that resolves after the project has loaded
475
+ */
476
+ fromJSON (json) {
477
+ log.warning('fromJSON is now just a wrapper around loadProject, please use that function instead.');
478
+ return this.loadProject(json);
479
+ }
480
+
481
+ /**
482
+ * Load a project from a Scratch JSON representation.
483
+ * @param {string} projectJSON JSON string representing a project.
484
+ * @param {?JSZip} zip Optional zipped project containing assets to be loaded.
485
+ * @returns {Promise} Promise that resolves after the project has loaded
486
+ */
487
+ deserializeProject (projectJSON, zip) {
488
+ // Clear the current runtime
489
+ this.clear();
490
+
491
+ if (typeof performance !== 'undefined') {
492
+ performance.mark('scratch-vm-deserialize-start');
493
+ }
494
+ const runtime = this.runtime;
495
+ const deserializePromise = function () {
496
+ const projectVersion = projectJSON.projectVersion;
497
+ if (projectVersion === 2) {
498
+ const sb2 = require('./serialization/sb2');
499
+ return sb2.deserialize(projectJSON, runtime, false, zip);
500
+ }
501
+ if (projectVersion === 3) {
502
+ const sb3 = require('./serialization/sb3');
503
+ return sb3.deserialize(projectJSON, runtime, zip);
504
+ }
505
+ // TODO: reject with an Error (possible breaking API change!)
506
+ // eslint-disable-next-line prefer-promise-reject-errors
507
+ return Promise.reject('Unable to verify Scratch Project version.');
508
+ };
509
+ return deserializePromise()
510
+ .then(({targets, extensions}) => {
511
+ if (typeof performance !== 'undefined') {
512
+ performance.mark('scratch-vm-deserialize-end');
513
+ performance.measure('scratch-vm-deserialize',
514
+ 'scratch-vm-deserialize-start', 'scratch-vm-deserialize-end');
515
+ }
516
+ return this.installTargets(targets, extensions, true);
517
+ });
518
+ }
519
+
520
+ /**
521
+ * Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets.
522
+ * @param {Array.<Target>} targets - the targets to be installed
523
+ * @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
524
+ * @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
525
+ * @returns {Promise} resolved once targets have been installed
526
+ */
527
+ installTargets (targets, extensions, wholeProject) {
528
+ const extensionPromises = [];
529
+
530
+ extensions.extensionIDs.forEach(extensionID => {
531
+ if (!this.extensionManager.isExtensionLoaded(extensionID)) {
532
+ const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
533
+ extensionPromises.push(this.extensionManager.loadExtensionURL(extensionURL));
534
+ }
535
+ });
536
+
537
+ targets = targets.filter(target => !!target);
538
+
539
+ return Promise.all(extensionPromises).then(() => {
540
+ targets.forEach(target => {
541
+ this.runtime.addTarget(target);
542
+ (/** @type RenderedTarget */ target).updateAllDrawableProperties();
543
+ // Ensure unique sprite name
544
+ if (target.isSprite()) this.renameSprite(target.id, target.getName());
545
+ });
546
+ // Sort the executable targets by layerOrder.
547
+ // Remove layerOrder property after use.
548
+ this.runtime.executableTargets.sort((a, b) => a.layerOrder - b.layerOrder);
549
+ targets.forEach(target => {
550
+ delete target.layerOrder;
551
+ });
552
+
553
+ // Select the first target for editing, e.g., the first sprite.
554
+ if (wholeProject && (targets.length > 1)) {
555
+ this.editingTarget = targets[1];
556
+ } else {
557
+ this.editingTarget = targets[0];
558
+ }
559
+
560
+ if (!wholeProject) {
561
+ this.editingTarget.fixUpVariableReferences();
562
+ }
563
+
564
+ // Update the VM user's knowledge of targets and blocks on the workspace.
565
+ this.emitTargetsUpdate(false /* Don't emit project change */);
566
+ this.emitWorkspaceUpdate();
567
+ this.runtime.setEditingTarget(this.editingTarget);
568
+ this.runtime.ioDevices.cloud.setStage(this.runtime.getTargetForStage());
569
+ });
570
+ }
571
+
572
+ /**
573
+ * Add a sprite, this could be .sprite2 or .sprite3. Unpack and validate
574
+ * such a file first.
575
+ * @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
576
+ * @return {!Promise} Promise that resolves after targets are installed.
577
+ */
578
+ addSprite (input) {
579
+ const errorPrefix = 'Sprite Upload Error:';
580
+ if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
581
+ !ArrayBuffer.isView(input)) {
582
+ // If the input is an object and not any ArrayBuffer
583
+ // or an ArrayBuffer view (this includes all typed arrays and DataViews)
584
+ // turn the object into a JSON string, because we suspect
585
+ // this is a project.json as an object
586
+ // validate expects a string or buffer as input
587
+ // TODO not sure if we need to check that it also isn't a data view
588
+ input = JSON.stringify(input);
589
+ }
590
+
591
+ const validationPromise = new Promise((resolve, reject) => {
592
+ const validate = require('scratch-parser');
593
+ // The second argument of true below indicates to the parser/validator
594
+ // that the given input should be treated as a single sprite and not
595
+ // an entire project
596
+ validate(input, true, (error, res) => {
597
+ if (error) return reject(error);
598
+ resolve(res);
599
+ });
600
+ });
601
+
602
+ return validationPromise
603
+ .then(validatedInput => {
604
+ const projectVersion = validatedInput[0].projectVersion;
605
+ if (projectVersion === 2) {
606
+ return this._addSprite2(validatedInput[0], validatedInput[1]);
607
+ }
608
+ if (projectVersion === 3) {
609
+ return this._addSprite3(validatedInput[0], validatedInput[1]);
610
+ }
611
+ // TODO: reject with an Error (possible breaking API change!)
612
+ // eslint-disable-next-line prefer-promise-reject-errors
613
+ return Promise.reject(`${errorPrefix} Unable to verify sprite version.`);
614
+ })
615
+ .then(() => this.runtime.emitProjectChanged())
616
+ .catch(error => {
617
+ // Intentionally rejecting here (want errors to be handled by caller)
618
+ if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
619
+ return Promise.reject(JSON.stringify(error));
620
+ }
621
+ // TODO: reject with an Error (possible breaking API change!)
622
+ // eslint-disable-next-line prefer-promise-reject-errors
623
+ return Promise.reject(`${errorPrefix} ${error}`);
624
+ });
625
+ }
626
+
627
+ /**
628
+ * Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
629
+ * @param {object} sprite Object representing 2.0 sprite to be added.
630
+ * @param {?ArrayBuffer} zip Optional zip of assets being referenced by json
631
+ * @returns {Promise} Promise that resolves after the sprite is added
632
+ */
633
+ _addSprite2 (sprite, zip) {
634
+ // Validate & parse
635
+
636
+ const sb2 = require('./serialization/sb2');
637
+ return sb2.deserialize(sprite, this.runtime, true, zip)
638
+ .then(({targets, extensions}) =>
639
+ this.installTargets(targets, extensions, false));
640
+ }
641
+
642
+ /**
643
+ * Add a single sb3 sprite.
644
+ * @param {object} sprite Object rperesenting 3.0 sprite to be added.
645
+ * @param {?ArrayBuffer} zip Optional zip of assets being referenced by target json
646
+ * @returns {Promise} Promise that resolves after the sprite is added
647
+ */
648
+ _addSprite3 (sprite, zip) {
649
+ // Validate & parse
650
+ const sb3 = require('./serialization/sb3');
651
+ return sb3
652
+ .deserialize(sprite, this.runtime, zip, true)
653
+ .then(({targets, extensions}) => this.installTargets(targets, extensions, false));
654
+ }
655
+
656
+ /**
657
+ * Add a costume to the current editing target.
658
+ * @param {string} md5ext - the MD5 and extension of the costume to be loaded.
659
+ * @param {!object} costumeObject Object representing the costume.
660
+ * @property {int} skinId - the ID of the costume's render skin, once installed.
661
+ * @property {number} rotationCenterX - the X component of the costume's origin.
662
+ * @property {number} rotationCenterY - the Y component of the costume's origin.
663
+ * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
664
+ * @param {string} optTargetId - the id of the target to add to, if not the editing target.
665
+ * @param {string} optVersion - if this is 2, load costume as sb2, otherwise load costume as sb3.
666
+ * @returns {?Promise} - a promise that resolves when the costume has been added
667
+ */
668
+ addCostume (md5ext, costumeObject, optTargetId, optVersion) {
669
+ const target = optTargetId ? this.runtime.getTargetById(optTargetId) :
670
+ this.editingTarget;
671
+ if (target) {
672
+ return loadCostume(md5ext, costumeObject, this.runtime, optVersion).then(() => {
673
+ target.addCostume(costumeObject);
674
+ target.setCostume(
675
+ target.getCostumes().length - 1
676
+ );
677
+ this.runtime.emitProjectChanged();
678
+ });
679
+ }
680
+ // If the target cannot be found by id, return a rejected promise
681
+ // TODO: reject with an Error (possible breaking API change!)
682
+ // eslint-disable-next-line prefer-promise-reject-errors
683
+ return Promise.reject();
684
+ }
685
+
686
+ /**
687
+ * Add a costume loaded from the library to the current editing target.
688
+ * @param {string} md5ext - the MD5 and extension of the costume to be loaded.
689
+ * @param {!object} costumeObject Object representing the costume.
690
+ * @property {int} skinId - the ID of the costume's render skin, once installed.
691
+ * @property {number} rotationCenterX - the X component of the costume's origin.
692
+ * @property {number} rotationCenterY - the Y component of the costume's origin.
693
+ * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
694
+ * @returns {?Promise} - a promise that resolves when the costume has been added
695
+ */
696
+ addCostumeFromLibrary (md5ext, costumeObject) {
697
+ // TODO: reject with an Error (possible breaking API change!)
698
+ // eslint-disable-next-line prefer-promise-reject-errors
699
+ if (!this.editingTarget) return Promise.reject();
700
+ return this.addCostume(md5ext, costumeObject, this.editingTarget.id, 2 /* optVersion */);
701
+ }
702
+
703
+ /**
704
+ * Duplicate the costume at the given index. Add it at that index + 1.
705
+ * @param {!int} costumeIndex Index of costume to duplicate
706
+ * @returns {?Promise} - a promise that resolves when the costume has been decoded and added
707
+ */
708
+ duplicateCostume (costumeIndex) {
709
+ const originalCostume = this.editingTarget.getCostumes()[costumeIndex];
710
+ const clone = Object.assign({}, originalCostume);
711
+ const md5ext = `${clone.assetId}.${clone.dataFormat}`;
712
+ return loadCostume(md5ext, clone, this.runtime).then(() => {
713
+ this.editingTarget.addCostume(clone, costumeIndex + 1);
714
+ this.editingTarget.setCostume(costumeIndex + 1);
715
+ this.emitTargetsUpdate();
716
+ });
717
+ }
718
+
719
+ /**
720
+ * Duplicate the sound at the given index. Add it at that index + 1.
721
+ * @param {!int} soundIndex Index of sound to duplicate
722
+ * @returns {?Promise} - a promise that resolves when the sound has been decoded and added
723
+ */
724
+ duplicateSound (soundIndex) {
725
+ const originalSound = this.editingTarget.getSounds()[soundIndex];
726
+ const clone = Object.assign({}, originalSound);
727
+ return loadSound(clone, this.runtime, this.editingTarget.sprite.soundBank).then(() => {
728
+ this.editingTarget.addSound(clone, soundIndex + 1);
729
+ this.emitTargetsUpdate();
730
+ });
731
+ }
732
+
733
+ /**
734
+ * Rename a costume on the current editing target.
735
+ * @param {int} costumeIndex - the index of the costume to be renamed.
736
+ * @param {string} newName - the desired new name of the costume (will be modified if already in use).
737
+ */
738
+ renameCostume (costumeIndex, newName) {
739
+ this.editingTarget.renameCostume(costumeIndex, newName);
740
+ this.emitTargetsUpdate();
741
+ }
742
+
743
+ /**
744
+ * Delete a costume from the current editing target.
745
+ * @param {int} costumeIndex - the index of the costume to be removed.
746
+ * @return {?function} A function to restore the deleted costume, or null,
747
+ * if no costume was deleted.
748
+ */
749
+ deleteCostume (costumeIndex) {
750
+ const deletedCostume = this.editingTarget.deleteCostume(costumeIndex);
751
+ if (deletedCostume) {
752
+ const target = this.editingTarget;
753
+ this.runtime.emitProjectChanged();
754
+ return () => {
755
+ target.addCostume(deletedCostume);
756
+ this.emitTargetsUpdate();
757
+ };
758
+ }
759
+ return null;
760
+ }
761
+
762
+ /**
763
+ * Add a sound to the current editing target.
764
+ * @param {!object} soundObject Object representing the costume.
765
+ * @param {string} optTargetId - the id of the target to add to, if not the editing target.
766
+ * @returns {?Promise} - a promise that resolves when the sound has been decoded and added
767
+ */
768
+ addSound (soundObject, optTargetId) {
769
+ const target = optTargetId ? this.runtime.getTargetById(optTargetId) :
770
+ this.editingTarget;
771
+ if (target) {
772
+ return loadSound(soundObject, this.runtime, target.sprite.soundBank).then(() => {
773
+ target.addSound(soundObject);
774
+ this.emitTargetsUpdate();
775
+ });
776
+ }
777
+ // If the target cannot be found by id, return a rejected promise
778
+ return new Promise.reject();
779
+ }
780
+
781
+ /**
782
+ * Rename a sound on the current editing target.
783
+ * @param {int} soundIndex - the index of the sound to be renamed.
784
+ * @param {string} newName - the desired new name of the sound (will be modified if already in use).
785
+ */
786
+ renameSound (soundIndex, newName) {
787
+ this.editingTarget.renameSound(soundIndex, newName);
788
+ this.emitTargetsUpdate();
789
+ }
790
+
791
+ /**
792
+ * Get a sound buffer from the audio engine.
793
+ * @param {int} soundIndex - the index of the sound to be got.
794
+ * @return {AudioBuffer} the sound's audio buffer.
795
+ */
796
+ getSoundBuffer (soundIndex) {
797
+ const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
798
+ if (id && this.runtime && this.runtime.audioEngine) {
799
+ return this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer;
800
+ }
801
+ return null;
802
+ }
803
+
804
+ /**
805
+ * Update a sound buffer.
806
+ * @param {int} soundIndex - the index of the sound to be updated.
807
+ * @param {AudioBuffer} newBuffer - new audio buffer for the audio engine.
808
+ * @param {ArrayBuffer} soundEncoding - the new (wav) encoded sound to be stored
809
+ */
810
+ updateSoundBuffer (soundIndex, newBuffer, soundEncoding) {
811
+ const sound = this.editingTarget.sprite.sounds[soundIndex];
812
+ if (sound && sound.broken) delete sound.broken;
813
+ const id = sound ? sound.soundId : null;
814
+ if (id && this.runtime && this.runtime.audioEngine) {
815
+ this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer = newBuffer;
816
+ }
817
+ // Update sound in runtime
818
+ if (soundEncoding) {
819
+ // Now that we updated the sound, the format should also be updated
820
+ // so that the sound can eventually be decoded the right way.
821
+ // Sounds that were formerly 'adpcm', but were updated in sound editor
822
+ // will not get decoded by the audio engine correctly unless the format
823
+ // is updated as below.
824
+ sound.format = '';
825
+ const storage = this.runtime.storage;
826
+ sound.asset = storage.createAsset(
827
+ storage.AssetType.Sound,
828
+ storage.DataFormat.WAV,
829
+ soundEncoding,
830
+ null,
831
+ true // generate md5
832
+ );
833
+ sound.assetId = sound.asset.assetId;
834
+ sound.dataFormat = storage.DataFormat.WAV;
835
+ sound.md5 = `${sound.assetId}.${sound.dataFormat}`;
836
+ sound.sampleCount = newBuffer.length;
837
+ sound.rate = newBuffer.sampleRate;
838
+ }
839
+ // If soundEncoding is null, it's because gui had a problem
840
+ // encoding the updated sound. We don't want to store anything in this
841
+ // case, and gui should have logged an error.
842
+
843
+ this.emitTargetsUpdate();
844
+ }
845
+
846
+ /**
847
+ * Delete a sound from the current editing target.
848
+ * @param {int} soundIndex - the index of the sound to be removed.
849
+ * @return {?Function} A function to restore the sound that was deleted,
850
+ * or null, if no sound was deleted.
851
+ */
852
+ deleteSound (soundIndex) {
853
+ const target = this.editingTarget;
854
+ const deletedSound = this.editingTarget.deleteSound(soundIndex);
855
+ if (deletedSound) {
856
+ this.runtime.emitProjectChanged();
857
+ const restoreFun = () => {
858
+ target.addSound(deletedSound);
859
+ this.emitTargetsUpdate();
860
+ };
861
+ return restoreFun;
862
+ }
863
+ return null;
864
+ }
865
+
866
+ /**
867
+ * Get a string representation of the image from storage.
868
+ * @param {int} costumeIndex - the index of the costume to be got.
869
+ * @return {string} the costume's SVG string if it's SVG,
870
+ * a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded.
871
+ */
872
+ getCostume (costumeIndex) {
873
+ const asset = this.editingTarget.getCostumes()[costumeIndex].asset;
874
+ if (!asset || !this.runtime || !this.runtime.storage) return null;
875
+ const format = asset.dataFormat;
876
+ if (format === this.runtime.storage.DataFormat.SVG) {
877
+ return asset.decodeText();
878
+ } else if (format === this.runtime.storage.DataFormat.PNG ||
879
+ format === this.runtime.storage.DataFormat.JPG) {
880
+ return asset.encodeDataURI();
881
+ }
882
+ log.error(`Unhandled format: ${asset.dataFormat}`);
883
+ return null;
884
+ }
885
+
886
+ /**
887
+ * Update a costume with the given bitmap
888
+ * @param {!int} costumeIndex - the index of the costume to be updated.
889
+ * @param {!ImageData} bitmap - new bitmap for the renderer.
890
+ * @param {!number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner
891
+ * @param {!number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner
892
+ * @param {!number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage,
893
+ * 2 for double-resolution bitmaps
894
+ */
895
+ updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) {
896
+ const costume = this.editingTarget.getCostumes()[costumeIndex];
897
+ if (!(costume && this.runtime && this.runtime.renderer)) return;
898
+ if (costume && costume.broken) delete costume.broken;
899
+
900
+ costume.rotationCenterX = rotationCenterX;
901
+ costume.rotationCenterY = rotationCenterY;
902
+
903
+ // If the bitmap originally had a zero width or height, use that value
904
+ const bitmapWidth = bitmap.sourceWidth === 0 ? 0 : bitmap.width;
905
+ const bitmapHeight = bitmap.sourceHeight === 0 ? 0 : bitmap.height;
906
+ // @todo: updateBitmapSkin does not take ImageData
907
+ const canvas = document.createElement('canvas');
908
+ canvas.width = bitmapWidth;
909
+ canvas.height = bitmapHeight;
910
+ const context = canvas.getContext('2d');
911
+ context.putImageData(bitmap, 0, 0);
912
+
913
+ // Divide by resolution because the renderer's definition of the rotation center
914
+ // is the rotation center divided by the bitmap resolution
915
+ this.runtime.renderer.updateBitmapSkin(
916
+ costume.skinId,
917
+ canvas,
918
+ bitmapResolution,
919
+ [rotationCenterX / bitmapResolution, rotationCenterY / bitmapResolution]
920
+ );
921
+
922
+ // @todo there should be a better way to get from ImageData to a decodable storage format
923
+ canvas.toBlob(blob => {
924
+ const reader = new FileReader();
925
+ reader.addEventListener('loadend', () => {
926
+ const storage = this.runtime.storage;
927
+ costume.dataFormat = storage.DataFormat.PNG;
928
+ costume.bitmapResolution = bitmapResolution;
929
+ costume.size = [bitmapWidth, bitmapHeight];
930
+ costume.asset = storage.createAsset(
931
+ storage.AssetType.ImageBitmap,
932
+ costume.dataFormat,
933
+ Buffer.from(reader.result),
934
+ null, // id
935
+ true // generate md5
936
+ );
937
+ costume.assetId = costume.asset.assetId;
938
+ costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
939
+ this.emitTargetsUpdate();
940
+ });
941
+ // Bitmaps with a zero width or height return null for their blob
942
+ if (blob){
943
+ reader.readAsArrayBuffer(blob);
944
+ }
945
+ });
946
+ }
947
+
948
+ /**
949
+ * Update a costume with the given SVG
950
+ * @param {int} costumeIndex - the index of the costume to be updated.
951
+ * @param {string} svg - new SVG for the renderer.
952
+ * @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner
953
+ * @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner
954
+ */
955
+ updateSvg (costumeIndex, svg, rotationCenterX, rotationCenterY) {
956
+ const costume = this.editingTarget.getCostumes()[costumeIndex];
957
+ if (costume && costume.broken) delete costume.broken;
958
+ if (costume && this.runtime && this.runtime.renderer) {
959
+ costume.rotationCenterX = rotationCenterX;
960
+ costume.rotationCenterY = rotationCenterY;
961
+ this.runtime.renderer.updateSVGSkin(costume.skinId, svg, [rotationCenterX, rotationCenterY]);
962
+ costume.size = this.runtime.renderer.getSkinSize(costume.skinId);
963
+ }
964
+ const storage = this.runtime.storage;
965
+ // If we're in here, we've edited an svg in the vector editor,
966
+ // so the dataFormat should be 'svg'
967
+ costume.dataFormat = storage.DataFormat.SVG;
968
+ costume.bitmapResolution = 1;
969
+ costume.asset = storage.createAsset(
970
+ storage.AssetType.ImageVector,
971
+ costume.dataFormat,
972
+ (new _TextEncoder()).encode(svg),
973
+ null,
974
+ true // generate md5
975
+ );
976
+ costume.assetId = costume.asset.assetId;
977
+ costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
978
+ this.emitTargetsUpdate();
979
+ }
980
+
981
+ /**
982
+ * Add a backdrop to the stage.
983
+ * @param {string} md5ext - the MD5 and extension of the backdrop to be loaded.
984
+ * @param {!object} backdropObject Object representing the backdrop.
985
+ * @property {int} skinId - the ID of the backdrop's render skin, once installed.
986
+ * @property {number} rotationCenterX - the X component of the backdrop's origin.
987
+ * @property {number} rotationCenterY - the Y component of the backdrop's origin.
988
+ * @property {number} [bitmapResolution] - the resolution scale for a bitmap backdrop.
989
+ * @returns {?Promise} - a promise that resolves when the backdrop has been added
990
+ */
991
+ addBackdrop (md5ext, backdropObject) {
992
+ return loadCostume(md5ext, backdropObject, this.runtime).then(() => {
993
+ const stage = this.runtime.getTargetForStage();
994
+ stage.addCostume(backdropObject);
995
+ stage.setCostume(stage.getCostumes().length - 1);
996
+ this.runtime.emitProjectChanged();
997
+ });
998
+ }
999
+
1000
+ /**
1001
+ * Rename a sprite.
1002
+ * @param {string} targetId ID of a target whose sprite to rename.
1003
+ * @param {string} newName New name of the sprite.
1004
+ */
1005
+ renameSprite (targetId, newName) {
1006
+ const target = this.runtime.getTargetById(targetId);
1007
+ if (target) {
1008
+ if (!target.isSprite()) {
1009
+ throw new Error('Cannot rename non-sprite targets.');
1010
+ }
1011
+ const sprite = target.sprite;
1012
+ if (!sprite) {
1013
+ throw new Error('No sprite associated with this target.');
1014
+ }
1015
+ if (newName && RESERVED_NAMES.indexOf(newName) === -1) {
1016
+ const names = this.runtime.targets
1017
+ .filter(runtimeTarget => runtimeTarget.isSprite() && runtimeTarget.id !== target.id)
1018
+ .map(runtimeTarget => runtimeTarget.sprite.name);
1019
+ const oldName = sprite.name;
1020
+ const newUnusedName = StringUtil.unusedName(newName, names);
1021
+ sprite.name = newUnusedName;
1022
+ const allTargets = this.runtime.targets;
1023
+ for (let i = 0; i < allTargets.length; i++) {
1024
+ const currTarget = allTargets[i];
1025
+ currTarget.blocks.updateAssetName(oldName, newName, 'sprite');
1026
+ }
1027
+
1028
+ if (newUnusedName !== oldName) this.emitTargetsUpdate();
1029
+ }
1030
+ } else {
1031
+ throw new Error('No target with the provided id.');
1032
+ }
1033
+ }
1034
+
1035
+ /**
1036
+ * Delete a sprite and all its clones.
1037
+ * @param {string} targetId ID of a target whose sprite to delete.
1038
+ * @return {Function} Returns a function to restore the sprite that was deleted
1039
+ */
1040
+ deleteSprite (targetId) {
1041
+ const target = this.runtime.getTargetById(targetId);
1042
+
1043
+ if (target) {
1044
+ const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
1045
+ if (!target.isSprite()) {
1046
+ throw new Error('Cannot delete non-sprite targets.');
1047
+ }
1048
+ const sprite = target.sprite;
1049
+ if (!sprite) {
1050
+ throw new Error('No sprite associated with this target.');
1051
+ }
1052
+ const spritePromise = this.exportSprite(targetId, 'uint8array');
1053
+ const restoreSprite = () => spritePromise.then(spriteBuffer => this.addSprite(spriteBuffer));
1054
+ // Remove monitors from the runtime state and remove the
1055
+ // target-specific monitored blocks (e.g. local variables)
1056
+ target.deleteMonitors();
1057
+ const currentEditingTarget = this.editingTarget;
1058
+ for (let i = 0; i < sprite.clones.length; i++) {
1059
+ const clone = sprite.clones[i];
1060
+ this.runtime.stopForTarget(sprite.clones[i]);
1061
+ this.runtime.disposeTarget(sprite.clones[i]);
1062
+ // Ensure editing target is switched if we are deleting it.
1063
+ if (clone === currentEditingTarget) {
1064
+ const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete);
1065
+ if (this.runtime.targets.length > 0){
1066
+ this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
1067
+ } else {
1068
+ this.editingTarget = null;
1069
+ }
1070
+ }
1071
+ }
1072
+ // Sprite object should be deleted by GC.
1073
+ this.emitTargetsUpdate();
1074
+ return restoreSprite;
1075
+ }
1076
+
1077
+ throw new Error('No target with the provided id.');
1078
+ }
1079
+
1080
+ /**
1081
+ * Duplicate a sprite.
1082
+ * @param {string} targetId ID of a target whose sprite to duplicate.
1083
+ * @returns {Promise} Promise that resolves when duplicated target has
1084
+ * been added to the runtime.
1085
+ */
1086
+ duplicateSprite (targetId) {
1087
+ const target = this.runtime.getTargetById(targetId);
1088
+ if (!target) {
1089
+ throw new Error('No target with the provided id.');
1090
+ } else if (!target.isSprite()) {
1091
+ throw new Error('Cannot duplicate non-sprite targets.');
1092
+ } else if (!target.sprite) {
1093
+ throw new Error('No sprite associated with this target.');
1094
+ }
1095
+ return target.duplicate().then(newTarget => {
1096
+ this.runtime.addTarget(newTarget);
1097
+ newTarget.goBehindOther(target);
1098
+ this.setEditingTarget(newTarget.id);
1099
+ });
1100
+ }
1101
+
1102
+ /**
1103
+ * Set the audio engine for the VM/runtime
1104
+ * @param {!AudioEngine} audioEngine The audio engine to attach
1105
+ */
1106
+ attachAudioEngine (audioEngine) {
1107
+ this.runtime.attachAudioEngine(audioEngine);
1108
+ }
1109
+
1110
+ /**
1111
+ * Set the renderer for the VM/runtime
1112
+ * @param {!RenderWebGL} renderer The renderer to attach
1113
+ */
1114
+ attachRenderer (renderer) {
1115
+ this.runtime.attachRenderer(renderer);
1116
+ }
1117
+
1118
+ /**
1119
+ * @returns {RenderWebGL} The renderer attached to the vm
1120
+ */
1121
+ get renderer () {
1122
+ return this.runtime && this.runtime.renderer;
1123
+ }
1124
+
1125
+ // @deprecated
1126
+ attachV2SVGAdapter () {
1127
+ }
1128
+
1129
+ /**
1130
+ * Set the bitmap adapter for the VM/runtime, which converts scratch 2
1131
+ * bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2)
1132
+ * @param {!function} bitmapAdapter The adapter to attach
1133
+ */
1134
+ attachV2BitmapAdapter (bitmapAdapter) {
1135
+ this.runtime.attachV2BitmapAdapter(bitmapAdapter);
1136
+ }
1137
+
1138
+ /**
1139
+ * Set the storage module for the VM/runtime
1140
+ * @param {!ScratchStorage} storage The storage module to attach
1141
+ */
1142
+ attachStorage (storage) {
1143
+ this.runtime.attachStorage(storage);
1144
+ }
1145
+
1146
+ /**
1147
+ * set the current locale and builtin messages for the VM
1148
+ * @param {!string} locale current locale
1149
+ * @param {!object} messages builtin messages map for current locale
1150
+ * @returns {Promise} Promise that resolves when all the blocks have been
1151
+ * updated for a new locale (or empty if locale hasn't changed.)
1152
+ */
1153
+ setLocale (locale, messages) {
1154
+ if (locale !== formatMessage.setup().locale) {
1155
+ formatMessage.setup({locale: locale, translations: {[locale]: messages}});
1156
+ }
1157
+ return this.extensionManager.refreshBlocks();
1158
+ }
1159
+
1160
+ /**
1161
+ * get the current locale for the VM
1162
+ * @returns {string} the current locale in the VM
1163
+ */
1164
+ getLocale () {
1165
+ return formatMessage.setup().locale;
1166
+ }
1167
+
1168
+ /**
1169
+ * Handle a Blockly event for the current editing target.
1170
+ * @param {!Blockly.Event} e Any Blockly event.
1171
+ */
1172
+ blockListener (e) {
1173
+ if (this.editingTarget) {
1174
+ this.editingTarget.blocks.blocklyListen(e);
1175
+ }
1176
+ }
1177
+
1178
+ /**
1179
+ * Handle a Blockly event for the flyout.
1180
+ * @param {!Blockly.Event} e Any Blockly event.
1181
+ */
1182
+ flyoutBlockListener (e) {
1183
+ this.runtime.flyoutBlocks.blocklyListen(e);
1184
+ }
1185
+
1186
+ /**
1187
+ * Handle a Blockly event for the flyout to be passed to the monitor container.
1188
+ * @param {!Blockly.Event} e Any Blockly event.
1189
+ */
1190
+ monitorBlockListener (e) {
1191
+ // Filter events by type, since monitor blocks only need to listen to these events.
1192
+ // Monitor blocks shouldn't be destroyed when flyout blocks are deleted.
1193
+ if (['create', 'change'].indexOf(e.type) !== -1) {
1194
+ this.runtime.monitorBlocks.blocklyListen(e);
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * Handle a Blockly event for the variable map.
1200
+ * @param {!Blockly.Event} e Any Blockly event.
1201
+ */
1202
+ variableListener (e) {
1203
+ // Filter events by type, since blocks only needs to listen to these
1204
+ // var events.
1205
+ if (['var_create', 'var_rename', 'var_delete'].indexOf(e.type) !== -1) {
1206
+ this.runtime.getTargetForStage().blocks.blocklyListen(e);
1207
+ }
1208
+ }
1209
+
1210
+ /**
1211
+ * Delete all of the flyout blocks.
1212
+ */
1213
+ clearFlyoutBlocks () {
1214
+ this.runtime.flyoutBlocks.deleteAllBlocks();
1215
+ }
1216
+
1217
+ /**
1218
+ * Set an editing target. An editor UI can use this function to switch
1219
+ * between editing different targets, sprites, etc.
1220
+ * After switching the editing target, the VM may emit updates
1221
+ * to the list of targets and any attached workspace blocks
1222
+ * (see `emitTargetsUpdate` and `emitWorkspaceUpdate`).
1223
+ * @param {string} targetId Id of target to set as editing.
1224
+ */
1225
+ setEditingTarget (targetId) {
1226
+ // Has the target id changed? If not, exit.
1227
+ if (this.editingTarget && targetId === this.editingTarget.id) {
1228
+ return;
1229
+ }
1230
+ const target = this.runtime.getTargetById(targetId);
1231
+ if (target) {
1232
+ this.editingTarget = target;
1233
+ // Emit appropriate UI updates.
1234
+ this.emitTargetsUpdate(false /* Don't emit project change */);
1235
+ this.emitWorkspaceUpdate();
1236
+ this.runtime.setEditingTarget(target);
1237
+ }
1238
+ }
1239
+
1240
+ /**
1241
+ * Called when blocks are dragged from one sprite to another. Adds the blocks to the
1242
+ * workspace of the given target.
1243
+ * @param {!Array<object>} blocks Blocks to add.
1244
+ * @param {!string} targetId Id of target to add blocks to.
1245
+ * @param {?string} optFromTargetId Optional target id indicating that blocks are being
1246
+ * shared from that target. This is needed for resolving any potential variable conflicts.
1247
+ * @return {!Promise} Promise that resolves when the extensions and blocks have been added.
1248
+ */
1249
+ shareBlocksToTarget (blocks, targetId, optFromTargetId) {
1250
+ const sb3 = require('./serialization/sb3');
1251
+
1252
+ const copiedBlocks = JSON.parse(JSON.stringify(blocks));
1253
+ newBlockIds(copiedBlocks);
1254
+ const target = this.runtime.getTargetById(targetId);
1255
+
1256
+ if (optFromTargetId) {
1257
+ // If the blocks are being shared from another target,
1258
+ // resolve any possible variable conflicts that may arise.
1259
+ const fromTarget = this.runtime.getTargetById(optFromTargetId);
1260
+ fromTarget.resolveVariableSharingConflictsWithTarget(copiedBlocks, target);
1261
+ }
1262
+
1263
+ // Create a unique set of extensionIds that are not yet loaded
1264
+ const extensionIDs = new Set(copiedBlocks
1265
+ .map(b => sb3.getExtensionIdForOpcode(b.opcode))
1266
+ .filter(id => !!id) // Remove ids that do not exist
1267
+ .filter(id => !this.extensionManager.isExtensionLoaded(id)) // and remove loaded extensions
1268
+ );
1269
+
1270
+ // Create an array promises for extensions to load
1271
+ const extensionPromises = Array.from(extensionIDs,
1272
+ id => this.extensionManager.loadExtensionURL(id)
1273
+ );
1274
+
1275
+ return Promise.all(extensionPromises).then(() => {
1276
+ copiedBlocks.forEach(block => {
1277
+ target.blocks.createBlock(block);
1278
+ });
1279
+ target.blocks.updateTargetSpecificBlocks(target.isStage);
1280
+ });
1281
+ }
1282
+
1283
+ /**
1284
+ * Called when costumes are dragged from editing target to another target.
1285
+ * Sets the newly added costume as the current costume.
1286
+ * @param {!number} costumeIndex Index of the costume of the editing target to share.
1287
+ * @param {!string} targetId Id of target to add the costume.
1288
+ * @return {Promise} Promise that resolves when the new costume has been loaded.
1289
+ */
1290
+ shareCostumeToTarget (costumeIndex, targetId) {
1291
+ const originalCostume = this.editingTarget.getCostumes()[costumeIndex];
1292
+ const clone = Object.assign({}, originalCostume);
1293
+ const md5ext = `${clone.assetId}.${clone.dataFormat}`;
1294
+ return loadCostume(md5ext, clone, this.runtime).then(() => {
1295
+ const target = this.runtime.getTargetById(targetId);
1296
+ if (target) {
1297
+ target.addCostume(clone);
1298
+ target.setCostume(
1299
+ target.getCostumes().length - 1
1300
+ );
1301
+ }
1302
+ });
1303
+ }
1304
+
1305
+ /**
1306
+ * Called when sounds are dragged from editing target to another target.
1307
+ * @param {!number} soundIndex Index of the sound of the editing target to share.
1308
+ * @param {!string} targetId Id of target to add the sound.
1309
+ * @return {Promise} Promise that resolves when the new sound has been loaded.
1310
+ */
1311
+ shareSoundToTarget (soundIndex, targetId) {
1312
+ const originalSound = this.editingTarget.getSounds()[soundIndex];
1313
+ const clone = Object.assign({}, originalSound);
1314
+ const target = this.runtime.getTargetById(targetId);
1315
+ return loadSound(clone, this.runtime, target.sprite.soundBank).then(() => {
1316
+ if (target) {
1317
+ target.addSound(clone);
1318
+ this.emitTargetsUpdate();
1319
+ }
1320
+ });
1321
+ }
1322
+
1323
+ /**
1324
+ * Repopulate the workspace with the blocks of the current editingTarget. This
1325
+ * allows us to get around bugs like gui#413.
1326
+ */
1327
+ refreshWorkspace () {
1328
+ if (this.editingTarget) {
1329
+ this.emitWorkspaceUpdate();
1330
+ this.runtime.setEditingTarget(this.editingTarget);
1331
+ this.emitTargetsUpdate(false /* Don't emit project change */);
1332
+ }
1333
+ }
1334
+
1335
+ /**
1336
+ * Emit metadata about available targets.
1337
+ * An editor UI could use this to display a list of targets and show
1338
+ * the currently editing one.
1339
+ * @param {bool} triggerProjectChange If true, also emit a project changed event.
1340
+ * Disabled selectively by updates that don't affect project serialization.
1341
+ * Defaults to true.
1342
+ */
1343
+ emitTargetsUpdate (triggerProjectChange) {
1344
+ if (typeof triggerProjectChange === 'undefined') triggerProjectChange = true;
1345
+ this.emit('targetsUpdate', {
1346
+ // [[target id, human readable target name], ...].
1347
+ targetList: this.runtime.targets
1348
+ .filter(
1349
+ // Don't report clones.
1350
+ target => !Object.prototype.hasOwnProperty.call(target, 'isOriginal') || target.isOriginal
1351
+ ).map(
1352
+ target => target.toJSON()
1353
+ ),
1354
+ // Currently editing target id.
1355
+ editingTarget: this.editingTarget ? this.editingTarget.id : null
1356
+ });
1357
+ if (triggerProjectChange) {
1358
+ this.runtime.emitProjectChanged();
1359
+ }
1360
+ }
1361
+
1362
+ /**
1363
+ * Emit an Blockly/scratch-blocks compatible XML representation
1364
+ * of the current editing target's blocks.
1365
+ */
1366
+ emitWorkspaceUpdate () {
1367
+ // Create a list of broadcast message Ids according to the stage variables
1368
+ const stageVariables = this.runtime.getTargetForStage().variables;
1369
+ let messageIds = [];
1370
+ for (const varId in stageVariables) {
1371
+ if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) {
1372
+ messageIds.push(varId);
1373
+ }
1374
+ }
1375
+ // Go through all blocks on all targets, removing referenced
1376
+ // broadcast ids from the list.
1377
+ for (let i = 0; i < this.runtime.targets.length; i++) {
1378
+ const currTarget = this.runtime.targets[i];
1379
+ const currBlocks = currTarget.blocks._blocks;
1380
+ for (const blockId in currBlocks) {
1381
+ if (currBlocks[blockId].fields.BROADCAST_OPTION) {
1382
+ const id = currBlocks[blockId].fields.BROADCAST_OPTION.id;
1383
+ const index = messageIds.indexOf(id);
1384
+ if (index !== -1) {
1385
+ messageIds = messageIds.slice(0, index)
1386
+ .concat(messageIds.slice(index + 1));
1387
+ }
1388
+ }
1389
+ }
1390
+ }
1391
+ // Anything left in messageIds is not referenced by a block, so delete it.
1392
+ for (let i = 0; i < messageIds.length; i++) {
1393
+ const id = messageIds[i];
1394
+ delete this.runtime.getTargetForStage().variables[id];
1395
+ }
1396
+ const globalVarMap = Object.assign({}, this.runtime.getTargetForStage().variables);
1397
+ const localVarMap = this.editingTarget.isStage ?
1398
+ Object.create(null) :
1399
+ Object.assign({}, this.editingTarget.variables);
1400
+
1401
+ const globalVariables = Object.keys(globalVarMap).map(k => globalVarMap[k]);
1402
+ const localVariables = Object.keys(localVarMap).map(k => localVarMap[k]);
1403
+ const workspaceComments = Object.keys(this.editingTarget.comments)
1404
+ .map(k => this.editingTarget.comments[k])
1405
+ .filter(c => c.blockId === null);
1406
+
1407
+ const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
1408
+ <variables>
1409
+ ${globalVariables.map(v => v.toXML()).join()}
1410
+ ${localVariables.map(v => v.toXML(true)).join()}
1411
+ </variables>
1412
+ ${workspaceComments.map(c => c.toXML()).join()}
1413
+ ${this.editingTarget.blocks.toXML(this.editingTarget.comments)}
1414
+ </xml>`;
1415
+
1416
+ this.emit('workspaceUpdate', {xml: xmlString});
1417
+ }
1418
+
1419
+ /**
1420
+ * Get a target id for a drawable id. Useful for interacting with the renderer
1421
+ * @param {int} drawableId The drawable id to request the target id for
1422
+ * @returns {?string} The target id, if found. Will also be null if the target found is the stage.
1423
+ */
1424
+ getTargetIdForDrawableId (drawableId) {
1425
+ const target = this.runtime.getTargetByDrawableId(drawableId);
1426
+ if (target &&
1427
+ Object.prototype.hasOwnProperty.call(target, 'id') &&
1428
+ Object.prototype.hasOwnProperty.call(target, 'isStage') &&
1429
+ !target.isStage) {
1430
+ return target.id;
1431
+ }
1432
+ return null;
1433
+ }
1434
+
1435
+ /**
1436
+ * Reorder target by index. Return whether a change was made.
1437
+ * @param {!string} targetIndex Index of the target.
1438
+ * @param {!number} newIndex index that the target should be moved to.
1439
+ * @returns {boolean} Whether a target was reordered.
1440
+ */
1441
+ reorderTarget (targetIndex, newIndex) {
1442
+ let targets = this.runtime.targets;
1443
+ targetIndex = MathUtil.clamp(targetIndex, 0, targets.length - 1);
1444
+ newIndex = MathUtil.clamp(newIndex, 0, targets.length - 1);
1445
+ if (targetIndex === newIndex) return false;
1446
+ const target = targets[targetIndex];
1447
+ targets = targets.slice(0, targetIndex).concat(targets.slice(targetIndex + 1));
1448
+ targets.splice(newIndex, 0, target);
1449
+ this.runtime.targets = targets;
1450
+ this.emitTargetsUpdate();
1451
+ return true;
1452
+ }
1453
+
1454
+ /**
1455
+ * Reorder the costumes of a target if it exists. Return whether it succeeded.
1456
+ * @param {!string} targetId ID of the target which owns the costumes.
1457
+ * @param {!number} costumeIndex index of the costume to move.
1458
+ * @param {!number} newIndex index that the costume should be moved to.
1459
+ * @returns {boolean} Whether a costume was reordered.
1460
+ */
1461
+ reorderCostume (targetId, costumeIndex, newIndex) {
1462
+ const target = this.runtime.getTargetById(targetId);
1463
+ if (target) {
1464
+ const reorderSuccessful = target.reorderCostume(costumeIndex, newIndex);
1465
+ if (reorderSuccessful) {
1466
+ this.runtime.emitProjectChanged();
1467
+ }
1468
+ return reorderSuccessful;
1469
+ }
1470
+ return false;
1471
+ }
1472
+
1473
+ /**
1474
+ * Reorder the sounds of a target if it exists. Return whether it occured.
1475
+ * @param {!string} targetId ID of the target which owns the sounds.
1476
+ * @param {!number} soundIndex index of the sound to move.
1477
+ * @param {!number} newIndex index that the sound should be moved to.
1478
+ * @returns {boolean} Whether a sound was reordered.
1479
+ */
1480
+ reorderSound (targetId, soundIndex, newIndex) {
1481
+ const target = this.runtime.getTargetById(targetId);
1482
+ if (target) {
1483
+ const reorderSuccessful = target.reorderSound(soundIndex, newIndex);
1484
+ if (reorderSuccessful) {
1485
+ this.runtime.emitProjectChanged();
1486
+ }
1487
+ return reorderSuccessful;
1488
+ }
1489
+ return false;
1490
+ }
1491
+
1492
+ /**
1493
+ * Put a target into a "drag" state, during which its X/Y positions will be unaffected
1494
+ * by blocks.
1495
+ * @param {string} targetId The id for the target to put into a drag state
1496
+ */
1497
+ startDrag (targetId) {
1498
+ const target = this.runtime.getTargetById(targetId);
1499
+ if (target) {
1500
+ this._dragTarget = target;
1501
+ target.startDrag();
1502
+ }
1503
+ }
1504
+
1505
+ /**
1506
+ * Remove a target from a drag state, so blocks may begin affecting X/Y position again
1507
+ * @param {string} targetId The id for the target to remove from the drag state
1508
+ */
1509
+ stopDrag (targetId) {
1510
+ const target = this.runtime.getTargetById(targetId);
1511
+ if (target) {
1512
+ this._dragTarget = null;
1513
+ target.stopDrag();
1514
+ this.setEditingTarget(target.sprite && target.sprite.clones[0] ?
1515
+ target.sprite.clones[0].id : target.id);
1516
+ }
1517
+ }
1518
+
1519
+ /**
1520
+ * Post/edit sprite info for the current editing target or the drag target.
1521
+ * @param {object} data An object with sprite info data to set.
1522
+ */
1523
+ postSpriteInfo (data) {
1524
+ if (this._dragTarget) {
1525
+ this._dragTarget.postSpriteInfo(data);
1526
+ } else {
1527
+ this.editingTarget.postSpriteInfo(data);
1528
+ }
1529
+ // Post sprite info means the gui has changed something about a sprite,
1530
+ // either through the sprite info pane fields (e.g. direction, size) or
1531
+ // through dragging a sprite on the stage
1532
+ // Emit a project changed event.
1533
+ this.runtime.emitProjectChanged();
1534
+ }
1535
+
1536
+ /**
1537
+ * Set a target's variable's value. Return whether it succeeded.
1538
+ * @param {!string} targetId ID of the target which owns the variable.
1539
+ * @param {!string} variableId ID of the variable to set.
1540
+ * @param {!*} value The new value of that variable.
1541
+ * @returns {boolean} whether the target and variable were found and updated.
1542
+ */
1543
+ setVariableValue (targetId, variableId, value) {
1544
+ const target = this.runtime.getTargetById(targetId);
1545
+ if (target) {
1546
+ const variable = target.lookupVariableById(variableId);
1547
+ if (variable) {
1548
+ variable.value = value;
1549
+
1550
+ if (variable.isCloud) {
1551
+ this.runtime.ioDevices.cloud.requestUpdateVariable(variable.name, variable.value);
1552
+ }
1553
+
1554
+ return true;
1555
+ }
1556
+ }
1557
+ return false;
1558
+ }
1559
+
1560
+ /**
1561
+ * Get a target's variable's value. Return null if the target or variable does not exist.
1562
+ * @param {!string} targetId ID of the target which owns the variable.
1563
+ * @param {!string} variableId ID of the variable to set.
1564
+ * @returns {?*} The value of the variable, or null if it could not be looked up.
1565
+ */
1566
+ getVariableValue (targetId, variableId) {
1567
+ const target = this.runtime.getTargetById(targetId);
1568
+ if (target) {
1569
+ const variable = target.lookupVariableById(variableId);
1570
+ if (variable) {
1571
+ return variable.value;
1572
+ }
1573
+ }
1574
+ return null;
1575
+ }
1576
+
1577
+ /**
1578
+ * Allow VM consumer to configure the ScratchLink socket creator.
1579
+ * @param {Function} factory The custom ScratchLink socket factory.
1580
+ */
1581
+ configureScratchLinkSocketFactory (factory) {
1582
+ this.runtime.configureScratchLinkSocketFactory(factory);
1583
+ }
1584
+ }
1585
+
1586
+ module.exports = VirtualMachine;