mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
commit
77d0376b15
5 changed files with 323 additions and 55 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ function Runtime (targets) {
|
|||
* @type {Object.<string, Function>}
|
||||
*/
|
||||
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.<Thread>} 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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue