@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.
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/dist/node/extension-worker.js +2 -0
- package/dist/node/extension-worker.js.map +1 -0
- package/dist/node/scratch-vm.js +3 -0
- package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
- package/dist/node/scratch-vm.js.map +1 -0
- package/dist/web/extension-worker.js +2 -0
- package/dist/web/extension-worker.js.map +1 -0
- package/dist/web/scratch-vm.js +3 -0
- package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
- package/dist/web/scratch-vm.js.map +1 -0
- package/package.json +108 -0
- package/src/.eslintrc.js +7 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- 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
|
+
};
|