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