mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
General-purpose hat implementation
This commit is contained in:
parent
39fdbaf983
commit
b4cf64009f
2 changed files with 232 additions and 46 deletions
|
@ -1,5 +1,14 @@
|
||||||
var Thread = require('./thread');
|
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.
|
* Execute a block.
|
||||||
* @param {!Sequencer} sequencer Which sequencer is executing.
|
* @param {!Sequencer} sequencer Which sequencer is executing.
|
||||||
|
@ -21,10 +30,29 @@ var execute = function (sequencer, thread) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var blockFunction = runtime.getOpcodeFunction(opcode);
|
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 (isHat) {
|
||||||
|
var nextBlock = target.blocks.getNextBlock(currentBlockId);
|
||||||
|
if (!nextBlock) {
|
||||||
|
// Hat with no next block - don't try to evaluate it.
|
||||||
|
sequencer.retireThread(thread);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!blockFunction) {
|
||||||
|
// No predicate for the hat - just continue to next block.
|
||||||
|
sequencer.proceedThread(thread);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (!blockFunction) {
|
if (!blockFunction) {
|
||||||
console.warn('Could not get implementation for opcode: ' + opcode);
|
console.warn('Could not get implementation for opcode: ' + opcode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate values for arguments (inputs).
|
// Generate values for arguments (inputs).
|
||||||
var argValues = {};
|
var argValues = {};
|
||||||
|
@ -63,6 +91,8 @@ var execute = function (sequencer, thread) {
|
||||||
|
|
||||||
var primitiveReportedValue = null;
|
var primitiveReportedValue = null;
|
||||||
primitiveReportedValue = blockFunction(argValues, {
|
primitiveReportedValue = blockFunction(argValues, {
|
||||||
|
stackFrame: currentStackFrame.executionContext,
|
||||||
|
target: target,
|
||||||
yield: function() {
|
yield: function() {
|
||||||
thread.setStatus(Thread.STATUS_YIELD);
|
thread.setStatus(Thread.STATUS_YIELD);
|
||||||
},
|
},
|
||||||
|
@ -73,11 +103,14 @@ var execute = function (sequencer, thread) {
|
||||||
thread.setStatus(Thread.STATUS_RUNNING);
|
thread.setStatus(Thread.STATUS_RUNNING);
|
||||||
sequencer.proceedThread(thread);
|
sequencer.proceedThread(thread);
|
||||||
},
|
},
|
||||||
stackFrame: currentStackFrame.executionContext,
|
|
||||||
startBranch: function (branchNum) {
|
startBranch: function (branchNum) {
|
||||||
sequencer.stepToBranch(thread, branchNum);
|
sequencer.stepToBranch(thread, branchNum);
|
||||||
},
|
},
|
||||||
target: target,
|
triggerHats: function(requestedHat, opt_matchFields, opt_target) {
|
||||||
|
return (
|
||||||
|
runtime.triggerHats(requestedHat, opt_matchFields, opt_target)
|
||||||
|
);
|
||||||
|
},
|
||||||
ioQuery: function (device, func, args) {
|
ioQuery: function (device, func, args) {
|
||||||
// Find the I/O device and execute the query/function call.
|
// Find the I/O device and execute the query/function call.
|
||||||
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
|
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
|
||||||
|
@ -87,28 +120,53 @@ var execute = function (sequencer, thread) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deal with any reported value.
|
/**
|
||||||
// If it's a promise, wait until promise resolves.
|
* Handle any reported value from the primitive, either directly returned
|
||||||
var isPromise = (
|
* or after a promise resolves.
|
||||||
primitiveReportedValue &&
|
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||||
primitiveReportedValue.then &&
|
*/
|
||||||
typeof primitiveReportedValue.then === 'function'
|
var handleReport = function (resolvedValue) {
|
||||||
|
thread.pushReportedValue(resolvedValue);
|
||||||
|
if (isHat) {
|
||||||
|
// Hat predicate was evaluated.
|
||||||
|
if (runtime.getIsEdgeTriggeredHat(opcode)) {
|
||||||
|
// If this is an edge-triggered hat, only proceed if
|
||||||
|
// the value is true and used to be false.
|
||||||
|
var oldEdgeValue = runtime.updateEdgeTriggeredValue(
|
||||||
|
currentBlockId,
|
||||||
|
resolvedValue
|
||||||
);
|
);
|
||||||
if (isPromise) {
|
var edgeWasTriggered = !oldEdgeValue && resolvedValue;
|
||||||
|
if (!edgeWasTriggered) {
|
||||||
|
sequencer.retireThread(thread);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not an edge-triggered 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.
|
||||||
|
if (isPromise(primitiveReportedValue)) {
|
||||||
if (thread.status === Thread.STATUS_RUNNING) {
|
if (thread.status === Thread.STATUS_RUNNING) {
|
||||||
// Primitive returned a promise; automatically yield thread.
|
// Primitive returned a promise; automatically yield thread.
|
||||||
thread.setStatus(Thread.STATUS_YIELD);
|
thread.setStatus(Thread.STATUS_YIELD);
|
||||||
}
|
}
|
||||||
// Promise handlers
|
// Promise handlers
|
||||||
primitiveReportedValue.then(function(resolvedValue) {
|
primitiveReportedValue.then(function(resolvedValue) {
|
||||||
// Promise resolved: the primitive reported a value.
|
handleReport(resolvedValue);
|
||||||
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);
|
|
||||||
sequencer.proceedThread(thread);
|
sequencer.proceedThread(thread);
|
||||||
}, function(rejectionReason) {
|
}, function(rejectionReason) {
|
||||||
// Promise rejected: the primitive had some error.
|
// Promise rejected: the primitive had some error.
|
||||||
|
@ -118,12 +176,7 @@ var execute = function (sequencer, thread) {
|
||||||
sequencer.proceedThread(thread);
|
sequencer.proceedThread(thread);
|
||||||
});
|
});
|
||||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||||
thread.pushReportedValue(primitiveReportedValue);
|
handleReport(primitiveReportedValue);
|
||||||
// Report the value visually if necessary.
|
|
||||||
if (typeof primitiveReportedValue !== 'undefined' &&
|
|
||||||
thread.peekStack() === thread.topBlock) {
|
|
||||||
runtime.visualReport(thread.peekStack(), primitiveReportedValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,8 @@ function Runtime (targets) {
|
||||||
* @type {Object.<string, Function>}
|
* @type {Object.<string, Function>}
|
||||||
*/
|
*/
|
||||||
this._primitives = {};
|
this._primitives = {};
|
||||||
|
this._hats = {};
|
||||||
|
this._edgeTriggeredHatValues = {};
|
||||||
this._registerBlockPackages();
|
this._registerBlockPackages();
|
||||||
|
|
||||||
this.ioDevices = {
|
this.ioDevices = {
|
||||||
|
@ -109,11 +111,23 @@ Runtime.prototype._registerBlockPackages = function () {
|
||||||
if (defaultBlockPackages.hasOwnProperty(packageName)) {
|
if (defaultBlockPackages.hasOwnProperty(packageName)) {
|
||||||
// @todo pass a different runtime depending on package privilege?
|
// @todo pass a different runtime depending on package privilege?
|
||||||
var packageObject = new (defaultBlockPackages[packageName])(this);
|
var packageObject = new (defaultBlockPackages[packageName])(this);
|
||||||
var packageContents = packageObject.getPrimitives();
|
// Collect primitives from package.
|
||||||
for (var op in packageContents) {
|
if (packageObject.getPrimitives) {
|
||||||
if (packageContents.hasOwnProperty(op)) {
|
var packagePrimitives = packageObject.getPrimitives();
|
||||||
|
for (var op in packagePrimitives) {
|
||||||
|
if (packagePrimitives.hasOwnProperty(op)) {
|
||||||
this._primitives[op] =
|
this._primitives[op] =
|
||||||
packageContents[op].bind(packageObject);
|
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,6 +146,46 @@ 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 opcode in this._hats;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether an opcode represents an edge-triggered hat block.
|
||||||
|
* @param {!string} opcode The opcode to look up.
|
||||||
|
* @return {Boolean} True if the op is known to be a edge-triggered hat.
|
||||||
|
*/
|
||||||
|
Runtime.prototype.getIsEdgeTriggeredHat = function (opcode) {
|
||||||
|
return opcode in this._hats && this._hats[opcode].edgeTriggered;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an edge-triggered hat block value.
|
||||||
|
* @param {!string} blockId ID of hat to store value for.
|
||||||
|
* @param {*} newValue Value to store for edge-triggered hat.
|
||||||
|
* @return {*} The old value for the edge-triggered hat.
|
||||||
|
*/
|
||||||
|
Runtime.prototype.updateEdgeTriggeredValue = function (blockId, newValue) {
|
||||||
|
var oldValue = this._edgeTriggeredHatValues[blockId];
|
||||||
|
this._edgeTriggeredHatValues[blockId] = newValue;
|
||||||
|
return oldValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all edge-triggered hat values.
|
||||||
|
*/
|
||||||
|
Runtime.prototype.clearEdgeTriggeredValues = function () {
|
||||||
|
this._edgeTriggeredHatValues = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a thread and push it to the list of threads.
|
* Create a thread and push it to the list of threads.
|
||||||
* @param {!string} id ID of block that starts the stack
|
* @param {!string} id ID of block that starts the stack
|
||||||
|
@ -150,6 +204,7 @@ Runtime.prototype._pushThread = function (id) {
|
||||||
* @param {?Thread} thread Thread object to remove from actives
|
* @param {?Thread} thread Thread object to remove from actives
|
||||||
*/
|
*/
|
||||||
Runtime.prototype._removeThread = function (thread) {
|
Runtime.prototype._removeThread = function (thread) {
|
||||||
|
thread.setStatus(Thread.STATUS_DONE);
|
||||||
var i = this.threads.indexOf(thread);
|
var i = this.threads.indexOf(thread);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
this.glowScript(thread.topBlock, false);
|
this.glowScript(thread.topBlock, false);
|
||||||
|
@ -174,26 +229,97 @@ Runtime.prototype.toggleScript = function (topBlockId) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Green flag, which stops currently running threads
|
* Run a function `f` for all scripts in a workspace.
|
||||||
* and adds all top-level scripts that start with the green flag
|
* `f` will be called with two parameters:
|
||||||
|
* -the top block ID of each script
|
||||||
|
* -the opcode of that block, for convenience.
|
||||||
|
* -fields on that block, for convenience.
|
||||||
|
* @param {!Function} f Function to call for each script.
|
||||||
|
* @param {Target=} opt_target Optionally, a target to restrict to.
|
||||||
*/
|
*/
|
||||||
Runtime.prototype.greenFlag = function () {
|
Runtime.prototype.allScriptsDo = function (f, opt_target) {
|
||||||
// Remove all existing threads
|
var targets = this.targets;
|
||||||
for (var i = 0; i < this.threads.length; i++) {
|
if (opt_target) {
|
||||||
this._removeThread(this.threads[i]);
|
targets = [opt_target];
|
||||||
}
|
}
|
||||||
// Add all top scripts with green flag
|
for (var t = 0; t < targets.length; t++) {
|
||||||
for (var t = 0; t < this.targets.length; t++) {
|
var target = targets[t];
|
||||||
var target = this.targets[t];
|
|
||||||
var scripts = target.blocks.getScripts();
|
var scripts = target.blocks.getScripts();
|
||||||
for (var j = 0; j < scripts.length; j++) {
|
for (var j = 0; j < scripts.length; j++) {
|
||||||
var topBlock = scripts[j];
|
var topBlock = scripts[j];
|
||||||
if (target.blocks.getBlock(topBlock).opcode ===
|
var topOpcode = target.blocks.getBlock(topBlock).opcode;
|
||||||
'event_whenflagclicked') {
|
var topFields = target.blocks.getFields(topBlock);
|
||||||
this._pushThread(scripts[j]);
|
f(topBlock, topOpcode, topFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger all relevant hats.
|
||||||
|
* @param {!string} requestedHat Name of hat to trigger.
|
||||||
|
* @param {Object=} opt_matchFields Optionally, fields to match on the hat.
|
||||||
|
* @param {Target=} opt_target Optionally, a target to restrict to.
|
||||||
|
* @return {Array.<Thread>} List of threads started by this trigger.
|
||||||
|
*/
|
||||||
|
Runtime.prototype.triggerHats = function (requestedHat,
|
||||||
|
opt_matchFields, opt_target) {
|
||||||
|
var instance = this;
|
||||||
|
var newThreads = [];
|
||||||
|
// Consider all scripts, looking for hats named `requestedHat`.
|
||||||
|
this.allScriptsDo(function(topBlockId, topOpcode, topFields) {
|
||||||
|
if (topOpcode !== requestedHat) {
|
||||||
|
// 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 triggered threads.
|
||||||
|
if (opt_matchFields) {
|
||||||
|
for (var matchField in opt_matchFields) {
|
||||||
|
if (topFields[matchField].value !==
|
||||||
|
opt_matchFields[matchField]) {
|
||||||
|
// Field mismatch.
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (instance._hats.hasOwnProperty(topOpcode)) {
|
||||||
|
// Look up metadata for the relevant hat.
|
||||||
|
var hatMeta = instance._hats[topOpcode];
|
||||||
|
if (hatMeta.restartExistingThreads) {
|
||||||
|
// If `restartExistingThreads` is true, this trigger
|
||||||
|
// 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, this trigger
|
||||||
|
// 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.clearEdgeTriggeredValues();
|
||||||
|
this.triggerHats('event_whenflagclicked');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -217,9 +343,16 @@ Runtime.prototype.stopAll = function () {
|
||||||
* inactive threads after each iteration.
|
* inactive threads after each iteration.
|
||||||
*/
|
*/
|
||||||
Runtime.prototype._step = function () {
|
Runtime.prototype._step = function () {
|
||||||
|
// Find all edge-triggered hats, and add them to threads to be evaluated.
|
||||||
|
for (var hatType in this._hats) {
|
||||||
|
var hat = this._hats[hatType];
|
||||||
|
if (hat.edgeTriggered) {
|
||||||
|
this.triggerHats(hatType);
|
||||||
|
}
|
||||||
|
}
|
||||||
var inactiveThreads = this.sequencer.stepThreads(this.threads);
|
var inactiveThreads = this.sequencer.stepThreads(this.threads);
|
||||||
for (var i = 0; i < inactiveThreads.length; i++) {
|
for (var j = 0; j < inactiveThreads.length; j++) {
|
||||||
this._removeThread(inactiveThreads[i]);
|
this._removeThread(inactiveThreads[j]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue