@scratch/scratch-vm 11.0.0-UEPR-176

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 (178) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +120 -0
  3. package/dist/node/extension-worker.js +2 -0
  4. package/dist/node/extension-worker.js.map +1 -0
  5. package/dist/node/scratch-vm.js +3 -0
  6. package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
  7. package/dist/node/scratch-vm.js.map +1 -0
  8. package/dist/web/extension-worker.js +2 -0
  9. package/dist/web/extension-worker.js.map +1 -0
  10. package/dist/web/scratch-vm.js +3 -0
  11. package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
  12. package/dist/web/scratch-vm.js.map +1 -0
  13. package/package.json +108 -0
  14. package/src/.eslintrc.js +7 -0
  15. package/src/blocks/scratch3_control.js +206 -0
  16. package/src/blocks/scratch3_core_example.js +69 -0
  17. package/src/blocks/scratch3_data.js +257 -0
  18. package/src/blocks/scratch3_event.js +137 -0
  19. package/src/blocks/scratch3_looks.js +612 -0
  20. package/src/blocks/scratch3_motion.js +288 -0
  21. package/src/blocks/scratch3_operators.js +154 -0
  22. package/src/blocks/scratch3_procedures.js +79 -0
  23. package/src/blocks/scratch3_sensing.js +336 -0
  24. package/src/blocks/scratch3_sound.js +350 -0
  25. package/src/dispatch/central-dispatch.js +143 -0
  26. package/src/dispatch/shared-dispatch.js +235 -0
  27. package/src/dispatch/worker-dispatch.js +110 -0
  28. package/src/engine/adapter.js +176 -0
  29. package/src/engine/block-utility.js +242 -0
  30. package/src/engine/blocks-execute-cache.js +19 -0
  31. package/src/engine/blocks-runtime-cache.js +78 -0
  32. package/src/engine/blocks.js +1299 -0
  33. package/src/engine/comment.js +56 -0
  34. package/src/engine/execute.js +580 -0
  35. package/src/engine/monitor-record.js +23 -0
  36. package/src/engine/mutation-adapter.js +48 -0
  37. package/src/engine/profiler.js +390 -0
  38. package/src/engine/runtime.js +2686 -0
  39. package/src/engine/scratch-blocks-constants.js +27 -0
  40. package/src/engine/sequencer.js +361 -0
  41. package/src/engine/stage-layering.js +29 -0
  42. package/src/engine/target.js +799 -0
  43. package/src/engine/thread.js +404 -0
  44. package/src/engine/variable.js +70 -0
  45. package/src/extension-support/argument-type.js +47 -0
  46. package/src/extension-support/block-type.js +50 -0
  47. package/src/extension-support/define-messages.js +18 -0
  48. package/src/extension-support/extension-manager.js +440 -0
  49. package/src/extension-support/extension-metadata.js +64 -0
  50. package/src/extension-support/extension-worker.js +59 -0
  51. package/src/extension-support/reporter-scope.js +18 -0
  52. package/src/extension-support/target-type.js +17 -0
  53. package/src/extensions/scratch3_boost/index.js +2113 -0
  54. package/src/extensions/scratch3_ev3/index.js +1355 -0
  55. package/src/extensions/scratch3_gdx_for/index.js +981 -0
  56. package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
  57. package/src/extensions/scratch3_makeymakey/index.js +396 -0
  58. package/src/extensions/scratch3_microbit/index.js +984 -0
  59. package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
  60. package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
  61. package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
  62. package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
  63. package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
  64. package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
  65. package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
  66. package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
  67. package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
  68. package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
  69. package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
  70. package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
  71. package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
  72. package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
  73. package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
  74. package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
  75. package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
  76. package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
  77. package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
  78. package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
  79. package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
  80. package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
  81. package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
  82. package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
  83. package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
  84. package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
  85. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
  86. package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
  87. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
  88. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
  89. package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
  90. package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
  91. package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
  92. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
  93. package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
  94. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
  95. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
  96. package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
  97. package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
  98. package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
  99. package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
  100. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
  101. package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
  102. package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
  103. package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
  104. package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
  105. package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
  106. package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
  107. package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
  108. package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
  109. package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
  110. package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
  111. package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
  112. package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
  113. package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
  114. package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
  115. package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
  116. package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
  117. package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
  118. package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
  119. package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
  120. package/src/extensions/scratch3_music/index.js +1333 -0
  121. package/src/extensions/scratch3_music/manifest.js +63 -0
  122. package/src/extensions/scratch3_pen/index.js +770 -0
  123. package/src/extensions/scratch3_speech2text/index.js +700 -0
  124. package/src/extensions/scratch3_text2speech/index.js +766 -0
  125. package/src/extensions/scratch3_translate/index.js +286 -0
  126. package/src/extensions/scratch3_video_sensing/debug.js +13 -0
  127. package/src/extensions/scratch3_video_sensing/index.js +594 -0
  128. package/src/extensions/scratch3_video_sensing/library.js +384 -0
  129. package/src/extensions/scratch3_video_sensing/math.js +76 -0
  130. package/src/extensions/scratch3_video_sensing/view.js +509 -0
  131. package/src/extensions/scratch3_wedo2/index.js +1616 -0
  132. package/src/import/load-costume.js +416 -0
  133. package/src/import/load-sound.js +116 -0
  134. package/src/index.js +10 -0
  135. package/src/io/ble.js +256 -0
  136. package/src/io/bt.js +202 -0
  137. package/src/io/clock.js +39 -0
  138. package/src/io/cloud.js +169 -0
  139. package/src/io/keyboard.js +146 -0
  140. package/src/io/mouse.js +145 -0
  141. package/src/io/mouseWheel.js +28 -0
  142. package/src/io/userData.js +24 -0
  143. package/src/io/video.js +210 -0
  144. package/src/playground/benchmark.css +83 -0
  145. package/src/playground/benchmark.js +753 -0
  146. package/src/playground/index.html +111 -0
  147. package/src/playground/suite.css +78 -0
  148. package/src/playground/suite.html +25 -0
  149. package/src/playground/suite.js +544 -0
  150. package/src/playground/video-sensing.html +18 -0
  151. package/src/playground/video-sensing.js +133 -0
  152. package/src/serialization/deserialize-assets.js +178 -0
  153. package/src/serialization/sb2.js +1295 -0
  154. package/src/serialization/sb2_specmap.js +1818 -0
  155. package/src/serialization/sb3.js +1320 -0
  156. package/src/serialization/serialize-assets.js +60 -0
  157. package/src/sprites/rendered-target.js +1115 -0
  158. package/src/sprites/sprite.js +177 -0
  159. package/src/util/base64-util.js +48 -0
  160. package/src/util/cast.js +218 -0
  161. package/src/util/clone.js +17 -0
  162. package/src/util/color.js +204 -0
  163. package/src/util/fetch-with-timeout.js +53 -0
  164. package/src/util/get-monitor-id.js +33 -0
  165. package/src/util/jsonrpc.js +114 -0
  166. package/src/util/log.js +4 -0
  167. package/src/util/math-util.js +122 -0
  168. package/src/util/maybe-format-message.js +18 -0
  169. package/src/util/new-block-ids.js +33 -0
  170. package/src/util/rateLimiter.js +73 -0
  171. package/src/util/scratch-link-websocket.js +133 -0
  172. package/src/util/string-util.js +94 -0
  173. package/src/util/task-queue.js +203 -0
  174. package/src/util/timer.js +103 -0
  175. package/src/util/uid.js +29 -0
  176. package/src/util/variable-util.js +48 -0
  177. package/src/util/xml-escape.js +33 -0
  178. package/src/virtual-machine.js +1586 -0
