diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 237db61239a..03a587841ef 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -143,6 +143,16 @@ class Scratch3SoundBlocks { }; } + /** + * Retrieve the cancel block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getCancelPrimitives () { + return { + sound_playuntildone: this.cancelPlaySoundAndWait + }; + } + getMonitored () { return { sound_volume: { @@ -162,11 +172,10 @@ class Scratch3SoundBlocks { } _playSound (args, util, storeWaiting) { - const index = this._getSoundIndex(args.SOUND_MENU, util); - if (index >= 0) { + const soundId = this._getSoundId(args.SOUND_MENU, util); + if (soundId) { const {target} = util; const {sprite} = target; - const {soundId} = sprite.sounds[index]; if (sprite.soundBank) { if (storeWaiting === STORE_WAITING) { this._addWaitingSound(target.id, soundId); @@ -178,6 +187,26 @@ class Scratch3SoundBlocks { } } + cancelPlaySoundAndWait (args, util) { + const soundId = this._getSoundId(args.SOUND_MENU, util); + const {target} = util; + const {sprite} = target; + if (sprite.soundBank) { + sprite.soundBank.stop(target, soundId); + } + } + + _getSoundId (indexArg, util) { + const index = this._getSoundIndex(indexArg, util); + if (index >= 0) { + const {target} = util; + const {sprite} = target; + const {soundId} = sprite.sounds[index]; + return soundId; + } + return null; + } + _addWaitingSound (targetId, soundId) { if (!this.waitingSounds[targetId]) { this.waitingSounds[targetId] = new Set(); diff --git a/src/engine/execute.js b/src/engine/execute.js index c8638d01ab8..fb77cd6cc41 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -209,6 +209,18 @@ class BlockCached { */ this._definedBlockFunction = false; + /** + * The block opcode's "cancel" implementation function. + * @type {?function} + */ + this._cancelBlockFunction = null; + + /** + * Is the cancel block function defined for this opcode? + * @type {boolean} + */ + this._definedCancelBlockFunction = false; + /** * Is this block a block with no function but a static value to return. * @type {boolean} @@ -279,7 +291,9 @@ class BlockCached { // Assign opcode isHat and blockFunction data to avoid dynamic lookups. this._isHat = runtime.getIsHat(opcode); this._blockFunction = runtime.getOpcodeFunction(opcode); + this._cancelBlockFunction = runtime.getCancelOpcodeFunction(opcode); this._definedBlockFunction = typeof this._blockFunction !== 'undefined'; + this._definedCancelBlockFunction = typeof this._cancelBlockFunction !== 'undefined'; // Store the current shadow value if there is a shadow value. const fieldKeys = Object.keys(fields); @@ -370,6 +384,16 @@ class BlockCached { } } +const getBlockCached = function (currentBlockId, thread, runtime) { + let blockContainer = thread.blockContainer; + let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + if (blockCached === null) { + blockContainer = runtime.flyoutBlocks; + blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + } + return {blockCached, blockContainer}; +}; + /** * Execute a block. * @param {!Sequencer} sequencer Which sequencer is executing. @@ -386,18 +410,13 @@ const execute = function (sequencer, thread) { // Current block to execute is the one on the top of the stack. const currentBlockId = thread.peekStack(); const currentStackFrame = thread.peekStackFrame(); + const {blockCached, blockContainer} = getBlockCached(currentBlockId, thread, runtime); - let blockContainer = thread.blockContainer; - let blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); + // Stop if block or target no longer exists. if (blockCached === null) { - blockContainer = runtime.flyoutBlocks; - blockCached = BlocksExecuteCache.getCached(blockContainer, currentBlockId, BlockCached); - // Stop if block or target no longer exists. - if (blockCached === null) { - // No block found: stop the thread; script no longer exists. - sequencer.retireThread(thread); - return; - } + // No block found: stop the thread; script no longer exists. + sequencer.retireThread(thread); + return; } const ops = blockCached._ops; @@ -560,4 +579,40 @@ const execute = function (sequencer, thread) { } }; +/** + * Cancel the block which is currently running, if implementation is provided. + * @param {!Sequencer} sequencer Which sequencer is executing. + * @param {!Thread} thread Thread which to read and execute. + */ +const cancelExecution = function (sequencer, thread) { + const runtime = sequencer.runtime; + + blockUtility.sequencer = sequencer; + blockUtility.thread = thread; + + const currentBlockId = thread.peekStack(); + const {blockCached} = getBlockCached(currentBlockId, thread, runtime); + + if (!blockCached) { + return; + } + + // Only the last op is actually a block and not an input; it is the one + // that may have a cancel implementation. + const ops = blockCached._ops; + const opCached = ops[ops.length - 1]; + + // If there is no cancel implementation provided, stop. + if (!opCached._definedCancelBlockFunction) { + return; + } + + // Execute the cancel function. + const cancelBlockFunction = opCached._cancelBlockFunction; + const argValues = opCached._argValues; + cancelBlockFunction(argValues, blockUtility); +}; + +execute.cancelExecution = cancelExecution; + module.exports = execute; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 08a3e95a027..e5d9e931e3e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -207,6 +207,12 @@ class Runtime extends EventEmitter { */ this._primitives = {}; + /** + * Map to look up a block primitive's "cancel" implementation function by its opcode. + * @type {Object.} + */ + this._cancelPrimitives = {}; + /** * Map to look up all block information by extended opcode. * @type {Array.} @@ -727,6 +733,15 @@ class Runtime extends EventEmitter { } } } + if (packageObject.getCancelPrimitives) { + const packagePrimitives = packageObject.getCancelPrimitives(); + for (const op in packagePrimitives) { + if (packagePrimitives.hasOwnProperty(op)) { + this._cancelPrimitives[op] = + packagePrimitives[op].bind(packageObject); + } + } + } // Collect hat metadata from package. if (packageObject.getHats) { const packageHats = packageObject.getHats(); @@ -1314,6 +1329,15 @@ class Runtime extends EventEmitter { return this._primitives[opcode]; } + /** + * Retrieve the function associated with cancelling a call to the given opcode. + * @param {!string} opcode The opcode to look up. + * @return {Function} The function which implements cancelling the opcode. + */ + getCancelOpcodeFunction (opcode) { + return this._cancelPrimitives[opcode]; + } + /** * Return whether an opcode represents a hat block. * @param {!string} opcode The opcode to look up. diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index d5d71fd50a5..398668e6a63 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -350,6 +350,11 @@ class Sequencer { * @param {!Thread} thread Thread object to retire. */ retireThread (thread) { + // Run the currently executing block's "cancel" function if present. + // This is used to cancel actions that have not yet finished, such as + // playing a sound or spinning a motor. + execute.cancelExecution(this, thread); + thread.stack = []; thread.stackFrame = []; thread.requestScriptGlowInFrame = false;