2017-04-17 15:10:04 -04:00
|
|
|
const EventEmitter = require('events');
|
2017-08-04 14:25:17 -04:00
|
|
|
const {OrderedMap} = require('immutable');
|
|
|
|
|
|
|
|
const ArgumentType = require('../extension-support/argument-type');
|
2017-04-17 15:10:04 -04:00
|
|
|
const Blocks = require('./blocks');
|
2017-08-04 14:25:17 -04:00
|
|
|
const BlockType = require('../extension-support/block-type');
|
|
|
|
const Sequencer = require('./sequencer');
|
2017-04-17 15:10:04 -04:00
|
|
|
const Thread = require('./thread');
|
2016-04-18 18:03:18 -04:00
|
|
|
|
2016-08-15 21:37:36 -04:00
|
|
|
// Virtual I/O devices.
|
2017-04-17 15:10:04 -04:00
|
|
|
const Clock = require('../io/clock');
|
2017-04-28 16:40:13 -04:00
|
|
|
const DeviceManager = require('../io/deviceManager');
|
2017-04-17 15:10:04 -04:00
|
|
|
const Keyboard = require('../io/keyboard');
|
|
|
|
const Mouse = require('../io/mouse');
|
2016-08-15 21:37:36 -04:00
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
const defaultBlockPackages = {
|
2016-10-24 11:02:19 -04:00
|
|
|
scratch3_control: require('../blocks/scratch3_control'),
|
|
|
|
scratch3_event: require('../blocks/scratch3_event'),
|
|
|
|
scratch3_looks: require('../blocks/scratch3_looks'),
|
|
|
|
scratch3_motion: require('../blocks/scratch3_motion'),
|
|
|
|
scratch3_operators: require('../blocks/scratch3_operators'),
|
2017-01-19 14:26:38 -05:00
|
|
|
scratch3_pen: require('../blocks/scratch3_pen'),
|
2016-12-21 15:18:38 -05:00
|
|
|
scratch3_sound: require('../blocks/scratch3_sound'),
|
2016-10-24 11:02:19 -04:00
|
|
|
scratch3_sensing: require('../blocks/scratch3_sensing'),
|
|
|
|
scratch3_data: require('../blocks/scratch3_data'),
|
2017-05-03 19:39:31 -04:00
|
|
|
scratch3_procedures: require('../blocks/scratch3_procedures'),
|
|
|
|
scratch3_wedo2: require('../blocks/scratch3_wedo2')
|
2016-05-02 12:20:27 -04:00
|
|
|
};
|
|
|
|
|
2017-08-04 14:25:17 -04:00
|
|
|
/**
|
|
|
|
* Information used for converting Scratch argument types into scratch-blocks data.
|
|
|
|
* @type {object.<ArgumentType, {shadowType: string, fieldType: string}>}}
|
|
|
|
*/
|
|
|
|
const ArgumentTypeMap = (() => {
|
|
|
|
const map = {};
|
2017-10-06 16:37:59 -04:00
|
|
|
map[ArgumentType.ANGLE] = {
|
|
|
|
shadowType: 'math_angle',
|
|
|
|
fieldType: 'NUM'
|
|
|
|
};
|
2017-09-27 00:02:53 -04:00
|
|
|
map[ArgumentType.COLOR] = {
|
|
|
|
shadowType: 'colour_picker'
|
|
|
|
};
|
2017-08-04 14:25:17 -04:00
|
|
|
map[ArgumentType.NUMBER] = {
|
|
|
|
shadowType: 'math_number',
|
|
|
|
fieldType: 'NUM'
|
|
|
|
};
|
|
|
|
map[ArgumentType.STRING] = {
|
|
|
|
shadowType: 'text',
|
|
|
|
fieldType: 'TEXT'
|
|
|
|
};
|
|
|
|
map[ArgumentType.BOOLEAN] = {
|
2017-10-07 02:24:41 -04:00
|
|
|
check: 'Boolean'
|
2017-08-04 14:25:17 -04:00
|
|
|
};
|
|
|
|
return map;
|
|
|
|
})();
|
|
|
|
|
2017-09-06 03:24:24 -04:00
|
|
|
/**
|
|
|
|
* These constants are copied from scratch-blocks/core/constants.js
|
|
|
|
* @TODO find a way to require() these... maybe make a scratch-blocks/dist/constants.js or something like that?
|
|
|
|
* @readonly
|
|
|
|
* @enum {int}
|
|
|
|
*/
|
|
|
|
const ScratchBlocksConstants = {
|
|
|
|
/**
|
|
|
|
* ENUM for output shape: hexagonal (booleans/predicates).
|
|
|
|
* @const
|
|
|
|
*/
|
|
|
|
OUTPUT_SHAPE_HEXAGONAL: 1,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ENUM for output shape: rounded (numbers).
|
|
|
|
* @const
|
|
|
|
*/
|
|
|
|
OUTPUT_SHAPE_ROUND: 2,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ENUM for output shape: squared (any/all values; strings).
|
|
|
|
* @const
|
|
|
|
*/
|
|
|
|
OUTPUT_SHAPE_SQUARE: 3
|
|
|
|
};
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
/**
|
2016-08-11 11:11:27 -04:00
|
|
|
* Manages targets, scripts, and the sequencer.
|
2016-12-23 10:39:19 -05:00
|
|
|
* @constructor
|
2016-04-18 17:20:30 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
class Runtime extends EventEmitter {
|
|
|
|
constructor () {
|
|
|
|
super();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Target management and storage.
|
|
|
|
* @type {Array.<!Target>}
|
|
|
|
*/
|
|
|
|
this.targets = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of threads that are currently running in the VM.
|
|
|
|
* Threads are added when execution starts and pruned when execution ends.
|
|
|
|
* @type {Array.<Thread>}
|
|
|
|
*/
|
|
|
|
this.threads = [];
|
|
|
|
|
|
|
|
/** @type {!Sequencer} */
|
|
|
|
this.sequencer = new Sequencer(this);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Storage container for flyout blocks.
|
|
|
|
* These will execute on `_editingTarget.`
|
|
|
|
* @type {!Blocks}
|
|
|
|
*/
|
|
|
|
this.flyoutBlocks = new Blocks();
|
|
|
|
|
2017-05-08 09:53:16 -04:00
|
|
|
/**
|
|
|
|
* Storage container for monitor blocks.
|
|
|
|
* These will execute on a target maybe
|
|
|
|
* @type {!Blocks}
|
|
|
|
*/
|
|
|
|
this.monitorBlocks = new Blocks();
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Currently known editing target for the VM.
|
|
|
|
* @type {?Target}
|
|
|
|
*/
|
|
|
|
this._editingTarget = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Map to look up a block primitive's implementation function by its opcode.
|
|
|
|
* This is a two-step lookup: package name first, then primitive name.
|
|
|
|
* @type {Object.<string, Function>}
|
|
|
|
*/
|
|
|
|
this._primitives = {};
|
|
|
|
|
2017-08-04 14:25:17 -04:00
|
|
|
/**
|
|
|
|
* Map to look up all block information by extended opcode.
|
2017-08-25 16:53:57 -04:00
|
|
|
* @type {Array.<CategoryInfo>}
|
2017-08-04 14:25:17 -04:00
|
|
|
* @private
|
|
|
|
*/
|
2017-08-25 16:53:57 -04:00
|
|
|
this._blockInfo = [];
|
2017-08-04 14:25:17 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Map to look up hat blocks' metadata.
|
|
|
|
* Keys are opcode for hat, values are metadata objects.
|
|
|
|
* @type {Object.<string, Object>}
|
|
|
|
*/
|
|
|
|
this._hats = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Currently known values for edge-activated hats.
|
|
|
|
* Keys are block ID for the hat; values are the currently known values.
|
|
|
|
* @type {Object.<string, *>}
|
|
|
|
*/
|
|
|
|
this._edgeActivatedHatValues = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of script block IDs that were glowing during the previous frame.
|
|
|
|
* @type {!Array.<!string>}
|
|
|
|
*/
|
|
|
|
this._scriptGlowsPreviousFrame = [];
|
|
|
|
|
|
|
|
/**
|
2017-05-10 14:06:02 -04:00
|
|
|
* Number of non-monitor threads running during the previous frame.
|
2017-04-17 19:42:48 -04:00
|
|
|
* @type {number}
|
|
|
|
*/
|
2017-05-10 14:06:02 -04:00
|
|
|
this._nonMonitorThreadCount = 0;
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Currently known number of clones, used to enforce clone limit.
|
|
|
|
* @type {number}
|
|
|
|
*/
|
|
|
|
this._cloneCounter = 0;
|
|
|
|
|
2017-05-15 16:25:34 -04:00
|
|
|
/**
|
|
|
|
* Flag to emit a targets update at the end of a step. When target data
|
|
|
|
* changes, this flag is set to true.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
this._refreshTargets = false;
|
|
|
|
|
2017-05-11 15:23:11 -04:00
|
|
|
/**
|
2017-05-24 15:42:29 -04:00
|
|
|
* Ordered map of all monitors, which are MonitorReporter objects.
|
2017-05-11 15:23:11 -04:00
|
|
|
*/
|
2017-05-24 15:42:29 -04:00
|
|
|
this._monitorState = OrderedMap({});
|
2017-05-19 12:28:05 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Monitor state from last tick
|
|
|
|
*/
|
2017-05-24 15:42:29 -04:00
|
|
|
this._prevMonitorState = OrderedMap({});
|
2017-05-15 10:18:48 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Whether the project is in "turbo mode."
|
|
|
|
* @type {Boolean}
|
|
|
|
*/
|
|
|
|
this.turboMode = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the project is in "compatibility mode" (30 TPS).
|
|
|
|
* @type {Boolean}
|
|
|
|
*/
|
|
|
|
this.compatibilityMode = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A reference to the current runtime stepping interval, set
|
|
|
|
* by a `setInterval`.
|
|
|
|
* @type {!number}
|
|
|
|
*/
|
|
|
|
this._steppingInterval = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Current length of a step.
|
|
|
|
* Changes as mode switches, and used by the sequencer to calculate
|
|
|
|
* WORK_TIME.
|
|
|
|
* @type {!number}
|
|
|
|
*/
|
|
|
|
this.currentStepTime = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether any primitive has requested a redraw.
|
|
|
|
* Affects whether `Sequencer.stepThreads` will yield
|
|
|
|
* after stepping each thread.
|
|
|
|
* Reset on every frame.
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
this.redrawRequested = false;
|
|
|
|
|
|
|
|
// Register all given block packages.
|
|
|
|
this._registerBlockPackages();
|
|
|
|
|
|
|
|
// Register and initialize "IO devices", containers for processing
|
|
|
|
// I/O related data.
|
|
|
|
/** @type {Object.<string, Object>} */
|
|
|
|
this.ioDevices = {
|
|
|
|
clock: new Clock(),
|
2017-04-28 16:40:13 -04:00
|
|
|
deviceManager: new DeviceManager(),
|
2017-04-17 19:42:48 -04:00
|
|
|
keyboard: new Keyboard(this),
|
|
|
|
mouse: new Mouse(this)
|
|
|
|
};
|
|
|
|
}
|
2016-04-18 17:20:30 -04:00
|
|
|
|
2016-04-26 15:51:14 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Width of the stage, in pixels.
|
|
|
|
* @const {number}
|
2016-04-26 15:51:14 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get STAGE_WIDTH () {
|
|
|
|
return 480;
|
|
|
|
}
|
2016-04-26 14:20:44 -04:00
|
|
|
|
2016-04-26 15:51:14 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Height of the stage, in pixels.
|
|
|
|
* @const {number}
|
2016-04-26 15:51:14 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get STAGE_HEIGHT () {
|
|
|
|
return 360;
|
|
|
|
}
|
2016-05-02 12:20:27 -04:00
|
|
|
|
2016-10-17 23:23:16 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for glowing a script.
|
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get SCRIPT_GLOW_ON () {
|
|
|
|
return 'SCRIPT_GLOW_ON';
|
|
|
|
}
|
2016-10-13 17:15:49 -04:00
|
|
|
|
2016-10-17 23:23:16 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for unglowing a script.
|
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get SCRIPT_GLOW_OFF () {
|
|
|
|
return 'SCRIPT_GLOW_OFF';
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
2016-05-02 12:20:27 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for glowing a block.
|
|
|
|
* @const {string}
|
2016-05-02 12:20:27 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get BLOCK_GLOW_ON () {
|
|
|
|
return 'BLOCK_GLOW_ON';
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for unglowing a block.
|
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get BLOCK_GLOW_OFF () {
|
|
|
|
return 'BLOCK_GLOW_OFF';
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for glowing the green flag
|
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get PROJECT_RUN_START () {
|
|
|
|
return 'PROJECT_RUN_START';
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for unglowing the green flag
|
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get PROJECT_RUN_STOP () {
|
|
|
|
return 'PROJECT_RUN_STOP';
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
2016-11-23 15:47:49 -05:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Event name for visual value report.
|
|
|
|
* @const {string}
|
2016-11-23 15:47:49 -05:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get VISUAL_REPORT () {
|
|
|
|
return 'VISUAL_REPORT';
|
|
|
|
}
|
2016-11-23 15:47:49 -05:00
|
|
|
|
2016-10-17 23:23:16 -04:00
|
|
|
/**
|
2017-05-12 11:42:22 -04:00
|
|
|
* Event name for targets update report.
|
2017-04-17 19:42:48 -04:00
|
|
|
* @const {string}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-05-12 11:42:22 -04:00
|
|
|
static get TARGETS_UPDATE () {
|
|
|
|
return 'TARGETS_UPDATE';
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
2017-05-09 12:10:11 -04:00
|
|
|
/**
|
|
|
|
* Event name for monitors update.
|
|
|
|
* @const {string}
|
|
|
|
*/
|
|
|
|
static get MONITORS_UPDATE () {
|
|
|
|
return 'MONITORS_UPDATE';
|
|
|
|
}
|
|
|
|
|
2017-08-29 14:43:09 -04:00
|
|
|
/**
|
|
|
|
* Event name for reporting that an extension was added.
|
|
|
|
* @const {string}
|
|
|
|
*/
|
2017-09-04 14:06:31 -04:00
|
|
|
static get EXTENSION_ADDED () {
|
|
|
|
return 'EXTENSION_ADDED';
|
2017-08-29 14:43:09 -04:00
|
|
|
}
|
|
|
|
|
2016-10-17 23:23:16 -04:00
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* How rapidly we try to step threads by default, in ms.
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get THREAD_STEP_INTERVAL () {
|
|
|
|
return 1000 / 60;
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* In compatibility mode, how rapidly we try to step threads, in ms.
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get THREAD_STEP_INTERVAL_COMPATIBILITY () {
|
|
|
|
return 1000 / 30;
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* How many clones can be created at a time.
|
|
|
|
* @const {number}
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
static get MAX_CLONES () {
|
|
|
|
return 300;
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------------------
|
2016-10-17 23:23:16 -04:00
|
|
|
|
|
|
|
/**
|
2017-04-17 19:42:48 -04:00
|
|
|
* Register default block packages with this runtime.
|
|
|
|
* @todo Prefix opcodes with package name.
|
|
|
|
* @private
|
2016-10-17 23:23:16 -04:00
|
|
|
*/
|
2017-04-17 19:42:48 -04:00
|
|
|
_registerBlockPackages () {
|
|
|
|
for (const packageName in defaultBlockPackages) {
|
|
|
|
if (defaultBlockPackages.hasOwnProperty(packageName)) {
|
|
|
|
// @todo pass a different runtime depending on package privilege?
|
|
|
|
const packageObject = new (defaultBlockPackages[packageName])(this);
|
|
|
|
// Collect primitives from package.
|
|
|
|
if (packageObject.getPrimitives) {
|
|
|
|
const packagePrimitives = packageObject.getPrimitives();
|
|
|
|
for (const op in packagePrimitives) {
|
|
|
|
if (packagePrimitives.hasOwnProperty(op)) {
|
|
|
|
this._primitives[op] =
|
|
|
|
packagePrimitives[op].bind(packageObject);
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
// Collect hat metadata from package.
|
|
|
|
if (packageObject.getHats) {
|
|
|
|
const packageHats = packageObject.getHats();
|
|
|
|
for (const hatName in packageHats) {
|
|
|
|
if (packageHats.hasOwnProperty(hatName)) {
|
|
|
|
this._hats[hatName] = packageHats[hatName];
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
2016-05-02 12:20:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-06 16:37:59 -04:00
|
|
|
/**
|
|
|
|
* Generate an extension-specific menu ID.
|
|
|
|
* @param {string} menuName - the name of the menu.
|
|
|
|
* @param {string} extensionId - the ID of the extension hosting the menu.
|
|
|
|
* @returns {string} - the constructed ID.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_makeExtensionMenuId (menuName, extensionId) {
|
2017-10-07 02:24:41 -04:00
|
|
|
return `${extensionId}.menu.${escapeHtml(menuName)}`;
|
2017-10-06 16:37:59 -04:00
|
|
|
}
|
|
|
|
|
2017-08-04 14:25:17 -04:00
|
|
|
/**
|
|
|
|
* Register the primitives provided by an extension.
|
|
|
|
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_registerExtensionPrimitives (extensionInfo) {
|
|
|
|
const categoryInfo = {
|
|
|
|
id: extensionInfo.id,
|
|
|
|
name: extensionInfo.name,
|
|
|
|
color1: '#FF6680',
|
|
|
|
color2: '#FF4D6A',
|
2017-08-25 16:53:57 -04:00
|
|
|
color3: '#FF3355',
|
2017-10-06 16:37:59 -04:00
|
|
|
blocks: [],
|
|
|
|
menus: []
|
2017-08-04 14:25:17 -04:00
|
|
|
};
|
|
|
|
|
2017-08-25 16:53:57 -04:00
|
|
|
this._blockInfo.push(categoryInfo);
|
|
|
|
|
2017-10-06 16:37:59 -04:00
|
|
|
for (const menuName in extensionInfo.menus) {
|
|
|
|
if (extensionInfo.menus.hasOwnProperty(menuName)) {
|
|
|
|
const menuItems = extensionInfo.menus[menuName];
|
|
|
|
const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuItems, extensionInfo);
|
|
|
|
categoryInfo.menus.push(convertedMenu);
|
|
|
|
}
|
|
|
|
}
|
2017-08-04 14:25:17 -04:00
|
|
|
for (const blockInfo of extensionInfo.blocks) {
|
|
|
|
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
2017-08-29 14:43:09 -04:00
|
|
|
const opcode = convertedBlock.json.type;
|
2017-08-25 16:53:57 -04:00
|
|
|
categoryInfo.blocks.push(convertedBlock);
|
2017-08-04 14:25:17 -04:00
|
|
|
this._primitives[opcode] = convertedBlock.info.func;
|
|
|
|
}
|
2017-08-29 14:43:09 -04:00
|
|
|
|
2017-10-06 16:37:59 -04:00
|
|
|
this.emit(Runtime.EXTENSION_ADDED, categoryInfo.blocks.concat(categoryInfo.menus));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block.
|
|
|
|
* @param {string} menuName - the name of the menu
|
|
|
|
* @param {array|string} menuItems - the list of menu items, or the name of an extension method to collect them.
|
|
|
|
* @param {CategoryInfo} categoryInfo - the category for this block
|
|
|
|
* @returns {object} - a JSON-esque object ready for scratch-blocks' consumption
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_buildMenuForScratchBlocks (menuName, menuItems, categoryInfo) {
|
|
|
|
const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id);
|
|
|
|
|
|
|
|
/** @TODO: support dynamic menus when 'menuItems' is a method name string (see extension spec) */
|
|
|
|
const options = menuItems.map(item => {
|
|
|
|
switch (typeof item) {
|
|
|
|
case 'string':
|
|
|
|
return [item, item];
|
|
|
|
case 'object':
|
|
|
|
return [item.text, item.value];
|
|
|
|
default:
|
|
|
|
throw new Error(`Can't interpret menu item: ${item}`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
json: {
|
|
|
|
message0: '%1',
|
|
|
|
type: menuId,
|
|
|
|
inputsInline: true,
|
|
|
|
output: 'String',
|
|
|
|
colour: categoryInfo.color1,
|
|
|
|
colourSecondary: categoryInfo.color2,
|
|
|
|
colourTertiary: categoryInfo.color3,
|
|
|
|
outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND,
|
|
|
|
args0: [
|
|
|
|
{
|
|
|
|
type: 'field_dropdown',
|
|
|
|
name: menuName,
|
|
|
|
options: options
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
};
|
2017-08-04 14:25:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert BlockInfo into scratch-blocks JSON & XML, and generate a proxy function.
|
|
|
|
* @param {BlockInfo} blockInfo - the block to convert
|
|
|
|
* @param {CategoryInfo} categoryInfo - the category for this block
|
|
|
|
* @returns {{info: BlockInfo, json: object, xml: string}} - the converted & original block information
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_convertForScratchBlocks (blockInfo, categoryInfo) {
|
|
|
|
const extendedOpcode = `${categoryInfo.id}.${blockInfo.opcode}`;
|
|
|
|
const blockJSON = {
|
2017-08-29 14:43:09 -04:00
|
|
|
type: extendedOpcode,
|
2017-08-04 14:25:17 -04:00
|
|
|
inputsInline: true,
|
|
|
|
category: categoryInfo.name,
|
|
|
|
colour: categoryInfo.color1,
|
|
|
|
colourSecondary: categoryInfo.color2,
|
|
|
|
colorTertiary: categoryInfo.color3,
|
|
|
|
args0: []
|
|
|
|
};
|
|
|
|
|
|
|
|
const inputList = [];
|
|
|
|
|
|
|
|
// TODO: store this somewhere so that we can map args appropriately after translation.
|
|
|
|
// This maps an arg name to its relative position in the original (usually English) block text.
|
|
|
|
// When displaying a block in another language we'll need to run a `replace` action similar to the one below,
|
|
|
|
// but each `[ARG]` will need to be replaced with the number in this map instead of `args0.length`.
|
|
|
|
const argsMap = {};
|
|
|
|
|
|
|
|
blockJSON.message0 = blockInfo.text.replace(/\[(.+?)]/g, (match, placeholder) => {
|
|
|
|
|
|
|
|
// Sanitize the placeholder to ensure valid XML
|
|
|
|
placeholder = placeholder.replace(/[<"&]/, '_');
|
|
|
|
|
2017-10-07 02:24:41 -04:00
|
|
|
const argJSON = {
|
2017-08-04 14:25:17 -04:00
|
|
|
type: 'input_value',
|
|
|
|
name: placeholder
|
2017-10-07 02:24:41 -04:00
|
|
|
};
|
2017-08-04 14:25:17 -04:00
|
|
|
|
|
|
|
const argInfo = blockInfo.arguments[placeholder] || {};
|
|
|
|
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
|
|
|
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
|
2017-10-02 18:29:32 -04:00
|
|
|
|
2017-10-07 02:24:41 -04:00
|
|
|
if (argTypeInfo.check) {
|
|
|
|
argJSON.check = argTypeInfo.check;
|
|
|
|
}
|
|
|
|
|
2017-10-06 16:37:59 -04:00
|
|
|
const shadowType = (argInfo.menu ?
|
|
|
|
this._makeExtensionMenuId(argInfo.menu, categoryInfo.id) :
|
|
|
|
argTypeInfo.shadowType);
|
|
|
|
const fieldType = argInfo.menu || argTypeInfo.fieldType;
|
|
|
|
|
2017-10-02 18:29:32 -04:00
|
|
|
// <value> is the ScratchBlocks name for a block input.
|
2017-10-07 02:24:41 -04:00
|
|
|
inputList.push(`<value name="${placeholder}">`);
|
|
|
|
|
2017-10-02 18:29:32 -04:00
|
|
|
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
|
2017-10-07 02:24:41 -04:00
|
|
|
// Boolean inputs don't need to specify a shadow in the XML.
|
|
|
|
if (shadowType) {
|
|
|
|
inputList.push(`<shadow type="${shadowType}">`);
|
|
|
|
|
|
|
|
// <field> is a text field that the user can type into. Some shadows, like the color picker, don't allow
|
|
|
|
// text input and therefore don't need a field element.
|
|
|
|
if (fieldType) {
|
|
|
|
inputList.push(`<field name="${fieldType}">${defaultValue}</field>`);
|
|
|
|
}
|
2017-10-02 18:29:32 -04:00
|
|
|
|
2017-10-07 02:24:41 -04:00
|
|
|
inputList.push('</shadow>');
|
2017-09-27 00:02:53 -04:00
|
|
|
}
|
2017-10-02 18:29:32 -04:00
|
|
|
|
2017-10-07 02:24:41 -04:00
|
|
|
inputList.push('</value>');
|
2017-08-04 14:25:17 -04:00
|
|
|
|
2017-10-07 02:24:41 -04:00
|
|
|
// scratch-blocks uses 1-based argument indexing
|
|
|
|
blockJSON.args0.push(argJSON);
|
|
|
|
const argNum = blockJSON.args0.length;
|
|
|
|
argsMap[placeholder] = argNum;
|
2017-08-04 14:25:17 -04:00
|
|
|
return `%${argNum}`;
|
|
|
|
});
|
|
|
|
|
|
|
|
switch (blockInfo.blockType) {
|
|
|
|
case BlockType.COMMAND:
|
2017-09-06 03:24:24 -04:00
|
|
|
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
|
2017-08-29 14:43:09 -04:00
|
|
|
blockJSON.previousStatement = null; // null = available connection; undefined = hat
|
|
|
|
if (!blockInfo.isTerminal) {
|
|
|
|
blockJSON.nextStatement = null; // null = available connection; undefined = terminal
|
|
|
|
}
|
2017-08-04 14:25:17 -04:00
|
|
|
break;
|
|
|
|
case BlockType.REPORTER:
|
|
|
|
blockJSON.output = 'String'; // TODO: distinguish number & string here?
|
2017-09-06 03:24:24 -04:00
|
|
|
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_ROUND;
|
2017-08-04 14:25:17 -04:00
|
|
|
break;
|
|
|
|
case BlockType.BOOLEAN:
|
|
|
|
blockJSON.output = 'Boolean';
|
2017-09-06 03:24:24 -04:00
|
|
|
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL;
|
2017-08-04 14:25:17 -04:00
|
|
|
break;
|
|
|
|
case BlockType.HAT:
|
2017-09-06 03:24:24 -04:00
|
|
|
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
|
2017-08-29 14:43:09 -04:00
|
|
|
blockJSON.nextStatement = null; // null = available connection; undefined = terminal
|
2017-08-04 14:25:17 -04:00
|
|
|
break;
|
|
|
|
case BlockType.CONDITIONAL:
|
|
|
|
// Statement inputs get names like 'SUBSTACK', 'SUBSTACK2', 'SUBSTACK3', ...
|
|
|
|
for (let branchNum = 1; branchNum <= blockInfo.branchCount; ++branchNum) {
|
2017-09-04 18:37:13 -04:00
|
|
|
blockJSON[`message${branchNum}`] = '%1';
|
|
|
|
blockJSON[`args${branchNum}`] = [{
|
2017-08-04 14:25:17 -04:00
|
|
|
type: 'input_statement',
|
|
|
|
name: `SUBSTACK${branchNum > 1 ? branchNum : ''}`
|
2017-09-04 18:37:13 -04:00
|
|
|
}];
|
2017-08-04 14:25:17 -04:00
|
|
|
}
|
2017-09-06 03:24:24 -04:00
|
|
|
blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE;
|
2017-08-29 14:43:09 -04:00
|
|
|
blockJSON.previousStatement = null; // null = available connection; undefined = hat
|
|
|
|
blockJSON.nextStatement = null; // null = available connection; undefined = terminal
|
2017-08-04 14:25:17 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (blockInfo.isTerminal) {
|
|
|
|
delete blockJSON.nextStatement;
|
|
|
|
}
|
|
|
|
|
|
|
|
const blockXML = `<block type="${extendedOpcode}">${inputList.join('')}</block>`;
|
|
|
|
|
|
|
|
return {
|
|
|
|
info: blockInfo,
|
|
|
|
json: blockJSON,
|
|
|
|
xml: blockXML
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-08-25 16:53:57 -04:00
|
|
|
/**
|
|
|
|
* @returns {string} scratch-blocks XML description for all dynamic blocks, wrapped in <category> elements.
|
|
|
|
*/
|
|
|
|
getBlocksXML () {
|
|
|
|
const xmlParts = [];
|
|
|
|
for (const categoryInfo of this._blockInfo) {
|
|
|
|
const {name, color1, color2} = categoryInfo;
|
|
|
|
xmlParts.push(`<category name="${name}" colour="${color1}" secondaryColour="${color2}">`);
|
|
|
|
xmlParts.push.apply(xmlParts, categoryInfo.blocks.map(blockInfo => blockInfo.xml));
|
|
|
|
xmlParts.push('</category>');
|
|
|
|
}
|
|
|
|
return xmlParts.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns {Array.<string>} - an array containing the scratch-blocks JSON information for each dynamic block.
|
|
|
|
*/
|
|
|
|
getBlocksJSON () {
|
|
|
|
return this._blockInfo.reduce(
|
|
|
|
(result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []);
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Retrieve the function associated with the given opcode.
|
|
|
|
* @param {!string} opcode The opcode to look up.
|
|
|
|
* @return {Function} The function which implements the opcode.
|
|
|
|
*/
|
|
|
|
getOpcodeFunction (opcode) {
|
|
|
|
return this._primitives[opcode];
|
|
|
|
}
|
2016-05-02 12:20:27 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Return whether an opcode represents a hat block.
|
|
|
|
* @param {!string} opcode The opcode to look up.
|
|
|
|
* @return {boolean} True if the op is known to be a hat.
|
|
|
|
*/
|
|
|
|
getIsHat (opcode) {
|
|
|
|
return this._hats.hasOwnProperty(opcode);
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Return whether an opcode represents an edge-activated hat block.
|
|
|
|
* @param {!string} opcode The opcode to look up.
|
|
|
|
* @return {boolean} True if the op is known to be a edge-activated hat.
|
|
|
|
*/
|
|
|
|
getIsEdgeActivatedHat (opcode) {
|
|
|
|
return this._hats.hasOwnProperty(opcode) &&
|
|
|
|
this._hats[opcode].edgeActivated;
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Update an edge-activated hat block value.
|
|
|
|
* @param {!string} blockId ID of hat to store value for.
|
|
|
|
* @param {*} newValue Value to store for edge-activated hat.
|
|
|
|
* @return {*} The old value for the edge-activated hat.
|
|
|
|
*/
|
|
|
|
updateEdgeActivatedValue (blockId, newValue) {
|
|
|
|
const oldValue = this._edgeActivatedHatValues[blockId];
|
|
|
|
this._edgeActivatedHatValues[blockId] = newValue;
|
|
|
|
return oldValue;
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Clear all edge-activaed hat values.
|
|
|
|
*/
|
|
|
|
clearEdgeActivatedValues () {
|
|
|
|
this._edgeActivatedHatValues = {};
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Attach the audio engine
|
|
|
|
* @param {!AudioEngine} audioEngine The audio engine to attach
|
|
|
|
*/
|
|
|
|
attachAudioEngine (audioEngine) {
|
|
|
|
this.audioEngine = audioEngine;
|
|
|
|
}
|
2017-01-27 13:44:48 -05:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Attach the renderer
|
|
|
|
* @param {!RenderWebGL} renderer The renderer to attach
|
|
|
|
*/
|
|
|
|
attachRenderer (renderer) {
|
|
|
|
this.renderer = renderer;
|
|
|
|
}
|
2016-09-20 15:07:05 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Attach the storage module
|
|
|
|
* @param {!ScratchStorage} storage The storage module to attach
|
|
|
|
*/
|
|
|
|
attachStorage (storage) {
|
|
|
|
this.storage = storage;
|
|
|
|
}
|
2017-01-04 18:37:55 -05:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------------------
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Create a thread and push it to the list of threads.
|
|
|
|
* @param {!string} id ID of block that starts the stack.
|
|
|
|
* @param {!Target} target Target to run thread on.
|
2017-05-09 17:34:13 -04:00
|
|
|
* @param {?object} opts optional arguments
|
2017-07-12 15:43:13 -04:00
|
|
|
* @param {?boolean} opts.stackClick true if the script was activated by clicking on the stack
|
2017-05-15 10:12:25 -04:00
|
|
|
* @param {?boolean} opts.updateMonitor true if the script should update a monitor value
|
2017-04-17 19:42:48 -04:00
|
|
|
* @return {!Thread} The newly created thread.
|
|
|
|
*/
|
2017-05-09 17:34:13 -04:00
|
|
|
_pushThread (id, target, opts) {
|
|
|
|
opts = Object.assign({
|
2017-07-12 15:43:13 -04:00
|
|
|
stackClick: false,
|
2017-05-09 17:34:13 -04:00
|
|
|
updateMonitor: false
|
|
|
|
}, opts);
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
const thread = new Thread(id);
|
|
|
|
thread.target = target;
|
2017-07-12 15:43:13 -04:00
|
|
|
thread.stackClick = opts.stackClick;
|
2017-05-09 17:34:13 -04:00
|
|
|
thread.updateMonitor = opts.updateMonitor;
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
thread.pushStack(id);
|
|
|
|
this.threads.push(thread);
|
|
|
|
return thread;
|
|
|
|
}
|
2016-04-26 16:50:49 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Remove a thread from the list of threads.
|
|
|
|
* @param {?Thread} thread Thread object to remove from actives
|
|
|
|
*/
|
|
|
|
_removeThread (thread) {
|
|
|
|
// Inform sequencer to stop executing that thread.
|
|
|
|
this.sequencer.retireThread(thread);
|
|
|
|
// Remove from the list.
|
|
|
|
const i = this.threads.indexOf(thread);
|
|
|
|
if (i > -1) {
|
|
|
|
this.threads.splice(i, 1);
|
|
|
|
}
|
2016-05-02 13:09:38 -04:00
|
|
|
}
|
2016-04-26 16:50:49 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Restart a thread in place, maintaining its position in the list of threads.
|
|
|
|
* This is used by `startHats` to and is necessary to ensure 2.0-like execution order.
|
|
|
|
* Test project: https://scratch.mit.edu/projects/130183108/
|
|
|
|
* @param {!Thread} thread Thread object to restart.
|
|
|
|
*/
|
|
|
|
_restartThread (thread) {
|
|
|
|
const newThread = new Thread(thread.topBlock);
|
|
|
|
newThread.target = thread.target;
|
2017-07-12 15:43:13 -04:00
|
|
|
newThread.stackClick = thread.stackClick;
|
2017-05-11 17:11:06 -04:00
|
|
|
newThread.updateMonitor = thread.updateMonitor;
|
2017-04-17 19:42:48 -04:00
|
|
|
newThread.pushStack(thread.topBlock);
|
|
|
|
const i = this.threads.indexOf(thread);
|
|
|
|
if (i > -1) {
|
|
|
|
this.threads[i] = newThread;
|
|
|
|
} else {
|
|
|
|
this.threads.push(thread);
|
|
|
|
}
|
2016-11-10 15:05:49 -05:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Return whether a thread is currently active/running.
|
|
|
|
* @param {?Thread} thread Thread object to check.
|
|
|
|
* @return {boolean} True if the thread is active/running.
|
|
|
|
*/
|
|
|
|
isActiveThread (thread) {
|
|
|
|
return this.threads.indexOf(thread) > -1;
|
|
|
|
}
|
2016-08-29 10:01:31 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Toggle a script.
|
|
|
|
* @param {!string} topBlockId ID of block that starts the script.
|
2017-05-08 16:51:45 -04:00
|
|
|
* @param {?object} opts optional arguments to toggle script
|
|
|
|
* @param {?string} opts.target target ID for target to run script on. If not supplied, uses editing target.
|
2017-07-31 17:57:17 -04:00
|
|
|
* @param {?boolean} opts.stackClick true if the user activated the stack by clicking, false if not. This
|
|
|
|
* determines whether we show a visual report when turning on the script.
|
2017-04-17 19:42:48 -04:00
|
|
|
*/
|
2017-05-08 16:51:45 -04:00
|
|
|
toggleScript (topBlockId, opts) {
|
2017-05-09 17:34:13 -04:00
|
|
|
opts = Object.assign({
|
|
|
|
target: this._editingTarget,
|
2017-07-31 17:57:17 -04:00
|
|
|
stackClick: false
|
2017-05-09 17:34:13 -04:00
|
|
|
}, opts);
|
2017-04-17 19:42:48 -04:00
|
|
|
// Remove any existing thread.
|
|
|
|
for (let i = 0; i < this.threads.length; i++) {
|
2017-07-28 14:38:26 -04:00
|
|
|
// Toggling a script that's already running turns it off
|
|
|
|
if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) {
|
2017-07-12 15:43:13 -04:00
|
|
|
const blockContainer = opts.target.blocks;
|
|
|
|
const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId));
|
2017-08-29 14:43:09 -04:00
|
|
|
|
2017-07-12 15:43:13 -04:00
|
|
|
if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) {
|
|
|
|
// Allow edge activated hat thread stack click to coexist with
|
|
|
|
// edge activated hat thread that runs every frame
|
|
|
|
continue;
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
this._removeThread(this.threads[i]);
|
|
|
|
return;
|
|
|
|
}
|
2016-04-29 17:31:04 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
// Otherwise add it.
|
2017-05-09 17:34:13 -04:00
|
|
|
this._pushThread(topBlockId, opts.target, opts);
|
2016-04-29 17:31:04 -04:00
|
|
|
}
|
|
|
|
|
2017-07-31 17:57:17 -04:00
|
|
|
/**
|
|
|
|
* Enqueue a script that when finished will update the monitor for the block.
|
|
|
|
* @param {!string} topBlockId ID of block that starts the script.
|
|
|
|
* @param {?string} optTarget target ID for target to run script on. If not supplied, uses editing target.
|
|
|
|
*/
|
|
|
|
addMonitorScript (topBlockId, optTarget) {
|
|
|
|
if (!optTarget) optTarget = this._editingTarget;
|
|
|
|
for (let i = 0; i < this.threads.length; i++) {
|
|
|
|
// Don't re-add the script if it's already running
|
|
|
|
if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE &&
|
|
|
|
this.threads[i].updateMonitor) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Otherwise add it.
|
|
|
|
this._pushThread(topBlockId, optTarget, {updateMonitor: true});
|
|
|
|
}
|
2017-07-12 15:43:13 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Run a function `f` for all scripts in a workspace.
|
|
|
|
* `f` will be called with two parameters:
|
|
|
|
* - the top block ID of the script.
|
|
|
|
* - the target that owns the script.
|
|
|
|
* @param {!Function} f Function to call for each script.
|
|
|
|
* @param {Target=} optTarget Optionally, a target to restrict to.
|
|
|
|
*/
|
|
|
|
allScriptsDo (f, optTarget) {
|
|
|
|
let targets = this.targets;
|
|
|
|
if (optTarget) {
|
|
|
|
targets = [optTarget];
|
|
|
|
}
|
|
|
|
for (let t = targets.length - 1; t >= 0; t--) {
|
|
|
|
const target = targets[t];
|
|
|
|
const scripts = target.blocks.getScripts();
|
|
|
|
for (let j = 0; j < scripts.length; j++) {
|
|
|
|
const topBlockId = scripts[j];
|
|
|
|
f(topBlockId, target);
|
|
|
|
}
|
2016-05-03 17:39:02 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Start all relevant hats.
|
|
|
|
* @param {!string} requestedHatOpcode Opcode of hats to start.
|
|
|
|
* @param {object=} optMatchFields Optionally, fields to match on the hat.
|
|
|
|
* @param {Target=} optTarget Optionally, a target to restrict to.
|
|
|
|
* @return {Array.<Thread>} List of threads started by this function.
|
|
|
|
*/
|
|
|
|
startHats (requestedHatOpcode,
|
|
|
|
optMatchFields, optTarget) {
|
|
|
|
if (!this._hats.hasOwnProperty(requestedHatOpcode)) {
|
|
|
|
// No known hat with this opcode.
|
2016-08-23 18:12:32 -04:00
|
|
|
return;
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
const instance = this;
|
|
|
|
const newThreads = [];
|
2016-12-23 09:38:18 -05:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
for (const opts in optMatchFields) {
|
|
|
|
if (!optMatchFields.hasOwnProperty(opts)) continue;
|
|
|
|
optMatchFields[opts] = optMatchFields[opts].toUpperCase();
|
2016-12-23 09:38:18 -05:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
// Consider all scripts, looking for hats with opcode `requestedHatOpcode`.
|
|
|
|
this.allScriptsDo((topBlockId, target) => {
|
|
|
|
const blocks = target.blocks;
|
|
|
|
const block = blocks.getBlock(topBlockId);
|
|
|
|
const potentialHatOpcode = block.opcode;
|
|
|
|
if (potentialHatOpcode !== requestedHatOpcode) {
|
|
|
|
// Not the right hat.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Match any requested fields.
|
|
|
|
// For example: ensures that broadcasts match.
|
|
|
|
// This needs to happen before the block is evaluated
|
|
|
|
// (i.e., before the predicate can be run) because "broadcast and wait"
|
|
|
|
// needs to have a precise collection of started threads.
|
|
|
|
let hatFields = blocks.getFields(block);
|
|
|
|
|
|
|
|
// If no fields are present, check inputs (horizontal blocks)
|
|
|
|
if (Object.keys(hatFields).length === 0) {
|
|
|
|
const hatInputs = blocks.getInputs(block);
|
|
|
|
for (const input in hatInputs) {
|
|
|
|
if (!hatInputs.hasOwnProperty(input)) continue;
|
|
|
|
const id = hatInputs[input].block;
|
|
|
|
const inpBlock = blocks.getBlock(id);
|
|
|
|
const fields = blocks.getFields(inpBlock);
|
|
|
|
hatFields = Object.assign(fields, hatFields);
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
|
|
|
}
|
2016-12-23 09:38:18 -05:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
if (optMatchFields) {
|
|
|
|
for (const matchField in optMatchFields) {
|
|
|
|
if (hatFields[matchField].value.toUpperCase() !==
|
|
|
|
optMatchFields[matchField]) {
|
|
|
|
// Field mismatch.
|
|
|
|
return;
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
2016-08-29 10:18:49 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
// Look up metadata for the relevant hat.
|
|
|
|
const hatMeta = instance._hats[requestedHatOpcode];
|
|
|
|
if (hatMeta.restartExistingThreads) {
|
|
|
|
// If `restartExistingThreads` is true, we should stop
|
|
|
|
// any existing threads starting with the top block.
|
|
|
|
for (let i = 0; i < instance.threads.length; i++) {
|
|
|
|
if (instance.threads[i].topBlock === topBlockId &&
|
2017-07-31 17:57:17 -04:00
|
|
|
!instance.threads[i].stackClick && // stack click threads and hat threads can coexist
|
2017-04-17 19:42:48 -04:00
|
|
|
instance.threads[i].target === target) {
|
|
|
|
instance._restartThread(instance.threads[i]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If `restartExistingThreads` is false, we should
|
|
|
|
// give up if any threads with the top block are running.
|
|
|
|
for (let j = 0; j < instance.threads.length; j++) {
|
|
|
|
if (instance.threads[j].topBlock === topBlockId &&
|
2017-07-12 15:43:13 -04:00
|
|
|
instance.threads[j].target === target &&
|
2017-07-31 17:57:17 -04:00
|
|
|
!instance.threads[j].stackClick && // stack click threads and hat threads can coexist
|
|
|
|
instance.threads[j].status !== Thread.STATUS_DONE) {
|
2017-04-17 19:42:48 -04:00
|
|
|
// Some thread is already running.
|
|
|
|
return;
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
// Start the thread with this top block.
|
|
|
|
newThreads.push(instance._pushThread(topBlockId, target));
|
|
|
|
}, optTarget);
|
|
|
|
return newThreads;
|
|
|
|
}
|
2016-08-23 18:12:32 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Dispose all targets. Return to clean state.
|
|
|
|
*/
|
|
|
|
dispose () {
|
|
|
|
this.stopAll();
|
|
|
|
this.targets.map(this.disposeTarget, this);
|
|
|
|
}
|
2016-10-17 13:43:38 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Dispose of a target.
|
|
|
|
* @param {!Target} disposingTarget Target to dispose of.
|
|
|
|
*/
|
|
|
|
disposeTarget (disposingTarget) {
|
|
|
|
this.targets = this.targets.filter(target => {
|
|
|
|
if (disposingTarget !== target) return true;
|
|
|
|
// Allow target to do dispose actions.
|
|
|
|
target.dispose();
|
|
|
|
// Remove from list of targets.
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
}
|
2016-09-15 19:37:12 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Stop any threads acting on the target.
|
|
|
|
* @param {!Target} target Target to stop threads for.
|
|
|
|
* @param {Thread=} optThreadException Optional thread to skip.
|
|
|
|
*/
|
|
|
|
stopForTarget (target, optThreadException) {
|
|
|
|
// Stop any threads on the target.
|
|
|
|
for (let i = 0; i < this.threads.length; i++) {
|
|
|
|
if (this.threads[i] === optThreadException) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (this.threads[i].target === target) {
|
2017-05-12 02:01:37 -04:00
|
|
|
this.threads[i].isKilled = true;
|
2017-04-17 19:42:48 -04:00
|
|
|
this._removeThread(this.threads[i]);
|
|
|
|
}
|
2016-09-15 19:37:12 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Start all threads that start with the green flag.
|
|
|
|
*/
|
|
|
|
greenFlag () {
|
|
|
|
this.stopAll();
|
|
|
|
this.ioDevices.clock.resetProjectTimer();
|
|
|
|
this.clearEdgeActivatedValues();
|
|
|
|
// Inform all targets of the green flag.
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
this.targets[i].onGreenFlag();
|
2016-09-15 19:37:12 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
this.startHats('event_whenflagclicked');
|
2016-09-15 19:37:12 -04:00
|
|
|
}
|
2016-04-29 17:58:31 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Stop "everything."
|
|
|
|
*/
|
|
|
|
stopAll () {
|
|
|
|
// Dispose all clones.
|
|
|
|
const newTargets = [];
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
this.targets[i].onStopAll();
|
|
|
|
if (this.targets[i].hasOwnProperty('isOriginal') &&
|
|
|
|
!this.targets[i].isOriginal) {
|
|
|
|
this.targets[i].dispose();
|
|
|
|
} else {
|
|
|
|
newTargets.push(this.targets[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.targets = newTargets;
|
|
|
|
// Dispose all threads.
|
|
|
|
const threadsCopy = this.threads.slice();
|
|
|
|
while (threadsCopy.length > 0) {
|
|
|
|
const poppedThread = threadsCopy.pop();
|
|
|
|
this._removeThread(poppedThread);
|
2016-08-23 18:12:32 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Repeatedly run `sequencer.stepThreads` and filter out
|
|
|
|
* inactive threads after each iteration.
|
|
|
|
*/
|
|
|
|
_step () {
|
|
|
|
// Find all edge-activated hats, and add them to threads to be evaluated.
|
|
|
|
for (const hatType in this._hats) {
|
|
|
|
if (!this._hats.hasOwnProperty(hatType)) continue;
|
|
|
|
const hat = this._hats[hatType];
|
|
|
|
if (hat.edgeActivated) {
|
|
|
|
this.startHats(hatType);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.redrawRequested = false;
|
2017-05-08 09:53:16 -04:00
|
|
|
this._pushMonitors();
|
2017-04-17 19:42:48 -04:00
|
|
|
const doneThreads = this.sequencer.stepThreads();
|
|
|
|
this._updateGlows(doneThreads);
|
2017-05-10 14:06:02 -04:00
|
|
|
// Add done threads so that even if a thread finishes within 1 frame, the green
|
|
|
|
// flag will still indicate that a script ran.
|
2017-05-11 10:20:33 -04:00
|
|
|
this._emitProjectRunStatus(
|
|
|
|
this.threads.length + doneThreads.length -
|
|
|
|
this._getMonitorThreadCount([...this.threads, ...doneThreads]));
|
2017-04-17 19:42:48 -04:00
|
|
|
if (this.renderer) {
|
|
|
|
// @todo: Only render when this.redrawRequested or clones rendered.
|
|
|
|
this.renderer.draw();
|
|
|
|
}
|
2017-05-18 16:50:34 -04:00
|
|
|
|
2017-05-16 09:20:52 -04:00
|
|
|
if (this._refreshTargets) {
|
|
|
|
this.emit(Runtime.TARGETS_UPDATE);
|
|
|
|
this._refreshTargets = false;
|
|
|
|
}
|
2017-05-15 16:25:34 -04:00
|
|
|
|
2017-05-19 12:28:05 -04:00
|
|
|
if (!this._prevMonitorState.equals(this._monitorState)) {
|
2017-05-22 17:31:53 -04:00
|
|
|
this.emit(Runtime.MONITORS_UPDATE, this._monitorState);
|
|
|
|
this._prevMonitorState = this._monitorState;
|
2017-05-19 12:28:05 -04:00
|
|
|
}
|
2016-10-26 11:32:15 -04:00
|
|
|
}
|
2017-05-15 10:18:48 -04:00
|
|
|
|
2017-05-10 14:06:02 -04:00
|
|
|
/**
|
|
|
|
* Get the number of threads in the given array that are monitor threads (threads
|
|
|
|
* that update monitor values, and don't count as running a script).
|
|
|
|
* @param {!Array.<Thread>} threads The set of threads to look through.
|
|
|
|
* @return {number} The number of monitor threads in threads.
|
|
|
|
*/
|
|
|
|
_getMonitorThreadCount (threads) {
|
|
|
|
let count = 0;
|
|
|
|
threads.forEach(thread => {
|
|
|
|
if (thread.updateMonitor) count++;
|
|
|
|
});
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2017-05-08 09:53:16 -04:00
|
|
|
/**
|
|
|
|
* Queue monitor blocks to sequencer to be run.
|
|
|
|
*/
|
|
|
|
_pushMonitors () {
|
|
|
|
this.monitorBlocks.runAllMonitored(this);
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Set the current editing target known by the runtime.
|
|
|
|
* @param {!Target} editingTarget New editing target.
|
|
|
|
*/
|
|
|
|
setEditingTarget (editingTarget) {
|
|
|
|
this._editingTarget = editingTarget;
|
|
|
|
// Script glows must be cleared.
|
|
|
|
this._scriptGlowsPreviousFrame = [];
|
|
|
|
this._updateGlows();
|
2017-05-12 11:42:22 -04:00
|
|
|
this.requestTargetsUpdate(editingTarget);
|
2017-04-17 19:42:48 -04:00
|
|
|
}
|
2016-09-08 09:40:53 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Set whether we are in 30 TPS compatibility mode.
|
|
|
|
* @param {boolean} compatibilityModeOn True iff in compatibility mode.
|
|
|
|
*/
|
|
|
|
setCompatibilityMode (compatibilityModeOn) {
|
|
|
|
this.compatibilityMode = compatibilityModeOn;
|
|
|
|
if (this._steppingInterval) {
|
|
|
|
clearInterval(this._steppingInterval);
|
|
|
|
this.start();
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Emit glows/glow clears for scripts after a single tick.
|
|
|
|
* Looks at `this.threads` and notices which have turned on/off new glows.
|
|
|
|
* @param {Array.<Thread>=} optExtraThreads Optional list of inactive threads.
|
|
|
|
*/
|
|
|
|
_updateGlows (optExtraThreads) {
|
|
|
|
const searchThreads = [];
|
|
|
|
searchThreads.push.apply(searchThreads, this.threads);
|
|
|
|
if (optExtraThreads) {
|
|
|
|
searchThreads.push.apply(searchThreads, optExtraThreads);
|
|
|
|
}
|
|
|
|
// Set of scripts that request a glow this frame.
|
|
|
|
const requestedGlowsThisFrame = [];
|
|
|
|
// Final set of scripts glowing during this frame.
|
|
|
|
const finalScriptGlows = [];
|
|
|
|
// Find all scripts that should be glowing.
|
|
|
|
for (let i = 0; i < searchThreads.length; i++) {
|
|
|
|
const thread = searchThreads[i];
|
|
|
|
const target = thread.target;
|
|
|
|
if (target === this._editingTarget) {
|
|
|
|
const blockForThread = thread.blockGlowInFrame;
|
|
|
|
if (thread.requestScriptGlowInFrame) {
|
|
|
|
let script = target.blocks.getTopLevelScript(blockForThread);
|
|
|
|
if (!script) {
|
|
|
|
// Attempt to find in flyout blocks.
|
|
|
|
script = this.flyoutBlocks.getTopLevelScript(
|
|
|
|
blockForThread
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (script) {
|
|
|
|
requestedGlowsThisFrame.push(script);
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
}
|
2016-10-13 17:15:49 -04:00
|
|
|
}
|
2016-09-08 09:40:53 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
// Compare to previous frame.
|
|
|
|
for (let j = 0; j < this._scriptGlowsPreviousFrame.length; j++) {
|
|
|
|
const previousFrameGlow = this._scriptGlowsPreviousFrame[j];
|
|
|
|
if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) {
|
|
|
|
// Glow turned off.
|
|
|
|
this.glowScript(previousFrameGlow, false);
|
|
|
|
} else {
|
|
|
|
// Still glowing.
|
|
|
|
finalScriptGlows.push(previousFrameGlow);
|
|
|
|
}
|
2016-09-08 09:40:53 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
for (let k = 0; k < requestedGlowsThisFrame.length; k++) {
|
|
|
|
const currentFrameGlow = requestedGlowsThisFrame[k];
|
|
|
|
if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) {
|
|
|
|
// Glow turned on.
|
|
|
|
this.glowScript(currentFrameGlow, true);
|
|
|
|
finalScriptGlows.push(currentFrameGlow);
|
|
|
|
}
|
2016-09-08 09:40:53 -04:00
|
|
|
}
|
2017-04-17 19:42:48 -04:00
|
|
|
this._scriptGlowsPreviousFrame = finalScriptGlows;
|
2016-09-08 09:40:53 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Emit run start/stop after each tick. Emits when `this.threads.length` goes
|
|
|
|
* between non-zero and zero
|
|
|
|
*
|
2017-05-10 14:06:02 -04:00
|
|
|
* @param {number} nonMonitorThreadCount The new nonMonitorThreadCount
|
2017-04-17 19:42:48 -04:00
|
|
|
*/
|
2017-05-11 10:20:33 -04:00
|
|
|
_emitProjectRunStatus (nonMonitorThreadCount) {
|
2017-05-10 14:06:02 -04:00
|
|
|
if (this._nonMonitorThreadCount === 0 && nonMonitorThreadCount > 0) {
|
2017-04-17 19:42:48 -04:00
|
|
|
this.emit(Runtime.PROJECT_RUN_START);
|
|
|
|
}
|
2017-05-10 14:06:02 -04:00
|
|
|
if (this._nonMonitorThreadCount > 0 && nonMonitorThreadCount === 0) {
|
2017-04-17 19:42:48 -04:00
|
|
|
this.emit(Runtime.PROJECT_RUN_STOP);
|
|
|
|
}
|
2017-05-10 14:06:02 -04:00
|
|
|
this._nonMonitorThreadCount = nonMonitorThreadCount;
|
2016-11-23 15:47:49 -05:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* "Quiet" a script's glow: stop the VM from generating glow/unglow events
|
|
|
|
* about that script. Use when a script has just been deleted, but we may
|
|
|
|
* still be tracking glow data about it.
|
|
|
|
* @param {!string} scriptBlockId Id of top-level block in script to quiet.
|
|
|
|
*/
|
|
|
|
quietGlow (scriptBlockId) {
|
|
|
|
const index = this._scriptGlowsPreviousFrame.indexOf(scriptBlockId);
|
|
|
|
if (index > -1) {
|
|
|
|
this._scriptGlowsPreviousFrame.splice(index, 1);
|
|
|
|
}
|
2016-09-15 13:51:40 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Emit feedback for block glowing (used in the sequencer).
|
|
|
|
* @param {?string} blockId ID for the block to update glow
|
|
|
|
* @param {boolean} isGlowing True to turn on glow; false to turn off.
|
|
|
|
*/
|
|
|
|
glowBlock (blockId, isGlowing) {
|
|
|
|
if (isGlowing) {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_ON, {id: blockId});
|
|
|
|
} else {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_OFF, {id: blockId});
|
|
|
|
}
|
2016-05-02 18:09:02 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Emit feedback for script glowing.
|
|
|
|
* @param {?string} topBlockId ID for the top block to update glow
|
|
|
|
* @param {boolean} isGlowing True to turn on glow; false to turn off.
|
|
|
|
*/
|
|
|
|
glowScript (topBlockId, isGlowing) {
|
|
|
|
if (isGlowing) {
|
|
|
|
this.emit(Runtime.SCRIPT_GLOW_ON, {id: topBlockId});
|
|
|
|
} else {
|
|
|
|
this.emit(Runtime.SCRIPT_GLOW_OFF, {id: topBlockId});
|
|
|
|
}
|
2016-08-23 15:47:21 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Emit value for reporter to show in the blocks.
|
|
|
|
* @param {string} blockId ID for the block.
|
|
|
|
* @param {string} value Value to show associated with the block.
|
|
|
|
*/
|
|
|
|
visualReport (blockId, value) {
|
|
|
|
this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)});
|
|
|
|
}
|
2016-07-07 19:42:38 -04:00
|
|
|
|
2017-05-09 12:10:11 -04:00
|
|
|
/**
|
2017-05-11 15:23:11 -04:00
|
|
|
* Add a monitor to the state. If the monitor already exists in the state,
|
|
|
|
* overwrites it.
|
2017-05-24 15:42:29 -04:00
|
|
|
* @param {!MonitorRecord} monitor Monitor to add.
|
2017-05-09 12:10:11 -04:00
|
|
|
*/
|
2017-05-15 10:12:25 -04:00
|
|
|
requestAddMonitor (monitor) {
|
2017-05-24 15:48:11 -04:00
|
|
|
this._monitorState = this._monitorState.set(monitor.id, monitor);
|
2017-05-09 12:10:11 -04:00
|
|
|
}
|
|
|
|
|
2017-05-10 15:47:06 -04:00
|
|
|
/**
|
2017-05-11 15:23:11 -04:00
|
|
|
* Update a monitor in the state. Does nothing if the monitor does not already
|
|
|
|
* exist in the state.
|
2017-05-24 15:42:29 -04:00
|
|
|
* @param {!Map} monitor Monitor values to update. Values on the monitor with overwrite
|
|
|
|
* values on the old monitor with the same ID. If a value isn't defined on the new monitor,
|
|
|
|
* the old monitor will keep its old value.
|
2017-05-10 15:47:06 -04:00
|
|
|
*/
|
2017-05-15 10:12:25 -04:00
|
|
|
requestUpdateMonitor (monitor) {
|
2017-05-19 17:28:00 -04:00
|
|
|
if (this._monitorState.has(monitor.get('id'))) {
|
2017-05-19 12:28:05 -04:00
|
|
|
this._monitorState =
|
2017-05-19 17:28:00 -04:00
|
|
|
this._monitorState.set(monitor.get('id'), this._monitorState.get(monitor.get('id')).merge(monitor));
|
2017-05-19 12:28:05 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-10 17:00:08 -04:00
|
|
|
/**
|
2017-05-11 15:23:11 -04:00
|
|
|
* Removes a monitor from the state. Does nothing if the monitor already does
|
|
|
|
* not exist in the state.
|
2017-05-19 17:28:00 -04:00
|
|
|
* @param {!string} monitorId ID of the monitor to remove.
|
2017-05-10 17:00:08 -04:00
|
|
|
*/
|
2017-05-15 10:12:25 -04:00
|
|
|
requestRemoveMonitor (monitorId) {
|
2017-05-19 12:28:05 -04:00
|
|
|
this._monitorState = this._monitorState.delete(monitorId);
|
2017-05-10 17:00:08 -04:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Get a target by its id.
|
|
|
|
* @param {string} targetId Id of target to find.
|
|
|
|
* @return {?Target} The target, if found.
|
|
|
|
*/
|
|
|
|
getTargetById (targetId) {
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
const target = this.targets[i];
|
|
|
|
if (target.id === targetId) {
|
|
|
|
return target;
|
|
|
|
}
|
2016-06-29 13:48:30 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Get the first original (non-clone-block-created) sprite given a name.
|
|
|
|
* @param {string} spriteName Name of sprite to look for.
|
|
|
|
* @return {?Target} Target representing a sprite of the given name.
|
|
|
|
*/
|
|
|
|
getSpriteTargetByName (spriteName) {
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
const target = this.targets[i];
|
|
|
|
if (target.sprite && target.sprite.name === spriteName) {
|
|
|
|
return target;
|
|
|
|
}
|
2016-08-31 12:18:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Get a target by its drawable id.
|
|
|
|
* @param {number} drawableID drawable id of target to find
|
|
|
|
* @return {?Target} The target, if found
|
|
|
|
*/
|
|
|
|
getTargetByDrawableId (drawableID) {
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
const target = this.targets[i];
|
|
|
|
if (target.drawableID === drawableID) return target;
|
|
|
|
}
|
2017-03-03 09:35:57 -05:00
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Update the clone counter to track how many clones are created.
|
|
|
|
* @param {number} changeAmount How many clones have been created/destroyed.
|
|
|
|
*/
|
|
|
|
changeCloneCounter (changeAmount) {
|
|
|
|
this._cloneCounter += changeAmount;
|
|
|
|
}
|
2016-09-15 19:37:12 -04:00
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Return whether there are clones available.
|
|
|
|
* @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
|
|
|
|
*/
|
|
|
|
clonesAvailable () {
|
|
|
|
return this._cloneCounter < Runtime.MAX_CLONES;
|
|
|
|
}
|
2016-09-15 19:37:12 -04:00
|
|
|
|
2017-06-16 17:22:51 -04:00
|
|
|
/**
|
|
|
|
* Report that a new target has been created, possibly by cloning an existing target.
|
|
|
|
* @param {Target} newTarget - the newly created target.
|
|
|
|
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
|
|
|
|
* @fires Runtime#targetWasCreated
|
|
|
|
*/
|
|
|
|
fireTargetWasCreated (newTarget, sourceTarget) {
|
|
|
|
this.emit('targetWasCreated', newTarget, sourceTarget);
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Get a target representing the Scratch stage, if one exists.
|
|
|
|
* @return {?Target} The target, if found.
|
|
|
|
*/
|
|
|
|
getTargetForStage () {
|
|
|
|
for (let i = 0; i < this.targets.length; i++) {
|
|
|
|
const target = this.targets[i];
|
|
|
|
if (target.isStage) {
|
|
|
|
return target;
|
|
|
|
}
|
2016-09-08 09:40:27 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-27 19:15:07 -04:00
|
|
|
/**
|
|
|
|
* Get the editing target.
|
|
|
|
* @return {?Target} The editing target.
|
|
|
|
*/
|
|
|
|
getEditingTarget () {
|
|
|
|
return this._editingTarget;
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Tell the runtime to request a redraw.
|
|
|
|
* Use after a clone/sprite has completed some visible operation on the stage.
|
|
|
|
*/
|
|
|
|
requestRedraw () {
|
|
|
|
this.redrawRequested = true;
|
|
|
|
}
|
2016-10-17 23:23:16 -04:00
|
|
|
|
2017-05-12 11:42:22 -04:00
|
|
|
/**
|
|
|
|
* Emit a targets update at the end of the step if the provided target is
|
|
|
|
* the original sprite
|
|
|
|
* @param {!Target} target Target requesting the targets update
|
|
|
|
*/
|
|
|
|
requestTargetsUpdate (target) {
|
|
|
|
if (!target.isOriginal) return;
|
|
|
|
this._refreshTargets = true;
|
|
|
|
}
|
|
|
|
|
2017-04-17 19:42:48 -04:00
|
|
|
/**
|
|
|
|
* Set up timers to repeatedly step in a browser.
|
|
|
|
*/
|
|
|
|
start () {
|
|
|
|
let interval = Runtime.THREAD_STEP_INTERVAL;
|
|
|
|
if (this.compatibilityMode) {
|
|
|
|
interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
|
|
|
|
}
|
|
|
|
this.currentStepTime = interval;
|
|
|
|
this._steppingInterval = setInterval(() => {
|
|
|
|
this._step();
|
|
|
|
}, interval);
|
|
|
|
}
|
|
|
|
}
|
2016-04-26 16:50:49 -04:00
|
|
|
|
2017-06-16 17:22:51 -04:00
|
|
|
/**
|
|
|
|
* Event fired after a new target has been created, possibly by cloning an existing target.
|
|
|
|
*
|
|
|
|
* @event Runtime#targetWasCreated
|
|
|
|
* @param {Target} newTarget - the newly created target.
|
|
|
|
* @param {Target} [sourceTarget] - the target used as a source for the new clone, if any.
|
|
|
|
*/
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
module.exports = Runtime;
|