@@ -0,0 +1,1320 @@
1
+ /**
2
+ * @fileoverview
3
+ * An SB3 serializer and deserializer. Parses provided
4
+ * JSON and then generates all needed scratch-vm runtime structures.
5
+ */
6
+
7
+ const vmPackage = require('../../package.json');
8
+ const Blocks = require('../engine/blocks');
9
+ const Sprite = require('../sprites/sprite');
10
+ const Variable = require('../engine/variable');
11
+ const Comment = require('../engine/comment');
12
+ const MonitorRecord = require('../engine/monitor-record');
13
+ const StageLayering = require('../engine/stage-layering');
14
+ const log = require('../util/log');
15
+ const uid = require('../util/uid');
16
+ const MathUtil = require('../util/math-util');
17
+ const StringUtil = require('../util/string-util');
18
+ const VariableUtil = require('../util/variable-util');
19
+
20
+ const {loadCostume} = require('../import/load-costume.js');
21
+ const {loadSound} = require('../import/load-sound.js');
22
+ const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js');
23
+
24
+ const hasOwnProperty = Object.prototype.hasOwnProperty;
25
+
26
+ /**
27
+ * @typedef {object} ImportedProject
28
+ * @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
29
+ * @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project.
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} ImportedExtensionsInfo
34
+ * @property {Set.<string>} extensionIDs - the ID of each extension actually in use by blocks in this project.
35
+ * @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
36
+ */
37
+
38
+ // Constants used during serialization and deserialization
39
+ const INPUT_SAME_BLOCK_SHADOW = 1; // unobscured shadow
40
+ const INPUT_BLOCK_NO_SHADOW = 2; // no shadow
41
+ const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow
42
+ // There shouldn't be a case where block is null, but shadow is present...
43
+
44
+ // Constants used during deserialization of an SB3 file
45
+ const CORE_EXTENSIONS = [
46
+ 'argument',
47
+ 'colour',
48
+ 'control',
49
+ 'data',
50
+ 'event',
51
+ 'looks',
52
+ 'math',
53
+ 'motion',
54
+ 'operator',
55
+ 'procedures',
56
+ 'sensing',
57
+ 'sound'
58
+ ];
59
+
60
+ // Constants referring to 'primitive' blocks that are usually shadows,
61
+ // or in the case of variables and lists, appear quite often in projects
62
+ // math_number
63
+ const MATH_NUM_PRIMITIVE = 4; // there's no reason these constants can't collide
64
+ // math_positive_number
65
+ const POSITIVE_NUM_PRIMITIVE = 5; // with the above, but removing duplication for clarity
66
+ // math_whole_number
67
+ const WHOLE_NUM_PRIMITIVE = 6;
68
+ // math_integer
69
+ const INTEGER_NUM_PRIMITIVE = 7;
70
+ // math_angle
71
+ const ANGLE_NUM_PRIMITIVE = 8;
72
+ // colour_picker
73
+ const COLOR_PICKER_PRIMITIVE = 9;
74
+ // text
75
+ const TEXT_PRIMITIVE = 10;
76
+ // event_broadcast_menu
77
+ const BROADCAST_PRIMITIVE = 11;
78
+ // data_variable
79
+ const VAR_PRIMITIVE = 12;
80
+ // data_listcontents
81
+ const LIST_PRIMITIVE = 13;
82
+
83
+ // Map block opcodes to the above primitives and the name of the field we can use
84
+ // to find the value of the field
85
+ const primitiveOpcodeInfoMap = {
86
+ math_number: [MATH_NUM_PRIMITIVE, 'NUM'],
87
+ math_positive_number: [POSITIVE_NUM_PRIMITIVE, 'NUM'],
88
+ math_whole_number: [WHOLE_NUM_PRIMITIVE, 'NUM'],
89
+ math_integer: [INTEGER_NUM_PRIMITIVE, 'NUM'],
90
+ math_angle: [ANGLE_NUM_PRIMITIVE, 'NUM'],
91
+ colour_picker: [COLOR_PICKER_PRIMITIVE, 'COLOUR'],
92
+ text: [TEXT_PRIMITIVE, 'TEXT'],
93
+ event_broadcast_menu: [BROADCAST_PRIMITIVE, 'BROADCAST_OPTION'],
94
+ data_variable: [VAR_PRIMITIVE, 'VARIABLE'],
95
+ data_listcontents: [LIST_PRIMITIVE, 'LIST']
96
+ };
97
+
98
+ /**
99
+ * Serializes primitives described above into a more compact format
100
+ * @param {object} block the block to serialize
101
+ * @return {array} An array representing the information in the block,
102
+ * or null if the given block is not one of the primitives described above.
103
+ */
104
+ const serializePrimitiveBlock = function (block) {
105
+ // Returns an array represeting a primitive block or null if not one of
106
+ // the primitive types above
107
+ if (hasOwnProperty.call(primitiveOpcodeInfoMap, block.opcode)) {
108
+ const primitiveInfo = primitiveOpcodeInfoMap[block.opcode];
109
+ const primitiveConstant = primitiveInfo[0];
110
+ const fieldName = primitiveInfo[1];
111
+ const field = block.fields[fieldName];
112
+ const primitiveDesc = [primitiveConstant, field.value];
113
+ if (block.opcode === 'event_broadcast_menu') {
114
+ primitiveDesc.push(field.id);
115
+ } else if (block.opcode === 'data_variable' || block.opcode === 'data_listcontents') {
116
+ primitiveDesc.push(field.id);
117
+ if (block.topLevel) {
118
+ primitiveDesc.push(block.x ? Math.round(block.x) : 0);
119
+ primitiveDesc.push(block.y ? Math.round(block.y) : 0);
120
+ }
121
+ }
122
+ return primitiveDesc;
123
+ }
124
+ return null;
125
+ };
126
+
127
+ /**
128
+ * Serializes the inputs field of a block in a compact form using
129
+ * constants described above to represent the relationship between the
130
+ * inputs of this block (e.g. if there is an unobscured shadow, an obscured shadow
131
+ * -- a block plugged into a droppable input -- or, if there is just a block).
132
+ * Based on this relationship, serializes the ids of the block and shadow (if present)
133
+ *
134
+ * @param {object} inputs The inputs to serialize
135
+ * @return {object} An object representing the serialized inputs
136
+ */
137
+ const serializeInputs = function (inputs) {
138
+ const obj = Object.create(null);
139
+ for (const inputName in inputs) {
140
+ if (!hasOwnProperty.call(inputs, inputName)) continue;
141
+ // if block and shadow refer to the same block, only serialize one
142
+ if (inputs[inputName].block === inputs[inputName].shadow) {
143
+ // has block and shadow, and they are the same
144
+ obj[inputName] = [
145
+ INPUT_SAME_BLOCK_SHADOW,
146
+ inputs[inputName].block
147
+ ];
148
+ } else if (inputs[inputName].shadow === null) {
149
+ // does not have shadow
150
+ obj[inputName] = [
151
+ INPUT_BLOCK_NO_SHADOW,
152
+ inputs[inputName].block
153
+ ];
154
+ } else {
155
+ // block and shadow are both present and are different
156
+ obj[inputName] = [
157
+ INPUT_DIFF_BLOCK_SHADOW,
158
+ inputs[inputName].block,
159
+ inputs[inputName].shadow
160
+ ];
161
+ }
162
+ }
163
+ return obj;
164
+ };
165
+
166
+ /**
167
+ * Serialize the fields of a block in a more compact form.
168
+ * @param {object} fields The fields object to serialize
169
+ * @return {object} An object representing the serialized fields
170
+ */
171
+ const serializeFields = function (fields) {
172
+ const obj = Object.create(null);
173
+ for (const fieldName in fields) {
174
+ if (!hasOwnProperty.call(fields, fieldName)) continue;
175
+ obj[fieldName] = [fields[fieldName].value];
176
+ if (Object.prototype.hasOwnProperty.call(fields[fieldName], 'id')) {
177
+ obj[fieldName].push(fields[fieldName].id);
178
+ }
179
+ }
180
+ return obj;
181
+ };
182
+
183
+ /**
184
+ * Serialize the given block in the SB3 format with some compression of inputs,
185
+ * fields, and primitives.
186
+ * @param {object} block The block to serialize
187
+ * @return {object | array} A serialized representation of the block. This is an
188
+ * array if the block is one of the primitive types described above or an object,
189
+ * if not.
190
+ */
191
+ const serializeBlock = function (block) {
192
+ const serializedPrimitive = serializePrimitiveBlock(block);
193
+ if (serializedPrimitive) return serializedPrimitive;
194
+ // If serializedPrimitive is null, proceed with serializing a non-primitive block
195
+ const obj = Object.create(null);
196
+ obj.opcode = block.opcode;
197
+ // NOTE: this is extremely important to serialize even if null;
198
+ // not serializing `next: null` results in strange behavior with block
199
+ // execution
200
+ obj.next = block.next;
201
+ obj.parent = block.parent;
202
+ obj.inputs = serializeInputs(block.inputs);
203
+ obj.fields = serializeFields(block.fields);
204
+ obj.shadow = block.shadow;
205
+ if (block.topLevel) {
206
+ obj.topLevel = true;
207
+ obj.x = block.x ? Math.round(block.x) : 0;
208
+ obj.y = block.y ? Math.round(block.y) : 0;
209
+ } else {
210
+ obj.topLevel = false;
211
+ }
212
+ if (block.mutation) {
213
+ obj.mutation = block.mutation;
214
+ }
215
+ if (block.comment) {
216
+ obj.comment = block.comment;
217
+ }
218
+ return obj;
219
+ };
220
+
221
+ /**
222
+ * Compresses the serialized inputs replacing block/shadow ids that refer to
223
+ * one of the primitives with the primitive itself. E.g.
224
+ *
225
+ * blocks: {
226
+ * aUidForMyBlock: {
227
+ * inputs: {
228
+ * MYINPUT: [1, 'aUidForAnUnobscuredShadowPrimitive']
229
+ * }
230
+ * },
231
+ * aUidForAnUnobscuredShadowPrimitive: [4, 10]
232
+ * // the above is a primitive representing a 'math_number' with value 10
233
+ * }
234
+ *
235
+ * becomes:
236
+ *
237
+ * blocks: {
238
+ * aUidForMyBlock: {
239
+ * inputs: {
240
+ * MYINPUT: [1, [4, 10]]
241
+ * }
242
+ * }
243
+ * }
244
+ * Note: this function modifies the given blocks object in place
245
+ * @param {object} block The block with inputs to compress
246
+ * @param {objec} blocks The object containing all the blocks currently getting serialized
247
+ * @return {object} The serialized block with compressed inputs
248
+ */
249
+ const compressInputTree = function (block, blocks) {
250
+ // This is the second pass on the block
251
+ // so the inputs field should be an object of key - array pairs
252
+ const serializedInputs = block.inputs;
253
+ for (const inputName in serializedInputs) {
254
+ // don't need to check for hasOwnProperty because of how we constructed
255
+ // inputs
256
+ const currInput = serializedInputs[inputName];
257
+ // traverse currInput skipping the first element, which describes whether the block
258
+ // and shadow are the same
259
+ for (let i = 1; i < currInput.length; i++) {
260
+ if (!currInput[i]) continue; // need this check b/c block/shadow can be null
261
+ const blockOrShadowID = currInput[i];
262
+ // replace element of currInput directly
263
+ // (modifying input block directly)
264
+ const blockOrShadow = blocks[blockOrShadowID];
265
+ if (Array.isArray(blockOrShadow)) {
266
+ currInput[i] = blockOrShadow;
267
+ // Modifying blocks in place!
268
+ delete blocks[blockOrShadowID];
269
+ }
270
+ }
271
+ }
272
+ return block;
273
+ };
274
+
275
+ /**
276
+ * Get sanitized non-core extension ID for a given sb3 opcode.
277
+ * Note that this should never return a URL. If in the future the SB3 loader supports loading extensions by URL, this
278
+ * ID should be used to (for example) look up the extension's full URL from a table in the SB3's JSON.
279
+ * @param {!string} opcode The opcode to examine for extension.
280
+ * @return {?string} The extension ID, if it exists and is not a core extension.
281
+ */
282
+ const getExtensionIdForOpcode = function (opcode) {
283
+ // Allowed ID characters are those matching the regular expression [\w-]: A-Z, a-z, 0-9, and hyphen ("-").
284
+ const index = opcode.indexOf('_');
285
+ const forbiddenSymbols = /[^\w-]/g;
286
+ const prefix = opcode.substring(0, index).replace(forbiddenSymbols, '-');
287
+ if (CORE_EXTENSIONS.indexOf(prefix) === -1) {
288
+ if (prefix !== '') return prefix;
289
+ }
290
+ };
291
+
292
+ /**
293
+ * Serialize the given blocks object (representing all the blocks for the target
294
+ * currently being serialized.)
295
+ * @param {object} blocks The blocks to be serialized
296
+ * @return {Array} An array of the serialized blocks with compressed inputs and
297
+ * compressed primitives and the list of all extension IDs present
298
+ * in the serialized blocks.
299
+ */
300
+ const serializeBlocks = function (blocks) {
301
+ const obj = Object.create(null);
302
+ const extensionIDs = new Set();
303
+ for (const blockID in blocks) {
304
+ if (!Object.prototype.hasOwnProperty.call(blocks, blockID)) continue;
305
+ obj[blockID] = serializeBlock(blocks[blockID], blocks);
306
+ const extensionID = getExtensionIdForOpcode(blocks[blockID].opcode);
307
+ if (extensionID) {
308
+ extensionIDs.add(extensionID);
309
+ }
310
+ }
311
+ // once we have completed a first pass, do a second pass on block inputs
312
+ for (const blockID in obj) {
313
+ // don't need to do the hasOwnProperty check here since we
314
+ // created an object that doesn't get extra properties/functions
315
+ const serializedBlock = obj[blockID];
316
+ // caution, this function deletes parts of this object in place as
317
+ // it's traversing it
318
+ obj[blockID] = compressInputTree(serializedBlock, obj);
319
+ // second pass on connecting primitives to serialized inputs directly
320
+ }
321
+ // Do one last pass and remove any top level shadows (these are caused by
322
+ // a bug: LLK/scratch-vm#1011, and this pass should be removed once that is
323
+ // completely fixed)
324
+ for (const blockID in obj) {
325
+ const serializedBlock = obj[blockID];
326
+ // If the current block is serialized as a primitive (e.g. it's an array
327
+ // instead of an object), AND it is not one of the top level primitives
328
+ // e.g. variable getter or list getter, then it should be deleted as it's
329
+ // a shadow block, and there are no blocks that reference it, otherwise
330
+ // they would have been compressed in the last pass)
331
+ if (Array.isArray(serializedBlock) &&
332
+ [VAR_PRIMITIVE, LIST_PRIMITIVE].indexOf(serializedBlock[0]) < 0) {
333
+ log.warn(`Found an unexpected top level primitive with block ID: ${
334
+ blockID}; deleting it from serialized blocks.`);
335
+ delete obj[blockID];
336
+ }
337
+ }
338
+ return [obj, Array.from(extensionIDs)];
339
+ };
340
+
341
+ /**
342
+ * Serialize the given costume.
343
+ * @param {object} costume The costume to be serialized.
344
+ * @return {object} A serialized representation of the costume.
345
+ */
346
+ const serializeCostume = function (costume) {
347
+ const obj = Object.create(null);
348
+ obj.name = costume.name;
349
+
350
+ const costumeToSerialize = costume.broken || costume;
351
+
352
+ obj.bitmapResolution = costumeToSerialize.bitmapResolution;
353
+ obj.dataFormat = costumeToSerialize.dataFormat.toLowerCase();
354
+
355
+ obj.assetId = costumeToSerialize.assetId;
356
+
357
+ // serialize this property with the name 'md5ext' because that's
358
+ // what it's actually referring to. TODO runtime objects need to be
359
+ // updated to actually refer to this as 'md5ext' instead of 'md5'
360
+ // but that change should be made carefully since it is very
361
+ // pervasive
362
+ obj.md5ext = costumeToSerialize.md5;
363
+
364
+ obj.rotationCenterX = costumeToSerialize.rotationCenterX;
365
+ obj.rotationCenterY = costumeToSerialize.rotationCenterY;
366
+
367
+ return obj;
368
+ };
369
+
370
+ /**
371
+ * Serialize the given sound.
372
+ * @param {object} sound The sound to be serialized.
373
+ * @return {object} A serialized representation of the sound.
374
+ */
375
+ const serializeSound = function (sound) {
376
+ const obj = Object.create(null);
377
+ obj.name = sound.name;
378
+
379
+ const soundToSerialize = sound.broken || sound;
380
+
381
+ obj.assetId = soundToSerialize.assetId;
382
+ obj.dataFormat = soundToSerialize.dataFormat.toLowerCase();
383
+ obj.format = soundToSerialize.format;
384
+ obj.rate = soundToSerialize.rate;
385
+ obj.sampleCount = soundToSerialize.sampleCount;
386
+ // serialize this property with the name 'md5ext' because that's
387
+ // what it's actually referring to. TODO runtime objects need to be
388
+ // updated to actually refer to this as 'md5ext' instead of 'md5'
389
+ // but that change should be made carefully since it is very
390
+ // pervasive
391
+ obj.md5ext = soundToSerialize.md5;
392
+ return obj;
393
+ };
394
+
395
+ /**
396
+ * Serialize the given variables object.
397
+ * @param {object} variables The variables to be serialized.
398
+ * @return {object} A serialized representation of the variables. They get
399
+ * separated by type to compress the representation of each given variable and
400
+ * reduce duplicate information.
401
+ */
402
+ const serializeVariables = function (variables) {
403
+ const obj = Object.create(null);
404
+ // separate out variables into types at the top level so we don't have
405
+ // keep track of a type for each
406
+ obj.variables = Object.create(null);
407
+ obj.lists = Object.create(null);
408
+ obj.broadcasts = Object.create(null);
409
+ for (const varId in variables) {
410
+ const v = variables[varId];
411
+ if (v.type === Variable.BROADCAST_MESSAGE_TYPE) {
412
+ obj.broadcasts[varId] = v.value; // name and value is the same for broadcast msgs
413
+ continue;
414
+ }
415
+ if (v.type === Variable.LIST_TYPE) {
416
+ obj.lists[varId] = [v.name, v.value];
417
+ continue;
418
+ }
419
+
420
+ // otherwise should be a scalar type
421
+ obj.variables[varId] = [v.name, v.value];
422
+ // only scalar vars have the potential to be cloud vars
423
+ if (v.isCloud) obj.variables[varId].push(true);
424
+ }
425
+ return obj;
426
+ };
427
+
428
+ const serializeComments = function (comments) {
429
+ const obj = Object.create(null);
430
+ for (const commentId in comments) {
431
+ if (!Object.prototype.hasOwnProperty.call(comments, commentId)) continue;
432
+ const comment = comments[commentId];
433
+
434
+ const serializedComment = Object.create(null);
435
+ serializedComment.blockId = comment.blockId;
436
+ serializedComment.x = comment.x;
437
+ serializedComment.y = comment.y;
438
+ serializedComment.width = comment.width;
439
+ serializedComment.height = comment.height;
440
+ serializedComment.minimized = comment.minimized;
441
+ serializedComment.text = comment.text;
442
+
443
+ obj[commentId] = serializedComment;
444
+ }
445
+ return obj;
446
+ };
447
+
448
+ /**
449
+ * Serialize the given target. Only serialize properties that are necessary
450
+ * for saving and loading this target.
451
+ * @param {object} target The target to be serialized.
452
+ * @param {Set} extensions A set of extensions to add extension IDs to
453
+ * @return {object} A serialized representation of the given target.
454
+ */
455
+ const serializeTarget = function (target, extensions) {
456
+ const obj = Object.create(null);
457
+ let targetExtensions = [];
458
+ obj.isStage = target.isStage;
459
+ obj.name = obj.isStage ? 'Stage' : target.name;
460
+ const vars = serializeVariables(target.variables);
461
+ obj.variables = vars.variables;
462
+ obj.lists = vars.lists;
463
+ obj.broadcasts = vars.broadcasts;
464
+ [obj.blocks, targetExtensions] = serializeBlocks(target.blocks);
465
+ obj.comments = serializeComments(target.comments);
466
+
467
+ // TODO remove this check/patch when (#1901) is fixed
468
+ if (target.currentCostume < 0 || target.currentCostume >= target.costumes.length) {
469
+ log.warn(`currentCostume property for target ${target.name} is out of range`);
470
+ target.currentCostume = MathUtil.clamp(target.currentCostume, 0, target.costumes.length - 1);
471
+ }
472
+
473
+ obj.currentCostume = target.currentCostume;
474
+ obj.costumes = target.costumes.map(serializeCostume);
475
+ obj.sounds = target.sounds.map(serializeSound);
476
+ if (Object.prototype.hasOwnProperty.call(target, 'volume')) obj.volume = target.volume;
477
+ if (Object.prototype.hasOwnProperty.call(target, 'layerOrder')) obj.layerOrder = target.layerOrder;
478
+ if (obj.isStage) { // Only the stage should have these properties
479
+ if (Object.prototype.hasOwnProperty.call(target, 'tempo')) {
480
+ obj.tempo = target.tempo;
481
+ }
482
+ if (Object.prototype.hasOwnProperty.call(target, 'videoTransparency')) {
483
+ obj.videoTransparency = target.videoTransparency;
484
+ }
485
+ if (Object.prototype.hasOwnProperty.call(target, 'videoState')) {
486
+ obj.videoState = target.videoState;
487
+ }
488
+ if (Object.prototype.hasOwnProperty.call(target, 'textToSpeechLanguage')) {
489
+ obj.textToSpeechLanguage = target.textToSpeechLanguage;
490
+ }
491
+ } else { // The stage does not need the following properties, but sprites should
492
+ obj.visible = target.visible;
493
+ obj.x = target.x;
494
+ obj.y = target.y;
495
+ obj.size = target.size;
496
+ obj.direction = target.direction;
497
+ obj.draggable = target.draggable;
498
+ obj.rotationStyle = target.rotationStyle;
499
+ }
500
+
501
+ // Add found extensions to the extensions object
502
+ targetExtensions.forEach(extensionId => {
503
+ extensions.add(extensionId);
504
+ });
505
+ return obj;
506
+ };
507
+
508
+ const getSimplifiedLayerOrdering = function (targets) {
509
+ const layerOrders = targets.map(t => t.getLayerOrder());
510
+ return MathUtil.reducedSortOrdering(layerOrders);
511
+ };
512
+
513
+ const serializeMonitors = function (monitors) {
514
+ return monitors.valueSeq().map(monitorData => {
515
+ const serializedMonitor = {
516
+ id: monitorData.id,
517
+ mode: monitorData.mode,
518
+ opcode: monitorData.opcode,
519
+ params: monitorData.params,
520
+ spriteName: monitorData.spriteName,
521
+ value: monitorData.value,
522
+ width: monitorData.width,
523
+ height: monitorData.height,
524
+ x: monitorData.x,
525
+ y: monitorData.y,
526
+ visible: monitorData.visible
527
+ };
528
+ if (monitorData.mode !== 'list') {
529
+ serializedMonitor.sliderMin = monitorData.sliderMin;
530
+ serializedMonitor.sliderMax = monitorData.sliderMax;
531
+ serializedMonitor.isDiscrete = monitorData.isDiscrete;
532
+ }
533
+ return serializedMonitor;
534
+ });
535
+ };
536
+
537
+ /**
538
+ * Serializes the specified VM runtime.
539
+ * @param {!Runtime} runtime VM runtime instance to be serialized.
540
+ * @param {string=} targetId Optional target id if serializing only a single target
541
+ * @return {object} Serialized runtime instance.
542
+ */
543
+ const serialize = function (runtime, targetId) {
544
+ // Fetch targets
545
+ const obj = Object.create(null);
546
+ // Create extension set to hold extension ids found while serializing targets
547
+ const extensions = new Set();
548
+
549
+ const originalTargetsToSerialize = targetId ?
550
+ [runtime.getTargetById(targetId)] :
551
+ runtime.targets.filter(target => target.isOriginal);
552
+
553
+ const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize);
554
+
555
+ const flattenedOriginalTargets = originalTargetsToSerialize.map(t => t.toJSON());
556
+
557
+ // If the renderer is attached, and we're serializing a whole project (not a sprite)
558
+ // add a temporary layerOrder property to each target.
559
+ if (runtime.renderer && !targetId) {
560
+ flattenedOriginalTargets.forEach((t, index) => {
561
+ t.layerOrder = layerOrdering[index];
562
+ });
563
+ }
564
+
565
+ const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
566
+
567
+ if (targetId) {
568
+ return serializedTargets[0];
569
+ }
570
+
571
+ obj.targets = serializedTargets;
572
+
573
+ obj.monitors = serializeMonitors(runtime.getMonitorState());
574
+
575
+ // Assemble extension list
576
+ obj.extensions = Array.from(extensions);
577
+
578
+ // Assemble metadata
579
+ const meta = Object.create(null);
580
+ meta.semver = '3.0.0';
581
+ meta.vm = vmPackage.version;
582
+ if (runtime.origin) {
583
+ meta.origin = runtime.origin;
584
+ }
585
+
586
+ // Attach full user agent string to metadata if available
587
+ meta.agent = 'none';
588
+ if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent;
589
+
590
+ // Assemble payload and return
591
+ obj.meta = meta;
592
+ return obj;
593
+ };
594
+
595
+ /**
596
+ * Deserialize a block input descriptors. This is either a
597
+ * block id or a serialized primitive, e.g. an array
598
+ * (see serializePrimitiveBlock function).
599
+ * @param {string | array} inputDescOrId The block input descriptor to be serialized.
600
+ * @param {string} parentId The id of the parent block for this input block.
601
+ * @param {boolean} isShadow Whether or not this input block is a shadow.
602
+ * @param {object} blocks The entire blocks object currently in the process of getting serialized.
603
+ * @return {object} The deserialized input descriptor.
604
+ */
605
+ const deserializeInputDesc = function (inputDescOrId, parentId, isShadow, blocks) {
606
+ if (!Array.isArray(inputDescOrId)) return inputDescOrId;
607
+ const primitiveObj = Object.create(null);
608
+ const newId = uid();
609
+ primitiveObj.id = newId;
610
+ primitiveObj.next = null;
611
+ primitiveObj.parent = parentId;
612
+ primitiveObj.shadow = isShadow;
613
+ primitiveObj.inputs = Object.create(null);
614
+ // need a reference to parent id
615
+ switch (inputDescOrId[0]) {
616
+ case MATH_NUM_PRIMITIVE: {
617
+ primitiveObj.opcode = 'math_number';
618
+ primitiveObj.fields = {
619
+ NUM: {
620
+ name: 'NUM',
621
+ value: inputDescOrId[1]
622
+ }
623
+ };
624
+ primitiveObj.topLevel = false;
625
+ break;
626
+ }
627
+ case POSITIVE_NUM_PRIMITIVE: {
628
+ primitiveObj.opcode = 'math_positive_number';
629
+ primitiveObj.fields = {
630
+ NUM: {
631
+ name: 'NUM',
632
+ value: inputDescOrId[1]
633
+ }
634
+ };
635
+ primitiveObj.topLevel = false;
636
+ break;
637
+ }
638
+ case WHOLE_NUM_PRIMITIVE: {
639
+ primitiveObj.opcode = 'math_whole_number';
640
+ primitiveObj.fields = {
641
+ NUM: {
642
+ name: 'NUM',
643
+ value: inputDescOrId[1]
644
+ }
645
+ };
646
+ primitiveObj.topLevel = false;
647
+ break;
648
+ }
649
+ case INTEGER_NUM_PRIMITIVE: {
650
+ primitiveObj.opcode = 'math_integer';
651
+ primitiveObj.fields = {
652
+ NUM: {
653
+ name: 'NUM',
654
+ value: inputDescOrId[1]
655
+ }
656
+ };
657
+ primitiveObj.topLevel = false;
658
+ break;
659
+ }
660
+ case ANGLE_NUM_PRIMITIVE: {
661
+ primitiveObj.opcode = 'math_angle';
662
+ primitiveObj.fields = {
663
+ NUM: {
664
+ name: 'NUM',
665
+ value: inputDescOrId[1]
666
+ }
667
+ };
668
+ primitiveObj.topLevel = false;
669
+ break;
670
+ }
671
+ case COLOR_PICKER_PRIMITIVE: {
672
+ primitiveObj.opcode = 'colour_picker';
673
+ primitiveObj.fields = {
674
+ COLOUR: {
675
+ name: 'COLOUR',
676
+ value: inputDescOrId[1]
677
+ }
678
+ };
679
+ primitiveObj.topLevel = false;
680
+ break;
681
+ }
682
+ case TEXT_PRIMITIVE: {
683
+ primitiveObj.opcode = 'text';
684
+ primitiveObj.fields = {
685
+ TEXT: {
686
+ name: 'TEXT',
687
+ value: inputDescOrId[1]
688
+ }
689
+ };
690
+ primitiveObj.topLevel = false;
691
+ break;
692
+ }
693
+ case BROADCAST_PRIMITIVE: {
694
+ primitiveObj.opcode = 'event_broadcast_menu';
695
+ primitiveObj.fields = {
696
+ BROADCAST_OPTION: {
697
+ name: 'BROADCAST_OPTION',
698
+ value: inputDescOrId[1],
699
+ id: inputDescOrId[2],
700
+ variableType: Variable.BROADCAST_MESSAGE_TYPE
701
+ }
702
+ };
703
+ primitiveObj.topLevel = false;
704
+ break;
705
+ }
706
+ case VAR_PRIMITIVE: {
707
+ primitiveObj.opcode = 'data_variable';
708
+ primitiveObj.fields = {
709
+ VARIABLE: {
710
+ name: 'VARIABLE',
711
+ value: inputDescOrId[1],
712
+ id: inputDescOrId[2],
713
+ variableType: Variable.SCALAR_TYPE
714
+ }
715
+ };
716
+ if (inputDescOrId.length > 3) {
717
+ primitiveObj.topLevel = true;
718
+ primitiveObj.x = inputDescOrId[3];
719
+ primitiveObj.y = inputDescOrId[4];
720
+ }
721
+ break;
722
+ }
723
+ case LIST_PRIMITIVE: {
724
+ primitiveObj.opcode = 'data_listcontents';
725
+ primitiveObj.fields = {
726
+ LIST: {
727
+ name: 'LIST',
728
+ value: inputDescOrId[1],
729
+ id: inputDescOrId[2],
730
+ variableType: Variable.LIST_TYPE
731
+ }
732
+ };
733
+ if (inputDescOrId.length > 3) {
734
+ primitiveObj.topLevel = true;
735
+ primitiveObj.x = inputDescOrId[3];
736
+ primitiveObj.y = inputDescOrId[4];
737
+ }
738
+ break;
739
+ }
740
+ default: {
741
+ log.error(`Found unknown primitive type during deserialization: ${JSON.stringify(inputDescOrId)}`);
742
+ return null;
743
+ }
744
+ }
745
+ blocks[newId] = primitiveObj;
746
+ return newId;
747
+ };
748
+
749
+ /**
750
+ * Deserialize the given block inputs.
751
+ * @param {object} inputs The inputs to deserialize.
752
+ * @param {string} parentId The block id of the parent block
753
+ * @param {object} blocks The object representing the entire set of blocks currently
754
+ * in the process of getting deserialized.
755
+ * @return {object} The deserialized and uncompressed inputs.
756
+ */
757
+ const deserializeInputs = function (inputs, parentId, blocks) {
758
+ // Explicitly not using Object.create(null) here
759
+ // because we call prototype functions later in the vm
760
+ const obj = {};
761
+ for (const inputName in inputs) {
762
+ if (!hasOwnProperty.call(inputs, inputName)) continue;
763
+ const inputDescArr = inputs[inputName];
764
+ // If this block has already been deserialized (it's not an array) skip it
765
+ if (!Array.isArray(inputDescArr)) continue;
766
+ let block = null;
767
+ let shadow = null;
768
+ const blockShadowInfo = inputDescArr[0];
769
+ if (blockShadowInfo === INPUT_SAME_BLOCK_SHADOW) {
770
+ // block and shadow are the same id, and only one is provided
771
+ block = shadow = deserializeInputDesc(inputDescArr[1], parentId, true, blocks);
772
+ } else if (blockShadowInfo === INPUT_BLOCK_NO_SHADOW) {
773
+ block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks);
774
+ } else { // assume INPUT_DIFF_BLOCK_SHADOW
775
+ block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks);
776
+ shadow = deserializeInputDesc(inputDescArr[2], parentId, true, blocks);
777
+ }
778
+ obj[inputName] = {
779
+ name: inputName,
780
+ block: block,
781
+ shadow: shadow
782
+ };
783
+ }
784
+ return obj;
785
+ };
786
+
787
+ /**
788
+ * Deserialize the given block fields.
789
+ * @param {object} fields The fields to be deserialized
790
+ * @return {object} The deserialized and uncompressed block fields.
791
+ */
792
+ const deserializeFields = function (fields) {
793
+ // Explicitly not using Object.create(null) here
794
+ // because we call prototype functions later in the vm
795
+ const obj = {};
796
+ for (const fieldName in fields) {
797
+ if (!hasOwnProperty.call(fields, fieldName)) continue;
798
+ const fieldDescArr = fields[fieldName];
799
+ // If this block has already been deserialized (it's not an array) skip it
800
+ if (!Array.isArray(fieldDescArr)) continue;
801
+ obj[fieldName] = {
802
+ name: fieldName,
803
+ value: fieldDescArr[0]
804
+ };
805
+ if (fieldDescArr.length > 1) {
806
+ obj[fieldName].id = fieldDescArr[1];
807
+ }
808
+ if (fieldName === 'BROADCAST_OPTION') {
809
+ obj[fieldName].variableType = Variable.BROADCAST_MESSAGE_TYPE;
810
+ } else if (fieldName === 'VARIABLE') {
811
+ obj[fieldName].variableType = Variable.SCALAR_TYPE;
812
+ } else if (fieldName === 'LIST') {
813
+ obj[fieldName].variableType = Variable.LIST_TYPE;
814
+ }
815
+ }
816
+ return obj;
817
+ };
818
+
819
+ /**
820
+ * Covnert serialized INPUT and FIELD primitives back to hydrated block templates.
821
+ * Should be able to deserialize a format that has already been deserialized. The only
822
+ * "east" path to adding new targets/code requires going through deserialize, so it should
823
+ * work with pre-parsed deserialized blocks.
824
+ *
825
+ * @param {object} blocks Serialized SB3 "blocks" property of a target. Will be mutated.
826
+ * @return {object} input is modified and returned
827
+ */
828
+ const deserializeBlocks = function (blocks) {
829
+ for (const blockId in blocks) {
830
+ if (!Object.prototype.hasOwnProperty.call(blocks, blockId)) {
831
+ continue;
832
+ }
833
+ const block = blocks[blockId];
834
+ if (Array.isArray(block)) {
835
+ // this is one of the primitives
836
+ // delete the old entry in object.blocks and replace it w/the
837
+ // deserialized object
838
+ delete blocks[blockId];
839
+ deserializeInputDesc(block, null, false, blocks);
840
+ continue;
841
+ }
842
+ block.id = blockId; // add id back to block since it wasn't serialized
843
+ block.inputs = deserializeInputs(block.inputs, blockId, blocks);
844
+ block.fields = deserializeFields(block.fields);
845
+ }
846
+ return blocks;
847
+ };
848
+
849
+
850
+ /**
851
+ * Parse the assets of a single "Scratch object" and load them. This
852
+ * preprocesses objects to support loading the data for those assets over a
853
+ * network while the objects are further processed into Blocks, Sprites, and a
854
+ * list of needed Extensions.
855
+ * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
856
+ * @param {!Runtime} runtime Runtime object to load all structures into.
857
+ * @param {JSZip} zip Sb3 file describing this project (to load assets from)
858
+ * @return {?{costumePromises:Array.<Promise>,soundPromises:Array.<Promise>,soundBank:SoundBank}}
859
+ * Object of arrays of promises for asset objects used in Sprites. As well as a
860
+ * SoundBank for the sound assets. null for unsupported objects.
861
+ */
862
+ const parseScratchAssets = function (object, runtime, zip) {
863
+ if (!Object.prototype.hasOwnProperty.call(object, 'name')) {
864
+ // Watcher/monitor - skip this object until those are implemented in VM.
865
+ // @todo
866
+ return Promise.resolve(null);
867
+ }
868
+
869
+ const assets = {
870
+ costumePromises: null,
871
+ soundPromises: null,
872
+ soundBank: runtime.audioEngine && runtime.audioEngine.createBank()
873
+ };
874
+
875
+ // Costumes from JSON.
876
+ assets.costumePromises = (object.costumes || []).map(costumeSource => {
877
+ // @todo: Make sure all the relevant metadata is being pulled out.
878
+ const costume = {
879
+ // costumeSource only has an asset if an image is being uploaded as
880
+ // a sprite
881
+ asset: costumeSource.asset,
882
+ assetId: costumeSource.assetId,
883
+ skinId: null,
884
+ name: costumeSource.name,
885
+ bitmapResolution: costumeSource.bitmapResolution,
886
+ rotationCenterX: costumeSource.rotationCenterX,
887
+ rotationCenterY: costumeSource.rotationCenterY
888
+ };
889
+ const dataFormat =
890
+ costumeSource.dataFormat ||
891
+ (costumeSource.assetType && costumeSource.assetType.runtimeFormat) || // older format
892
+ 'png'; // if all else fails, guess that it might be a PNG
893
+ const costumeMd5Ext = Object.prototype.hasOwnProperty.call(costumeSource, 'md5ext') ?
894
+ costumeSource.md5ext : `${costumeSource.assetId}.${dataFormat}`;
895
+ costume.md5 = costumeMd5Ext;
896
+ costume.dataFormat = dataFormat;
897
+ // deserializeCostume should be called on the costume object we're
898
+ // creating above instead of the source costume object, because this way
899
+ // we're always loading the 'sb3' representation of the costume
900
+ // any translation that needs to happen will happen in the process
901
+ // of building up the costume object into an sb3 format
902
+ return deserializeCostume(costume, runtime, zip)
903
+ .then(() => loadCostume(costumeMd5Ext, costume, runtime));
904
+ // Only attempt to load the costume after the deserialization
905
+ // process has been completed
906
+ });
907
+ // Sounds from JSON
908
+ assets.soundPromises = (object.sounds || []).map(soundSource => {
909
+ const sound = {
910
+ assetId: soundSource.assetId,
911
+ format: soundSource.format,
912
+ rate: soundSource.rate,
913
+ sampleCount: soundSource.sampleCount,
914
+ name: soundSource.name,
915
+ // TODO we eventually want this property to be called md5ext,
916
+ // but there are many things relying on this particular name at the
917
+ // moment, so this translation is very important
918
+ md5: soundSource.md5ext,
919
+ dataFormat: soundSource.dataFormat,
920
+ data: null
921
+ };
922
+ // deserializeSound should be called on the sound object we're
923
+ // creating above instead of the source sound object, because this way
924
+ // we're always loading the 'sb3' representation of the costume
925
+ // any translation that needs to happen will happen in the process
926
+ // of building up the costume object into an sb3 format
927
+ return deserializeSound(sound, runtime, zip)
928
+ .then(() => loadSound(sound, runtime, assets.soundBank));
929
+ // Only attempt to load the sound after the deserialization
930
+ // process has been completed.
931
+ });
932
+
933
+ return assets;
934
+ };
935
+
936
+ /**
937
+ * Parse a single "Scratch object" and create all its in-memory VM objects.
938
+ * @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
939
+ * @param {!Runtime} runtime Runtime object to load all structures into.
940
+ * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
941
+ * @param {JSZip} zip Sb3 file describing this project (to load assets from)
942
+ * @param {object} assets - Promises for assets of this scratch object grouped
943
+ * into costumes and sounds
944
+ * @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
945
+ */
946
+ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
947
+ if (!Object.prototype.hasOwnProperty.call(object, 'name')) {
948
+ // Watcher/monitor - skip this object until those are implemented in VM.
949
+ // @todo
950
+ return Promise.resolve(null);
951
+ }
952
+ // Blocks container for this object.
953
+ const blocks = new Blocks(runtime);
954
+
955
+ // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
956
+ const sprite = new Sprite(blocks, runtime);
957
+
958
+ // Sprite/stage name from JSON.
959
+ if (Object.prototype.hasOwnProperty.call(object, 'name')) {
960
+ sprite.name = object.name;
961
+ }
962
+ if (Object.prototype.hasOwnProperty.call(object, 'blocks')) {
963
+ deserializeBlocks(object.blocks);
964
+ // Take a second pass to create objects and add extensions
965
+ for (const blockId in object.blocks) {
966
+ if (!Object.prototype.hasOwnProperty.call(object.blocks, blockId)) continue;
967
+ const blockJSON = object.blocks[blockId];
968
+ blocks.createBlock(blockJSON);
969
+
970
+ // If the block is from an extension, record it.
971
+ const extensionID = getExtensionIdForOpcode(blockJSON.opcode);
972
+ if (extensionID) {
973
+ extensions.extensionIDs.add(extensionID);
974
+ }
975
+ }
976
+ }
977
+ // Costumes from JSON.
978
+ const {costumePromises} = assets;
979
+ // Sounds from JSON
980
+ const {soundBank, soundPromises} = assets;
981
+ // Create the first clone, and load its run-state from JSON.
982
+ const target = sprite.createClone(object.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER);
983
+ // Load target properties from JSON.
984
+ if (Object.prototype.hasOwnProperty.call(object, 'tempo')) {
985
+ target.tempo = object.tempo;
986
+ }
987
+ if (Object.prototype.hasOwnProperty.call(object, 'volume')) {
988
+ target.volume = object.volume;
989
+ }
990
+ if (Object.prototype.hasOwnProperty.call(object, 'videoTransparency')) {
991
+ target.videoTransparency = object.videoTransparency;
992
+ }
993
+ if (Object.prototype.hasOwnProperty.call(object, 'videoState')) {
994
+ target.videoState = object.videoState;
995
+ }
996
+ if (Object.prototype.hasOwnProperty.call(object, 'textToSpeechLanguage')) {
997
+ target.textToSpeechLanguage = object.textToSpeechLanguage;
998
+ }
999
+ if (Object.prototype.hasOwnProperty.call(object, 'variables')) {
1000
+ for (const varId in object.variables) {
1001
+ const variable = object.variables[varId];
1002
+ // A variable is a cloud variable if:
1003
+ // - the project says it's a cloud variable, and
1004
+ // - it's a stage variable, and
1005
+ // - the runtime can support another cloud variable
1006
+ const isCloud = (variable.length === 3) && variable[2] &&
1007
+ object.isStage && runtime.canAddCloudVariable();
1008
+ const newVariable = new Variable(
1009
+ varId, // var id is the index of the variable desc array in the variables obj
1010
+ variable[0], // name of the variable
1011
+ Variable.SCALAR_TYPE, // type of the variable
1012
+ isCloud
1013
+ );
1014
+ if (isCloud) runtime.addCloudVariable();
1015
+ newVariable.value = variable[1];
1016
+ target.variables[newVariable.id] = newVariable;
1017
+ }
1018
+ }
1019
+ if (Object.prototype.hasOwnProperty.call(object, 'lists')) {
1020
+ for (const listId in object.lists) {
1021
+ const list = object.lists[listId];
1022
+ const newList = new Variable(
1023
+ listId,
1024
+ list[0],
1025
+ Variable.LIST_TYPE,
1026
+ false
1027
+ );
1028
+ newList.value = list[1];
1029
+ target.variables[newList.id] = newList;
1030
+ }
1031
+ }
1032
+ if (Object.prototype.hasOwnProperty.call(object, 'broadcasts')) {
1033
+ for (const broadcastId in object.broadcasts) {
1034
+ const broadcast = object.broadcasts[broadcastId];
1035
+ const newBroadcast = new Variable(
1036
+ broadcastId,
1037
+ broadcast,
1038
+ Variable.BROADCAST_MESSAGE_TYPE,
1039
+ false
1040
+ );
1041
+ // no need to explicitly set the value, variable constructor
1042
+ // sets the value to the same as the name for broadcast msgs
1043
+ target.variables[newBroadcast.id] = newBroadcast;
1044
+ }
1045
+ }
1046
+ if (Object.prototype.hasOwnProperty.call(object, 'comments')) {
1047
+ for (const commentId in object.comments) {
1048
+ const comment = object.comments[commentId];
1049
+ const newComment = new Comment(
1050
+ commentId,
1051
+ comment.text,
1052
+ comment.x,
1053
+ comment.y,
1054
+ comment.width,
1055
+ comment.height,
1056
+ comment.minimized
1057
+ );
1058
+ if (comment.blockId) {
1059
+ newComment.blockId = comment.blockId;
1060
+ }
1061
+ target.comments[newComment.id] = newComment;
1062
+ }
1063
+ }
1064
+ if (Object.prototype.hasOwnProperty.call(object, 'x')) {
1065
+ target.x = object.x;
1066
+ }
1067
+ if (Object.prototype.hasOwnProperty.call(object, 'y')) {
1068
+ target.y = object.y;
1069
+ }
1070
+ if (Object.prototype.hasOwnProperty.call(object, 'direction')) {
1071
+ // Sometimes the direction can be outside of the range: LLK/scratch-gui#5806
1072
+ // wrapClamp it (like we do on RenderedTarget.setDirection)
1073
+ target.direction = MathUtil.wrapClamp(object.direction, -179, 180);
1074
+ }
1075
+ if (Object.prototype.hasOwnProperty.call(object, 'size')) {
1076
+ target.size = object.size;
1077
+ }
1078
+ if (Object.prototype.hasOwnProperty.call(object, 'visible')) {
1079
+ target.visible = object.visible;
1080
+ }
1081
+ if (Object.prototype.hasOwnProperty.call(object, 'currentCostume')) {
1082
+ target.currentCostume = MathUtil.clamp(object.currentCostume, 0, object.costumes.length - 1);
1083
+ }
1084
+ if (Object.prototype.hasOwnProperty.call(object, 'rotationStyle')) {
1085
+ target.rotationStyle = object.rotationStyle;
1086
+ }
1087
+ if (Object.prototype.hasOwnProperty.call(object, 'isStage')) {
1088
+ target.isStage = object.isStage;
1089
+ }
1090
+ if (Object.prototype.hasOwnProperty.call(object, 'targetPaneOrder')) {
1091
+ // Temporarily store the 'targetPaneOrder' property
1092
+ // so that we can correctly order sprites in the target pane.
1093
+ // This will be deleted after we are done parsing and ordering the targets list.
1094
+ target.targetPaneOrder = object.targetPaneOrder;
1095
+ }
1096
+ if (Object.prototype.hasOwnProperty.call(object, 'draggable')) {
1097
+ target.draggable = object.draggable;
1098
+ }
1099
+ Promise.all(costumePromises).then(costumes => {
1100
+ sprite.costumes = costumes;
1101
+ });
1102
+ Promise.all(soundPromises).then(sounds => {
1103
+ sprite.sounds = sounds;
1104
+ // Make sure if soundBank is undefined, sprite.soundBank is then null.
1105
+ sprite.soundBank = soundBank || null;
1106
+ });
1107
+ return Promise.all(costumePromises.concat(soundPromises)).then(() => target);
1108
+ };
1109
+
1110
+ const deserializeMonitor = function (monitorData, runtime, targets, extensions) {
1111
+ // If the serialized monitor has spriteName defined, look up the sprite
1112
+ // by name in the given list of targets and update the monitor's targetId
1113
+ // to match the sprite's id.
1114
+ if (monitorData.spriteName) {
1115
+ const filteredTargets = targets.filter(t => t.sprite.name === monitorData.spriteName);
1116
+ if (filteredTargets && filteredTargets.length > 0) {
1117
+ monitorData.targetId = filteredTargets[0].id;
1118
+ } else {
1119
+ log.warn(`Tried to deserialize sprite specific monitor ${
1120
+ monitorData.opcode} but could not find sprite ${monitorData.spriteName}.`);
1121
+ }
1122
+ }
1123
+
1124
+ // Get information about this monitor, if it exists, given the monitor's opcode.
1125
+ // This will be undefined for extension blocks
1126
+ const monitorBlockInfo = runtime.monitorBlockInfo[monitorData.opcode];
1127
+
1128
+ // Due to a bug (see https://github.com/scratchfoundation/scratch-vm/pull/2322), renamed list monitors may have been serialized
1129
+ // with an outdated/incorrect LIST parameter. Fix it up to use the current name of the actual corresponding list.
1130
+ if (monitorData.opcode === 'data_listcontents') {
1131
+ const listTarget = monitorData.targetId ?
1132
+ targets.find(t => t.id === monitorData.targetId) :
1133
+ targets.find(t => t.isStage);
1134
+ if (
1135
+ listTarget &&
1136
+ Object.prototype.hasOwnProperty.call(listTarget.variables, monitorData.id)
1137
+ ) {
1138
+ monitorData.params.LIST = listTarget.variables[monitorData.id].name;
1139
+ }
1140
+ }
1141
+
1142
+ // Convert the serialized monitorData params into the block fields structure
1143
+ const fields = {};
1144
+ for (const paramKey in monitorData.params) {
1145
+ const field = {
1146
+ name: paramKey,
1147
+ value: monitorData.params[paramKey]
1148
+ };
1149
+ fields[paramKey] = field;
1150
+ }
1151
+
1152
+ // Variables, lists, and non-sprite-specific monitors, including any extension
1153
+ // monitors should already have the correct monitor ID serialized in the monitorData,
1154
+ // find the correct id for all other monitors.
1155
+ if (monitorData.opcode !== 'data_variable' && monitorData.opcode !== 'data_listcontents' &&
1156
+ monitorBlockInfo && monitorBlockInfo.isSpriteSpecific) {
1157
+ monitorData.id = monitorBlockInfo.getId(
1158
+ monitorData.targetId, fields);
1159
+ } else {
1160
+ // Replace unsafe characters in monitor ID, if there are any.
1161
+ // These would have come from projects that were originally 2.0 projects
1162
+ // that had unsafe characters in the variable name (and then the name was
1163
+ // used as part of the variable ID when importing the project).
1164
+ monitorData.id = StringUtil.replaceUnsafeChars(monitorData.id);
1165
+ }
1166
+
1167
+ // If the runtime already has a monitor block for this monitor's id,
1168
+ // update the existing block with the relevant monitor information.
1169
+ const existingMonitorBlock = runtime.monitorBlocks._blocks[monitorData.id];
1170
+ if (existingMonitorBlock) {
1171
+ // A monitor block already exists if the toolbox has been loaded and
1172
+ // the monitor block is not target specific (because the block gets recycled).
1173
+ existingMonitorBlock.isMonitored = monitorData.visible;
1174
+ existingMonitorBlock.targetId = monitorData.targetId;
1175
+ } else {
1176
+ // If a monitor block doesn't already exist for this monitor,
1177
+ // construct a monitor block to add to the monitor blocks container
1178
+ const monitorBlock = {
1179
+ id: monitorData.id,
1180
+ opcode: monitorData.opcode,
1181
+ inputs: {}, // Assuming that monitor blocks don't have droppable fields
1182
+ fields: fields,
1183
+ topLevel: true,
1184
+ next: null,
1185
+ parent: null,
1186
+ shadow: false,
1187
+ x: 0,
1188
+ y: 0,
1189
+ isMonitored: monitorData.visible,
1190
+ targetId: monitorData.targetId
1191
+ };
1192
+
1193
+ // Variables and lists have additional properties
1194
+ // stored in their fields, update this info in the
1195
+ // monitor block fields
1196
+ if (monitorData.opcode === 'data_variable') {
1197
+ const field = monitorBlock.fields.VARIABLE;
1198
+ field.id = monitorData.id;
1199
+ field.variableType = Variable.SCALAR_TYPE;
1200
+ } else if (monitorData.opcode === 'data_listcontents') {
1201
+ const field = monitorBlock.fields.LIST;
1202
+ field.id = monitorData.id;
1203
+ field.variableType = Variable.LIST_TYPE;
1204
+ }
1205
+
1206
+ runtime.monitorBlocks.createBlock(monitorBlock);
1207
+
1208
+ // If the block is from an extension, record it.
1209
+ const extensionID = getExtensionIdForOpcode(monitorBlock.opcode);
1210
+ if (extensionID) {
1211
+ extensions.extensionIDs.add(extensionID);
1212
+ }
1213
+ }
1214
+
1215
+ runtime.requestAddMonitor(MonitorRecord(monitorData));
1216
+ };
1217
+
1218
+ // Replace variable IDs throughout the project with
1219
+ // xml-safe versions.
1220
+ // This is to fix up projects imported from 2.0 where xml-unsafe names
1221
+ // were getting added to the variable ids.
1222
+ const replaceUnsafeCharsInVariableIds = function (targets) {
1223
+ const allVarRefs = VariableUtil.getAllVarRefsForTargets(targets, true);
1224
+ // Re-id the variables in the actual targets
1225
+ targets.forEach(t => {
1226
+ Object.keys(t.variables).forEach(id => {
1227
+ const newId = StringUtil.replaceUnsafeChars(id);
1228
+ if (newId === id) return;
1229
+ t.variables[id].id = newId;
1230
+ t.variables[newId] = t.variables[id];
1231
+ delete t.variables[id];
1232
+ });
1233
+ });
1234
+
1235
+ // Replace the IDs in the blocks refrencing variables or lists
1236
+ for (const id in allVarRefs) {
1237
+ const newId = StringUtil.replaceUnsafeChars(id);
1238
+ if (id === newId) continue; // ID was already safe, skip
1239
+ // We're calling this on the stage target because we need a
1240
+ // target to call on but this shouldn't matter because we're passing
1241
+ // in all the varRefs we want to operate on
1242
+ VariableUtil.updateVariableIdentifiers(allVarRefs[id], newId);
1243
+ }
1244
+ return targets;
1245
+ };
1246
+
1247
+ /**
1248
+ * Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
1249
+ * @param {object} json - JSON representation of a VM runtime.
1250
+ * @param {Runtime} runtime - Runtime instance
1251
+ * @param {JSZip} zip - Sb3 file describing this project (to load assets from)
1252
+ * @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project
1253
+ * @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
1254
+ */
1255
+ const deserialize = function (json, runtime, zip, isSingleSprite) {
1256
+ const extensions = {
1257
+ extensionIDs: new Set(),
1258
+ extensionURLs: new Map()
1259
+ };
1260
+
1261
+ // Store the origin field (e.g. project originated at CSFirst) so that we can save it again.
1262
+ if (json.meta && json.meta.origin) {
1263
+ runtime.origin = json.meta.origin;
1264
+ } else {
1265
+ runtime.origin = null;
1266
+ }
1267
+
1268
+ // First keep track of the current target order in the json,
1269
+ // then sort by the layer order property before parsing the targets
1270
+ // so that their corresponding render drawables can be created in
1271
+ // their layer order (e.g. back to front)
1272
+ const targetObjects = ((isSingleSprite ? [json] : json.targets) || [])
1273
+ .map((t, i) => Object.assign(t, {targetPaneOrder: i}))
1274
+ .sort((a, b) => a.layerOrder - b.layerOrder);
1275
+
1276
+ const monitorObjects = json.monitors || [];
1277
+
1278
+ return Promise.resolve(
1279
+ targetObjects.map(target =>
1280
+ parseScratchAssets(target, runtime, zip))
1281
+ )
1282
+ // Force this promise to wait for the next loop in the js tick. Let
1283
+ // storage have some time to send off asset requests.
1284
+ .then(assets => Promise.resolve(assets))
1285
+ .then(assets => Promise.all(targetObjects
1286
+ .map((target, index) =>
1287
+ parseScratchObject(target, runtime, extensions, zip, assets[index]))))
1288
+ .then(targets => targets // Re-sort targets back into original sprite-pane ordering
1289
+ .map((t, i) => {
1290
+ // Add layer order property to deserialized targets.
1291
+ // This property is used to initialize executable targets in
1292
+ // the correct order and is deleted in VM's installTargets function
1293
+ t.layerOrder = i;
1294
+ return t;
1295
+ })
1296
+ .sort((a, b) => a.targetPaneOrder - b.targetPaneOrder)
1297
+ .map(t => {
1298
+ // Delete the temporary properties used for
1299
+ // sprite pane ordering and stage layer ordering
1300
+ delete t.targetPaneOrder;
1301
+ return t;
1302
+ }))
1303
+ .then(targets => replaceUnsafeCharsInVariableIds(targets))
1304
+ .then(targets => {
1305
+ monitorObjects.map(monitorDesc => deserializeMonitor(monitorDesc, runtime, targets, extensions));
1306
+ return targets;
1307
+ })
1308
+ .then(targets => ({
1309
+ targets,
1310
+ extensions
1311
+ }));
1312
+ };
1313
+
1314
+ module.exports = {
1315
+ serialize: serialize,
1316
+ deserialize: deserialize,
1317
+ deserializeBlocks: deserializeBlocks,
1318
+ serializeBlocks: serializeBlocks,
1319
+ getExtensionIdForOpcode: getExtensionIdForOpcode
1320
+ };