diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 18a5ab621..71326d494 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -12,23 +12,72 @@ function Scratch3EventBlocks(runtime) { */ Scratch3EventBlocks.prototype.getPrimitives = function() { return { - 'event_whenflagclicked': this.whenFlagClicked, - 'event_whenbroadcastreceived': this.whenBroadcastReceived, - 'event_broadcast': this.broadcast + 'event_broadcast': this.broadcast, + 'event_broadcastandwait': this.broadcastAndWait, + 'event_whengreaterthan': this.hatGreaterThanPredicate }; }; - -Scratch3EventBlocks.prototype.whenFlagClicked = function() { - // No-op +Scratch3EventBlocks.prototype.getHats = function () { + return { + 'event_whenflagclicked': { + restartExistingThreads: true + }, + /*'event_whenkeypressed': { + restartExistingThreads: false + }, + 'event_whenthisspriteclicked': { + restartExistingThreads: true + }, + 'event_whenbackdropswitchesto': { + restartExistingThreads: true + },*/ + 'event_whengreaterthan': { + restartExistingThreads: false, + edgeActivated: true + }, + 'event_whenbroadcastreceived': { + restartExistingThreads: true + } + }; }; -Scratch3EventBlocks.prototype.whenBroadcastReceived = function() { - // No-op +Scratch3EventBlocks.prototype.hatGreaterThanPredicate = function (args, util) { + // @todo: Other cases :) + if (args.WHENGREATERTHANMENU == 'TIMER') { + return util.ioQuery('clock', 'projectTimer') > args.VALUE; + } + return false; }; -Scratch3EventBlocks.prototype.broadcast = function() { - // @todo +Scratch3EventBlocks.prototype.broadcast = function(args, util) { + util.startHats('event_whenbroadcastreceived', { + 'BROADCAST_OPTION': args.BROADCAST_OPTION + }); +}; + +Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) { + // Have we run before, starting threads? + if (!util.stackFrame.startedThreads) { + // No - start hats for this broadcast. + util.stackFrame.startedThreads = util.startHats( + 'event_whenbroadcastreceived', { + 'BROADCAST_OPTION': args.BROADCAST_OPTION + } + ); + if (util.stackFrame.startedThreads.length == 0) { + // Nothing was started. + return; + } + } + // We've run before; check if the wait is still going on. + var instance = this; + var waiting = util.stackFrame.startedThreads.some(function(thread) { + return instance.runtime.isActiveThread(thread); + }); + if (waiting) { + util.yieldFrame(); + } }; module.exports = Scratch3EventBlocks; diff --git a/src/engine/execute.js b/src/engine/execute.js index d885a6c58..5efe9bebd 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -1,5 +1,14 @@ var Thread = require('./thread'); +/** + * Utility function to determine if a value is a Promise. + * @param {*} value Value to check for a Promise. + * @return {Boolean} True if the value appears to be a Promise. + */ +var isPromise = function (value) { + return value && value.then && typeof value.then === 'function'; +}; + /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. @@ -21,8 +30,17 @@ var execute = function (sequencer, thread) { } var blockFunction = runtime.getOpcodeFunction(opcode); + var isHat = runtime.getIsHat(opcode); + + // Hats are implemented slightly differently from regular blocks. + // If they have an associated block function, it's treated as a predicate; + // if not, execution will proceed right through it (as a no-op). if (!blockFunction) { - console.warn('Could not get implementation for opcode: ' + opcode); + if (!isHat) { + console.warn('Could not get implementation for opcode: ' + opcode); + } + // Skip through the block. + // (either hat with no predicate, or missing op). return; } @@ -63,6 +81,8 @@ var execute = function (sequencer, thread) { var primitiveReportedValue = null; primitiveReportedValue = blockFunction(argValues, { + stackFrame: currentStackFrame.executionContext, + target: target, yield: function() { thread.setStatus(Thread.STATUS_YIELD); }, @@ -73,11 +93,14 @@ var execute = function (sequencer, thread) { thread.setStatus(Thread.STATUS_RUNNING); sequencer.proceedThread(thread); }, - stackFrame: currentStackFrame.executionContext, startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, - target: target, + startHats: function(requestedHat, opt_matchFields, opt_target) { + return ( + runtime.startHats(requestedHat, opt_matchFields, opt_target) + ); + }, ioQuery: function (device, func, args) { // Find the I/O device and execute the query/function call. if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) { @@ -87,28 +110,53 @@ var execute = function (sequencer, thread) { } }); - // Deal with any reported value. + /** + * Handle any reported value from the primitive, either directly returned + * or after a promise resolves. + * @param {*} resolvedValue Value eventually returned from the primitive. + */ + var handleReport = function (resolvedValue) { + thread.pushReportedValue(resolvedValue); + if (isHat) { + // Hat predicate was evaluated. + if (runtime.getIsEdgeActivatedHat(opcode)) { + // If this is an edge-activated hat, only proceed if + // the value is true and used to be false. + var oldEdgeValue = runtime.updateEdgeActivatedValue( + currentBlockId, + resolvedValue + ); + var edgeWasActivated = !oldEdgeValue && resolvedValue; + if (!edgeWasActivated) { + sequencer.retireThread(thread); + } + } else { + // Not an edge-activated hat: retire the thread + // if predicate was false. + if (!resolvedValue) { + sequencer.retireThread(thread); + } + } + } else { + // In a non-hat, report the value visually if necessary if + // at the top of the thread stack. + if (typeof resolvedValue !== 'undefined' && thread.atStackTop()) { + runtime.visualReport(currentBlockId, resolvedValue); + } + // Finished any yields. + thread.setStatus(Thread.STATUS_RUNNING); + } + }; + // If it's a promise, wait until promise resolves. - var isPromise = ( - primitiveReportedValue && - primitiveReportedValue.then && - typeof primitiveReportedValue.then === 'function' - ); - if (isPromise) { + if (isPromise(primitiveReportedValue)) { if (thread.status === Thread.STATUS_RUNNING) { // Primitive returned a promise; automatically yield thread. thread.setStatus(Thread.STATUS_YIELD); } // Promise handlers primitiveReportedValue.then(function(resolvedValue) { - // Promise resolved: the primitive reported a value. - thread.pushReportedValue(resolvedValue); - // Report the value visually if necessary. - if (typeof resolvedValue !== 'undefined' && - thread.peekStack() === thread.topBlock) { - runtime.visualReport(thread.peekStack(), resolvedValue); - } - thread.setStatus(Thread.STATUS_RUNNING); + handleReport(resolvedValue); sequencer.proceedThread(thread); }, function(rejectionReason) { // Promise rejected: the primitive had some error. @@ -118,12 +166,7 @@ var execute = function (sequencer, thread) { sequencer.proceedThread(thread); }); } else if (thread.status === Thread.STATUS_RUNNING) { - thread.pushReportedValue(primitiveReportedValue); - // Report the value visually if necessary. - if (typeof primitiveReportedValue !== 'undefined' && - thread.peekStack() === thread.topBlock) { - runtime.visualReport(thread.peekStack(), primitiveReportedValue); - } + handleReport(primitiveReportedValue); } }; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index d244544aa..921839497 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -47,6 +47,8 @@ function Runtime (targets) { * @type {Object.} */ this._primitives = {}; + this._hats = {}; + this._edgeActivatedHatValues = {}; this._registerBlockPackages(); this.ioDevices = { @@ -109,11 +111,23 @@ Runtime.prototype._registerBlockPackages = function () { 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); + // 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]; + } } } } @@ -132,15 +146,58 @@ Runtime.prototype.getOpcodeFunction = function (opcode) { // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- +/** + * 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 this._hats.hasOwnProperty(opcode); +}; + +/** + * 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. + */ +Runtime.prototype.getIsEdgeActivatedHat = function (opcode) { + return this._hats.hasOwnProperty(opcode) && + this._hats[opcode].edgeActivated; +}; + +/** + * 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. + */ +Runtime.prototype.updateEdgeActivatedValue = function (blockId, newValue) { + var oldValue = this._edgeActivatedHatValues[blockId]; + this._edgeActivatedHatValues[blockId] = newValue; + return oldValue; +}; + +/** + * Clear all edge-activaed hat values. + */ +Runtime.prototype.clearEdgeActivatedValues = function () { + this._edgeActivatedHatValues = {}; +}; + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- + /** * 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) { - this.emit(Runtime.STACK_GLOW_ON, id); var thread = new Thread(id); + this.glowScript(id, true); thread.pushStack(id); this.threads.push(thread); + return thread; }; /** @@ -150,11 +207,20 @@ Runtime.prototype._pushThread = function (id) { Runtime.prototype._removeThread = function (thread) { var i = this.threads.indexOf(thread); if (i > -1) { - this.emit(Runtime.STACK_GLOW_OFF, thread.topBlock); + this.glowScript(thread.topBlock, false); this.threads.splice(i, 1); } }; +/** + * Return whether a thread is currently active/running. + * @param {?Thread} thread Thread object to check. + * @return {Boolean} True if the thread is active/running. + */ +Runtime.prototype.isActiveThread = function (thread) { + return this.threads.indexOf(thread) > -1; +}; + /** * Toggle a script. * @param {!string} topBlockId ID of block that starts the script. @@ -172,28 +238,100 @@ Runtime.prototype.toggleScript = function (topBlockId) { }; /** - * Green flag, which stops currently running threads - * and adds all top-level scripts that start with the green flag + * 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=} opt_target Optionally, a target to restrict to. */ -Runtime.prototype.greenFlag = function () { - // Remove all existing threads - for (var i = 0; i < this.threads.length; i++) { - this._removeThread(this.threads[i]); +Runtime.prototype.allScriptsDo = function (f, opt_target) { + var targets = this.targets; + if (opt_target) { + targets = [opt_target]; } - // Add all top scripts with green flag - for (var t = 0; t < this.targets.length; t++) { - var target = this.targets[t]; + 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]; - if (target.blocks.getBlock(topBlock).opcode === - 'event_whenflagclicked') { - this._pushThread(scripts[j]); - } + var topBlockId = scripts[j]; + f(topBlockId, target); } } }; +/** + * Start all relevant hats. + * @param {!string} requestedHatOpcode Opcode of hats to start. + * @param {Object=} opt_matchFields Optionally, fields to match on the hat. + * @param {Target=} opt_target Optionally, a target to restrict to. + * @return {Array.} List of threads started by this function. + */ +Runtime.prototype.startHats = function (requestedHatOpcode, + opt_matchFields, opt_target) { + if (!this._hats.hasOwnProperty(requestedHatOpcode)) { + // No known hat with this opcode. + return; + } + var instance = this; + var newThreads = []; + // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. + this.allScriptsDo(function(topBlockId, target) { + var potentialHatOpcode = target.blocks.getBlock(topBlockId).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. + var hatFields = target.blocks.getFields(topBlockId); + if (opt_matchFields) { + for (var matchField in opt_matchFields) { + if (hatFields[matchField].value !== + opt_matchFields[matchField]) { + // Field mismatch. + return; + } + } + } + // Look up metadata for the relevant hat. + var hatMeta = instance._hats[requestedHatOpcode]; + if (hatMeta.restartExistingThreads) { + // If `restartExistingThreads` is true, we 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, we 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.clearEdgeActivatedValues(); + this.startHats('event_whenflagclicked'); +}; + /** * Stop "everything" */ @@ -215,6 +353,13 @@ Runtime.prototype.stopAll = function () { * inactive threads after each iteration. */ Runtime.prototype._step = function () { + // Find all edge-activated hats, and add them to threads to be evaluated. + for (var hatType in this._hats) { + var hat = this._hats[hatType]; + if (hat.edgeActivated) { + this.startHats(hatType); + } + } var inactiveThreads = this.sequencer.stepThreads(this.threads); for (var i = 0; i < inactiveThreads.length; i++) { this._removeThread(inactiveThreads[i]); @@ -234,6 +379,19 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) { } }; +/** + * 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. diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 73126a227..11b134b53 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -166,4 +166,14 @@ Sequencer.prototype.proceedThread = function (thread) { } }; +/** + * Retire a thread in the middle, without considering further blocks. + * @param {!Thread} thread Thread object to retire. + */ +Sequencer.prototype.retireThread = function (thread) { + thread.stack = []; + thread.stackFrame = []; + thread.setStatus(Thread.STATUS_DONE); +}; + module.exports = Sequencer; diff --git a/src/engine/thread.js b/src/engine/thread.js index d1bd73fc0..07a98b862 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -123,6 +123,14 @@ Thread.prototype.pushReportedValue = function (value) { } }; +/** + * Whether the current execution of a thread is at the top of the stack. + * @return {Boolean} True if execution is at top of the stack. + */ +Thread.prototype.atStackTop = function () { + return this.peekStack() === this.topBlock; +}; + /** * Set thread status. * @param {number} status Enum representing thread status.