scratch-vm/src/engine/runtime.js

405 lines
12 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');
var defaultBlockPackages = {
'scratch3': require('../blocks/scratch3'),
'wedo2': require('../blocks/wedo2')
};
2016-04-18 17:20:30 -04:00
/**
* Manages blocks, stacks, and the sequencer.
2016-04-18 17:20:30 -04:00
*/
function Runtime () {
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
/**
* All blocks in the workspace.
* Keys are block IDs, values are metadata about the block.
* @type {Object.<string, Object>}
*/
2016-04-18 17:20:30 -04:00
this.blocks = {};
2016-04-26 15:51:14 -04:00
/**
* All stacks in the workspace.
* A list of block IDs that represent stacks (first block in stack).
* @type {Array.<String>}
*/
2016-04-18 17:20:30 -04:00
this.stacks = [];
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 = {};
this._registerBlockPackages();
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';
2016-04-18 18:03:18 -04:00
/**
* Inherit from EventEmitter
*/
util.inherits(Runtime, EventEmitter);
/**
* How rapidly we try to step threads, in ms.
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
/**
* Block management: create blocks and stacks from a `create` event
* @param {!Object} block Blockly create event to be processed
*/
Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
2016-04-18 17:20:30 -04:00
// Create new block
this.blocks[block.id] = block;
// Push block id to stacks array.
// Blocks are added as a top-level stack if they are marked as a topBlock
// (if they were top-level XML in the event) and if they are not
// flyout blocks.
if (!opt_isFlyoutBlock && block.topBlock) {
this._addStack(block.id);
}
};
/**
* Block management: change block field values
* @param {!Object} args Blockly change event to be processed
*/
Runtime.prototype.changeBlock = function (args) {
// Validate
if (args.element !== 'field') return;
if (typeof this.blocks[args.id] === 'undefined') return;
if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
// Update block value
this.blocks[args.id].fields[args.name].value = args.value;
2016-04-18 17:20:30 -04:00
};
/**
* Block management: move blocks from parent to parent
* @param {!Object} e Blockly move event to be processed
*/
2016-04-18 17:20:30 -04:00
Runtime.prototype.moveBlock = function (e) {
var _this = this;
// Remove from any old parent.
if (e.oldParent !== undefined) {
var oldParent = _this.blocks[e.oldParent];
if (e.oldInput !== undefined &&
oldParent.inputs[e.oldInput].block === e.id) {
// This block was connected to the old parent's input.
oldParent.inputs[e.oldInput].block = null;
} else if (oldParent.next === e.id) {
// This block was connected to the old parent's next connection.
oldParent.next = null;
}
}
// Has the block become a top-level block?
if (e.newParent === undefined) {
_this._addStack(e.id);
} else {
// Remove stack, if one exists.
2016-04-18 17:20:30 -04:00
_this._deleteStack(e.id);
// Otherwise, try to connect it in its new place.
if (e.newInput !== undefined) {
// Moved to the new parent's input.
_this.blocks[e.newParent].inputs[e.newInput] = {
name: e.newInput,
block: e.id
};
} else {
// Moved to the new parent's next connection.
_this.blocks[e.newParent].next = e.id;
2016-04-18 17:20:30 -04:00
}
}
};
/**
* Block management: delete blocks and their associated stacks
* @param {!Object} e Blockly delete event to be processed
*/
2016-04-18 17:20:30 -04:00
Runtime.prototype.deleteBlock = function (e) {
// @todo Stop threads running on this stack
// Get block
2016-04-18 17:20:30 -04:00
var block = this.blocks[e.id];
// Delete children
2016-04-18 17:20:30 -04:00
if (block.next !== null) {
this.deleteBlock({id: block.next});
}
// Delete inputs (including substacks)
for (var input in block.inputs) {
// If it's null, the block in this input moved away.
if (block.inputs[input].block !== null) {
this.deleteBlock({id: block.inputs[input].block});
}
2016-04-18 17:20:30 -04:00
}
// Delete stack
this._deleteStack(e.id);
// Delete block
delete this.blocks[e.id];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* 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);
var packageContents = packageObject.getPrimitives();
for (var op in packageContents) {
if (packageContents.hasOwnProperty(op)) {
this._primitives[op] =
packageContents[op].bind(packageObject);
}
}
}
}
};
/**
* 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];
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Create a thread and push it to the list of threads.
* @param {!string} id ID of block that starts the stack
*/
Runtime.prototype._pushThread = function (id) {
this.emit(Runtime.STACK_GLOW_ON, id);
var thread = new Thread(id);
this.threads.push(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.emit(Runtime.STACK_GLOW_OFF, thread.topBlock);
this.threads.splice(i, 1);
}
};
/**
* Toggle a stack
* @param {!string} stackId ID of block that starts the stack
*/
Runtime.prototype.toggleStack = function (stackId) {
// Remove any existing thread
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].topBlock == stackId) {
this._removeThread(this.threads[i]);
2016-04-29 17:39:18 -04:00
return;
}
}
2016-04-29 17:39:18 -04:00
// Otherwise add it
this._pushThread(stackId);
};
/**
* Green flag, which stops currently running threads
* and adds all top-level stacks that start with the green flag
*/
Runtime.prototype.greenFlag = function () {
// Remove all existing threads
for (var i = 0; i < this.threads.length; i++) {
this._removeThread(this.threads[i]);
}
// Add all top stacks with green flag
for (var j = 0; j < this.stacks.length; j++) {
var topBlock = this.stacks[j];
if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
this._pushThread(this.stacks[j]);
}
}
};
2016-05-03 17:39:02 -04:00
/**
* Distance sensor hack
*/
Runtime.prototype.startDistanceSensors = function () {
// Add all top stacks with distance sensor
for (var j = 0; j < this.stacks.length; j++) {
var topBlock = this.stacks[j];
if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') {
var alreadyRunning = false;
for (var k = 0; k < this.threads.length; k++) {
if (this.threads[k].topBlock === topBlock) {
alreadyRunning = true;
}
}
if (!alreadyRunning) {
this._pushThread(this.stacks[j]);
}
}
}
};
/**
* Stop "everything"
*/
Runtime.prototype.stopAll = function () {
var threadsCopy = this.threads.slice();
while (threadsCopy.length > 0) {
this._removeThread(threadsCopy.pop());
}
// @todo call stop function in all extensions/packages/WeDo stub
2016-05-03 17:18:28 -04:00
if (window.native) {
window.native.motorStop();
}
};
/**
* 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]);
}
};
/**
* 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);
}
};
/**
* Set up timers to repeatedly step in a browser
*/
Runtime.prototype.start = function () {
if (!window.setInterval) return;
window.setInterval(function() {
this._step();
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
};
// -----------------------------------------------------------------------------
2016-04-18 17:20:30 -04:00
// -----------------------------------------------------------------------------
/**
* Helper to add a stack to `this.stacks`
* @param {?string} id ID of block that starts the stack
*/
Runtime.prototype._addStack = function (id) {
var i = this.stacks.indexOf(id);
if (i > -1) return; // Already in stacks.
this.stacks.push(id);
// Update `topLevel` property on the top block.
this.blocks[id].topLevel = true;
};
/**
* Helper to remove a stack from `this.stacks`
* @param {?string} id ID of block that starts the stack
*/
2016-04-18 17:20:30 -04:00
Runtime.prototype._deleteStack = function (id) {
var i = this.stacks.indexOf(id);
if (i > -1) this.stacks.splice(i, 1);
// Update `topLevel` property on the top block.
if (this.blocks[id]) this.blocks[id].topLevel = false;
2016-04-18 17:20:30 -04:00
};
/**
* Helper to get the next block for a particular block
* @param {?string} id ID of block to get the next block for
* @return {?string} ID of next block in the sequence
*/
2016-04-18 17:20:30 -04:00
Runtime.prototype._getNextBlock = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].next;
};
/**
* Helper to get the substack for a particular C-shaped block
* @param {?string} id ID for block to get the substack for
* @return {?string} ID of block in the substack
*/
2016-04-18 17:20:30 -04:00
Runtime.prototype._getSubstack = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].fields['SUBSTACK'];
2016-04-18 17:20:30 -04:00
};
/**
* Helper to get the opcode for a particular block
* @param {?string} id ID of block to query
* @return {?string} the opcode corresponding to that block
*/
Runtime.prototype._getOpcode = function (id) {
if (typeof this.blocks[id] === 'undefined') return null;
return this.blocks[id].opcode;
};
2016-04-18 17:20:30 -04:00
module.exports = Runtime;