Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow primitives to have "cancel" behavior; cancel "play sound until done" on thread retire #2002

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/blocks/scratch3_sound.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ class Scratch3SoundBlocks {
};
}

/**
* Retrieve the cancel block primitives implemented by this package.
* @return {object.<string, FUnction>} Mapping of opcode to Function.
*/
getCancelPrimitives () {
return {
sound_playuntildone: this.cancelPlaySoundAndWait
};
}

getMonitored () {
return {
sound_volume: {
Expand All @@ -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);
Expand All @@ -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();
Expand Down
75 changes: 65 additions & 10 deletions src/engine/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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;
24 changes: 24 additions & 0 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<string, Function>}
*/
this._cancelPrimitives = {};

/**
* Map to look up all block information by extended opcode.
* @type {Array.<CategoryInfo>}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/engine/sequencer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down