2016-04-18 18:03:18 -04:00
|
|
|
var EventEmitter = require('events');
|
2016-04-26 14:20:44 -04:00
|
|
|
var Sequencer = require('./sequencer');
|
2016-04-26 16:50:49 -04:00
|
|
|
var Thread = require('./thread');
|
2016-04-18 18:03:18 -04:00
|
|
|
var util = require('util');
|
|
|
|
|
2016-08-15 21:37:36 -04:00
|
|
|
// Virtual I/O devices.
|
|
|
|
var Clock = require('../io/clock');
|
|
|
|
var Mouse = require('../io/mouse');
|
|
|
|
|
2016-05-02 12:20:27 -04:00
|
|
|
var defaultBlockPackages = {
|
2016-06-09 11:45:58 -04:00
|
|
|
'scratch3_control': require('../blocks/scratch3_control'),
|
|
|
|
'scratch3_event': require('../blocks/scratch3_event'),
|
2016-06-30 00:11:47 -04:00
|
|
|
'scratch3_looks': require('../blocks/scratch3_looks'),
|
2016-06-29 20:56:55 -04:00
|
|
|
'scratch3_motion': require('../blocks/scratch3_motion'),
|
2016-08-15 21:37:36 -04:00
|
|
|
'scratch3_operators': require('../blocks/scratch3_operators'),
|
|
|
|
'scratch3_sensing': require('../blocks/scratch3_sensing')
|
2016-05-02 12:20:27 -04:00
|
|
|
};
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
/**
|
2016-08-11 11:11:27 -04:00
|
|
|
* Manages targets, scripts, and the sequencer.
|
2016-06-29 13:48:30 -04:00
|
|
|
* @param {!Array.<Target>} targets List of targets for this runtime.
|
2016-04-18 17:20:30 -04:00
|
|
|
*/
|
2016-06-29 13:48:30 -04:00
|
|
|
function Runtime (targets) {
|
2016-04-18 18:03:18 -04:00
|
|
|
// Bind event emitter
|
2016-04-26 09:49:52 -04:00
|
|
|
EventEmitter.call(this);
|
2016-04-18 17:20:30 -04:00
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
// State for the runtime
|
2016-04-26 15:51:14 -04:00
|
|
|
|
|
|
|
/**
|
2016-06-29 13:48:30 -04:00
|
|
|
* Target management and storage.
|
2016-04-26 15:51:14 -04:00
|
|
|
*/
|
2016-06-29 13:48:30 -04:00
|
|
|
this.targets = targets;
|
2016-04-26 14:20:44 -04:00
|
|
|
|
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 = [];
|
|
|
|
|
2016-04-26 14:20:44 -04:00
|
|
|
/** @type {!Sequencer} */
|
2016-04-26 16:50:49 -04:00
|
|
|
this.sequencer = new Sequencer(this);
|
2016-05-02 12:20:27 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 = {};
|
|
|
|
this._registerBlockPackages();
|
2016-08-15 21:37:36 -04:00
|
|
|
|
|
|
|
this.ioDevices = {
|
|
|
|
'clock': new Clock(),
|
|
|
|
'mouse': new Mouse()
|
|
|
|
};
|
2016-04-18 17:20:30 -04:00
|
|
|
}
|
|
|
|
|
2016-05-02 13:09:38 -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';
|
|
|
|
|
2016-07-07 19:42:38 -04:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* 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-26 16:50:49 -04:00
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
2016-04-26 16:50:49 -04:00
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2016-05-02 12:20:27 -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)) {
|
2016-05-02 14:54:32 -04:00
|
|
|
// @todo pass a different runtime depending on package privilege?
|
2016-05-02 14:07:40 -04:00
|
|
|
var packageObject = new (defaultBlockPackages[packageName])(this);
|
2016-05-02 12:20:27 -04:00
|
|
|
var packageContents = packageObject.getPrimitives();
|
|
|
|
for (var op in packageContents) {
|
|
|
|
if (packageContents.hasOwnProperty(op)) {
|
2016-05-02 14:54:32 -04:00
|
|
|
this._primitives[op] =
|
|
|
|
packageContents[op].bind(packageObject);
|
2016-05-02 12:20:27 -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.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.getOpcodeFunction = function (opcode) {
|
|
|
|
return this._primitives[opcode];
|
|
|
|
};
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------------------
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Create a thread and push it to the list of threads.
|
|
|
|
* @param {!string} id ID of block that starts the stack
|
2016-08-23 15:47:21 -04:00
|
|
|
* @return {!Thread} The newly created thread.
|
2016-04-26 16:50:49 -04:00
|
|
|
*/
|
|
|
|
Runtime.prototype._pushThread = function (id) {
|
|
|
|
var thread = new Thread(id);
|
2016-08-23 15:47:21 -04:00
|
|
|
this.glowScript(id, true);
|
2016-06-09 17:08:30 -04:00
|
|
|
thread.pushStack(id);
|
2016-04-26 16:50:49 -04:00
|
|
|
this.threads.push(thread);
|
2016-08-23 15:47:21 -04:00
|
|
|
return thread;
|
2016-04-26 16:50:49 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove a thread from the list of threads.
|
2016-04-29 17:36:09 -04:00
|
|
|
* @param {?Thread} thread Thread object to remove from actives
|
2016-04-26 16:50:49 -04:00
|
|
|
*/
|
2016-04-29 17:36:09 -04:00
|
|
|
Runtime.prototype._removeThread = function (thread) {
|
|
|
|
var i = this.threads.indexOf(thread);
|
2016-05-02 13:09:38 -04:00
|
|
|
if (i > -1) {
|
2016-08-23 15:47:21 -04:00
|
|
|
this.glowScript(thread.topBlock, false);
|
2016-05-02 13:09:38 -04:00
|
|
|
this.threads.splice(i, 1);
|
|
|
|
}
|
2016-04-26 16:50:49 -04:00
|
|
|
};
|
|
|
|
|
2016-04-29 17:31:04 -04:00
|
|
|
/**
|
2016-08-11 11:11:27 -04:00
|
|
|
* Toggle a script.
|
|
|
|
* @param {!string} topBlockId ID of block that starts the script.
|
2016-04-29 17:31:04 -04:00
|
|
|
*/
|
2016-08-11 11:11:27 -04:00
|
|
|
Runtime.prototype.toggleScript = function (topBlockId) {
|
|
|
|
// Remove any existing thread.
|
2016-04-29 17:31:04 -04:00
|
|
|
for (var i = 0; i < this.threads.length; i++) {
|
2016-08-11 11:11:27 -04:00
|
|
|
if (this.threads[i].topBlock == topBlockId) {
|
2016-04-29 17:31:04 -04:00
|
|
|
this._removeThread(this.threads[i]);
|
2016-04-29 17:39:18 -04:00
|
|
|
return;
|
2016-04-29 17:31:04 -04:00
|
|
|
}
|
|
|
|
}
|
2016-08-11 11:11:27 -04:00
|
|
|
// Otherwise add it.
|
|
|
|
this._pushThread(topBlockId);
|
2016-04-29 17:31:04 -04:00
|
|
|
};
|
|
|
|
|
2016-04-29 17:58:31 -04:00
|
|
|
/**
|
|
|
|
* Green flag, which stops currently running threads
|
2016-08-11 11:11:27 -04:00
|
|
|
* and adds all top-level scripts that start with the green flag
|
2016-04-29 17:58:31 -04:00
|
|
|
*/
|
|
|
|
Runtime.prototype.greenFlag = function () {
|
|
|
|
// Remove all existing threads
|
|
|
|
for (var i = 0; i < this.threads.length; i++) {
|
|
|
|
this._removeThread(this.threads[i]);
|
|
|
|
}
|
2016-08-11 11:11:27 -04:00
|
|
|
// Add all top scripts with green flag
|
2016-06-29 13:48:30 -04:00
|
|
|
for (var t = 0; t < this.targets.length; t++) {
|
|
|
|
var target = this.targets[t];
|
2016-08-11 11:11:27 -04:00
|
|
|
var scripts = target.blocks.getScripts();
|
|
|
|
for (var j = 0; j < scripts.length; j++) {
|
|
|
|
var topBlock = scripts[j];
|
2016-06-29 13:48:30 -04:00
|
|
|
if (target.blocks.getBlock(topBlock).opcode ===
|
|
|
|
'event_whenflagclicked') {
|
2016-08-11 11:11:27 -04:00
|
|
|
this._pushThread(scripts[j]);
|
2016-05-03 17:39:02 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-29 17:58:31 -04:00
|
|
|
/**
|
|
|
|
* Stop "everything"
|
|
|
|
*/
|
|
|
|
Runtime.prototype.stopAll = function () {
|
2016-05-03 14:11:37 -04:00
|
|
|
var threadsCopy = this.threads.slice();
|
|
|
|
while (threadsCopy.length > 0) {
|
2016-06-07 20:59:34 -04:00
|
|
|
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);
|
2016-04-29 17:58:31 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Repeatedly run `sequencer.stepThreads` and filter out
|
|
|
|
* inactive threads after each iteration.
|
|
|
|
*/
|
|
|
|
Runtime.prototype._step = function () {
|
|
|
|
var inactiveThreads = this.sequencer.stepThreads(this.threads);
|
|
|
|
for (var i = 0; i < inactiveThreads.length; i++) {
|
|
|
|
this._removeThread(inactiveThreads[i]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-05-02 18:09:02 -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.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.glowBlock = function (blockId, isGlowing) {
|
|
|
|
if (isGlowing) {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_ON, blockId);
|
|
|
|
} else {
|
|
|
|
this.emit(Runtime.BLOCK_GLOW_OFF, blockId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-08-23 15:47:21 -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.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.glowScript = function (topBlockId, isGlowing) {
|
|
|
|
if (isGlowing) {
|
|
|
|
this.emit(Runtime.STACK_GLOW_ON, topBlockId);
|
|
|
|
} else {
|
|
|
|
this.emit(Runtime.STACK_GLOW_OFF, topBlockId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-07-07 19:42:38 -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.
|
|
|
|
*/
|
|
|
|
Runtime.prototype.visualReport = function (blockId, value) {
|
|
|
|
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
|
|
|
|
};
|
|
|
|
|
2016-06-29 13:48:30 -04:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
/**
|
2016-07-01 11:52:43 -04:00
|
|
|
* Handle an animation frame from the main thread.
|
2016-06-21 14:29:40 -04:00
|
|
|
*/
|
2016-07-01 11:52:43 -04:00
|
|
|
Runtime.prototype.animationFrame = function () {
|
|
|
|
if (self.renderer) {
|
|
|
|
self.renderer.draw();
|
2016-06-21 14:29:40 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-26 16:50:49 -04:00
|
|
|
/**
|
|
|
|
* Set up timers to repeatedly step in a browser
|
|
|
|
*/
|
|
|
|
Runtime.prototype.start = function () {
|
2016-07-01 11:52:43 -04:00
|
|
|
self.setInterval(function() {
|
2016-04-26 16:50:49 -04:00
|
|
|
this._step();
|
|
|
|
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
|
|
|
};
|
|
|
|
|
2016-04-18 17:20:30 -04:00
|
|
|
module.exports = Runtime;
|