scratch-vm/src/engine/runtime.js

429 lines
13 KiB
JavaScript
Raw Normal View History

2016-04-18 18:03:18 -04:00
var EventEmitter = require('events');
var Sequencer = require('./sequencer');
var Thread = require('./thread');
2016-04-18 18:03:18 -04:00
var util = require('util');
// Virtual I/O devices.
var Clock = require('../io/clock');
var Mouse = require('../io/mouse');
var defaultBlockPackages = {
'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'),
'scratch3_sensing': require('../blocks/scratch3_sensing')
};
2016-04-18 17:20:30 -04:00
/**
* Manages targets, scripts, and the sequencer.
* @param {!Array.<Target>} targets List of targets for this runtime.
2016-04-18 17:20:30 -04:00
*/
function Runtime (targets) {
2016-04-18 18:03:18 -04:00
// Bind event emitter
EventEmitter.call(this);
2016-04-18 17:20:30 -04:00
// State for the runtime
2016-04-26 15:51:14 -04:00
/**
* Target management and storage.
2016-04-26 15:51:14 -04:00
*/
this.targets = targets;
2016-04-26 15:51:14 -04:00
/**
* 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);
/**
* 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 = {};
2016-08-23 18:12:32 -04:00
this._hats = {};
this._edgeTriggeredHatValues = {};
this._registerBlockPackages();
this.ioDevices = {
'clock': new Clock(),
'mouse': new Mouse()
};
2016-04-18 17:20:30 -04:00
}
/**
* Event name for glowing a stack
* @const {string}
*/
Runtime.STACK_GLOW_ON = 'STACK_GLOW_ON';
/**
* Event name for unglowing a stack
* @const {string}
*/
Runtime.STACK_GLOW_OFF = 'STACK_GLOW_OFF';
/**
* Event name for glowing a block
* @const {string}
*/
Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON';
/**
* Event name for unglowing a block
* @const {string}
*/
Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
/**
* Event name for visual value report.
* @const {string}
*/
Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
2016-04-18 18:03:18 -04:00
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* How rapidly we try to step threads, in ms.
*/
2016-07-01 16:30:33 -04:00
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
2016-04-18 17:20:30 -04:00
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Register default block packages with this runtime.
* @todo Prefix opcodes with package name.
* @private
*/
Runtime.prototype._registerBlockPackages = function () {
for (var packageName in defaultBlockPackages) {
if (defaultBlockPackages.hasOwnProperty(packageName)) {
// @todo pass a different runtime depending on package privilege?
var packageObject = new (defaultBlockPackages[packageName])(this);
2016-08-23 18:12:32 -04:00
// Collect primitives from package.
if (packageObject.getPrimitives) {
var packagePrimitives = packageObject.getPrimitives();
for (var op in packagePrimitives) {
if (packagePrimitives.hasOwnProperty(op)) {
this._primitives[op] =
packagePrimitives[op].bind(packageObject);
}
}
}
// Collect hat metadata from package.
if (packageObject.getHats) {
var packageHats = packageObject.getHats();
for (var hatName in packageHats) {
if (packageHats.hasOwnProperty(hatName)) {
this._hats[hatName] = packageHats[hatName];
}
}
}
}
}
};
/**
* Retrieve the function associated with the given opcode.
* @param {!string} opcode The opcode to look up.
* @return {Function} The function which implements the opcode.
*/
Runtime.prototype.getOpcodeFunction = function (opcode) {
return this._primitives[opcode];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
2016-08-23 18:12:32 -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.
*/
Runtime.prototype.getIsHat = function (opcode) {
return opcode in this._hats;
};
/**
* Return whether an opcode represents an edge-triggered hat block.
* @param {!string} opcode The opcode to look up.
* @return {Boolean} True if the op is known to be a edge-triggered hat.
*/
Runtime.prototype.getIsEdgeTriggeredHat = function (opcode) {
return opcode in this._hats && this._hats[opcode].edgeTriggered;
};
/**
* Update an edge-triggered hat block value.
* @param {!string} blockId ID of hat to store value for.
* @param {*} newValue Value to store for edge-triggered hat.
* @return {*} The old value for the edge-triggered hat.
*/
Runtime.prototype.updateEdgeTriggeredValue = function (blockId, newValue) {
var oldValue = this._edgeTriggeredHatValues[blockId];
this._edgeTriggeredHatValues[blockId] = newValue;
return oldValue;
};
/**
* Clear all edge-triggered hat values.
*/
Runtime.prototype.clearEdgeTriggeredValues = function () {
this._edgeTriggeredHatValues = {};
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Create a thread and push it to the list of threads.
* @param {!string} id ID of block that starts the stack
* @return {!Thread} The newly created thread.
*/
Runtime.prototype._pushThread = function (id) {
var thread = new Thread(id);
this.glowScript(id, true);
thread.pushStack(id);
this.threads.push(thread);
return thread;
};
/**
* Remove a thread from the list of threads.
* @param {?Thread} thread Thread object to remove from actives
*/
Runtime.prototype._removeThread = function (thread) {
var i = this.threads.indexOf(thread);
if (i > -1) {
this.glowScript(thread.topBlock, false);
this.threads.splice(i, 1);
}
};
/**
* Toggle a script.
* @param {!string} topBlockId ID of block that starts the script.
*/
Runtime.prototype.toggleScript = function (topBlockId) {
// Remove any existing thread.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].topBlock == topBlockId) {
this._removeThread(this.threads[i]);
2016-04-29 17:39:18 -04:00
return;
}
}
// Otherwise add it.
this._pushThread(topBlockId);
};
/**
2016-08-23 18:12:32 -04:00
* Run a function `f` for all scripts in a workspace.
* `f` will be called with two parameters:
* -the top block ID of each script
* -the opcode of that block, for convenience.
* -fields on that block, for convenience.
* @param {!Function} f Function to call for each script.
* @param {Target=} opt_target Optionally, a target to restrict to.
*/
2016-08-23 18:12:32 -04:00
Runtime.prototype.allScriptsDo = function (f, opt_target) {
var targets = this.targets;
if (opt_target) {
targets = [opt_target];
}
2016-08-23 18:12:32 -04:00
for (var t = 0; t < targets.length; t++) {
var target = targets[t];
var scripts = target.blocks.getScripts();
for (var j = 0; j < scripts.length; j++) {
var topBlock = scripts[j];
2016-08-23 18:12:32 -04:00
var topOpcode = target.blocks.getBlock(topBlock).opcode;
var topFields = target.blocks.getFields(topBlock);
f(topBlock, topOpcode, topFields);
2016-05-03 17:39:02 -04:00
}
}
};
2016-08-23 18:12:32 -04:00
/**
* Trigger all relevant hats.
* @param {!string} requestedHat Name of hat to trigger.
* @param {Object=} opt_matchFields Optionally, fields to match on the hat.
* @param {Target=} opt_target Optionally, a target to restrict to.
* @return {Array.<Thread>} List of threads started by this trigger.
*/
Runtime.prototype.triggerHats = function (requestedHat,
opt_matchFields, opt_target) {
var instance = this;
var newThreads = [];
// Consider all scripts, looking for hats named `requestedHat`.
this.allScriptsDo(function(topBlockId, topOpcode, topFields) {
if (topOpcode !== requestedHat) {
// 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 triggered threads.
if (opt_matchFields) {
for (var matchField in opt_matchFields) {
if (topFields[matchField].value !==
opt_matchFields[matchField]) {
// Field mismatch.
return;
}
}
}
if (instance._hats.hasOwnProperty(topOpcode)) {
// Look up metadata for the relevant hat.
var hatMeta = instance._hats[topOpcode];
if (hatMeta.restartExistingThreads) {
// If `restartExistingThreads` is true, this trigger
// should stop any existing threads starting with the top block.
for (var i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId) {
instance._removeThread(instance.threads[i]);
}
}
} else {
// If `restartExistingThreads` is false, this trigger
// should give up if any threads with the top block are running.
for (var j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId) {
// Some thread is already running.
return;
}
}
}
// Start the thread with this top block.
newThreads.push(instance._pushThread(topBlockId));
}
}, opt_target);
return newThreads;
};
/**
* Start all threads that start with the green flag.
*/
Runtime.prototype.greenFlag = function () {
this.ioDevices.clock.resetProjectTimer();
this.clearEdgeTriggeredValues();
this.triggerHats('event_whenflagclicked');
};
/**
* Stop "everything"
*/
Runtime.prototype.stopAll = function () {
var threadsCopy = this.threads.slice();
while (threadsCopy.length > 0) {
var poppedThread = threadsCopy.pop();
// Unglow any blocks on this thread's stack.
for (var i = 0; i < poppedThread.stack.length; i++) {
this.glowBlock(poppedThread.stack[i], false);
}
// Actually remove the thread.
this._removeThread(poppedThread);
}
};
/**
* Repeatedly run `sequencer.stepThreads` and filter out
* inactive threads after each iteration.
*/
Runtime.prototype._step = function () {
2016-08-23 18:12:32 -04:00
// Find all edge-triggered hats, and add them to threads to be evaluated.
for (var hatType in this._hats) {
var hat = this._hats[hatType];
if (hat.edgeTriggered) {
this.triggerHats(hatType);
}
}
var inactiveThreads = this.sequencer.stepThreads(this.threads);
2016-08-23 18:12:32 -04:00
for (var j = 0; j < inactiveThreads.length; j++) {
this._removeThread(inactiveThreads[j]);
}
};
/**
* 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.
*/
Runtime.prototype.glowBlock = function (blockId, isGlowing) {
if (isGlowing) {
this.emit(Runtime.BLOCK_GLOW_ON, blockId);
} else {
this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
}
};
/**
* 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.
*/
Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
if (isGlowing) {
this.emit(Runtime.STACK_GLOW_ON, topBlockId);
} else {
this.emit(Runtime.STACK_GLOW_OFF, topBlockId);
}
};
/**
* 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.
*/
Runtime.prototype.visualReport = function (blockId, value) {
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
};
/**
* Return the Target for a particular thread.
* @param {!Thread} thread Thread to determine target for.
* @return {?Target} Target object, if one exists.
*/
Runtime.prototype.targetForThread = function (thread) {
// @todo This is a messy solution,
// but prevents having circular data references.
// Have a map or some other way to associate target with threads.
for (var t = 0; t < this.targets.length; t++) {
var target = this.targets[t];
if (target.blocks.getBlock(thread.topBlock)) {
return target;
}
}
};
2016-06-21 14:29:40 -04:00
/**
* Handle an animation frame from the main thread.
2016-06-21 14:29:40 -04:00
*/
Runtime.prototype.animationFrame = function () {
if (self.renderer) {
self.renderer.draw();
2016-06-21 14:29:40 -04:00
}
};
/**
* Set up timers to repeatedly step in a browser
*/
Runtime.prototype.start = function () {
self.setInterval(function() {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
};
2016-04-18 17:20:30 -04:00
module.exports = Runtime;