mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-10 23:07:12 -05:00
Merge branch 'develop' of https://github.com/LLK/scratch-vm into feature/extension-music
This commit is contained in:
commit
50c646259c
8 changed files with 352 additions and 111 deletions
|
@ -48,7 +48,7 @@
|
|||
"scratch-audio": "latest",
|
||||
"scratch-blocks": "latest",
|
||||
"scratch-render": "latest",
|
||||
"scratch-storage": "^0.2.0",
|
||||
"scratch-storage": "^0.3.0",
|
||||
"script-loader": "0.7.0",
|
||||
"socket.io-client": "1.7.3",
|
||||
"stats.js": "^0.17.0",
|
||||
|
|
|
@ -245,11 +245,12 @@ class Scratch3LooksBlocks {
|
|||
|
||||
sayforsecs (args, util) {
|
||||
this.say(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
this._updateBubble(util.target, 'say', '');
|
||||
this._updateBubble(_target, 'say', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
|
@ -261,11 +262,12 @@ class Scratch3LooksBlocks {
|
|||
|
||||
thinkforsecs (args, util) {
|
||||
this.think(args, util);
|
||||
const _target = util.target;
|
||||
return new Promise(resolve => {
|
||||
this._bubbleTimeout = setTimeout(() => {
|
||||
this._bubbleTimeout = null;
|
||||
// Clear say bubble and proceed.
|
||||
this._updateBubble(util.target, 'think', '');
|
||||
this._updateBubble(_target, 'think', '');
|
||||
resolve();
|
||||
}, 1000 * args.SECS);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,21 @@ class Scratch3SensingBlocks {
|
|||
* @type {Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
|
||||
/**
|
||||
* The "answer" block value.
|
||||
* @type {string}
|
||||
*/
|
||||
this._answer = '';
|
||||
|
||||
/**
|
||||
* The list of queued questions and respective `resolve` callbacks.
|
||||
* @type {!Array}
|
||||
*/
|
||||
this._questionList = [];
|
||||
|
||||
this.runtime.on('ANSWER', this._onAnswer.bind(this));
|
||||
this.runtime.on('PROJECT_STOP_ALL', this._clearAllQuestions.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,10 +43,51 @@ class Scratch3SensingBlocks {
|
|||
sensing_keypressed: this.getKeyPressed,
|
||||
sensing_current: this.current,
|
||||
sensing_dayssince2000: this.daysSince2000,
|
||||
sensing_loudness: this.getLoudness
|
||||
sensing_loudness: this.getLoudness,
|
||||
sensing_askandwait: this.askAndWait,
|
||||
sensing_answer: this.getAnswer
|
||||
};
|
||||
}
|
||||
|
||||
_onAnswer (answer) {
|
||||
this._answer = answer;
|
||||
const questionObj = this._questionList.shift();
|
||||
if (questionObj) {
|
||||
const resolve = questionObj[1];
|
||||
resolve();
|
||||
this._askNextQuestion();
|
||||
}
|
||||
}
|
||||
|
||||
_enqueueAsk (question, resolve) {
|
||||
this._questionList.push([question, resolve]);
|
||||
}
|
||||
|
||||
_askNextQuestion () {
|
||||
if (this._questionList.length > 0) {
|
||||
this.runtime.emit('QUESTION', this._questionList[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
_clearAllQuestions () {
|
||||
this._questionList = [];
|
||||
this.runtime.emit('QUESTION', null);
|
||||
}
|
||||
|
||||
askAndWait (args) {
|
||||
return new Promise(resolve => {
|
||||
const isQuestionAsked = this._questionList.length > 0;
|
||||
this._enqueueAsk(args.QUESTION, resolve);
|
||||
if (!isQuestionAsked) {
|
||||
this._askNextQuestion();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getAnswer () {
|
||||
return this._answer;
|
||||
}
|
||||
|
||||
touchingObject (args, util) {
|
||||
const requestedObject = args.TOUCHINGOBJECTMENU;
|
||||
if (requestedObject === '_mouse_') {
|
||||
|
|
153
src/engine/block-utility.js
Normal file
153
src/engine/block-utility.js
Normal file
|
@ -0,0 +1,153 @@
|
|||
const Thread = require('./thread');
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* Interface provided to block primitive functions for interacting with the
|
||||
* runtime, thread, target, and convenient methods.
|
||||
*/
|
||||
|
||||
class BlockUtility {
|
||||
constructor (sequencer = null, thread = null) {
|
||||
/**
|
||||
* A sequencer block primitives use to branch or start procedures with
|
||||
* @type {?Sequencer}
|
||||
*/
|
||||
this.sequencer = sequencer;
|
||||
|
||||
/**
|
||||
* The block primitives thread with the block's target, stackFrame and
|
||||
* modifiable status.
|
||||
* @type {?Thread}
|
||||
*/
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target the primitive is working on.
|
||||
* @type {Target}
|
||||
*/
|
||||
get target () {
|
||||
return this.thread.target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime the block primitive is running in.
|
||||
* @type {Runtime}
|
||||
*/
|
||||
get runtime () {
|
||||
return this.sequencer.runtime;
|
||||
}
|
||||
|
||||
/**
|
||||
* The stack frame used by loop and other blocks to track internal state.
|
||||
* @type {object}
|
||||
*/
|
||||
get stackFrame () {
|
||||
return this.thread.peekStackFrame().executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the thread to yield.
|
||||
*/
|
||||
yield () {
|
||||
this.thread.status = Thread.STATUS_YIELD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a branch in the current block.
|
||||
* @param {number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
startBranch (branchNum, isLoop) {
|
||||
this.sequencer.stepToBranch(this.thread, branchNum, isLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads.
|
||||
*/
|
||||
stopAll () {
|
||||
this.sequencer.runtime.stopAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop threads other on this target other than the thread holding the
|
||||
* executed block.
|
||||
*/
|
||||
stopOtherTargetThreads () {
|
||||
this.sequencer.runtime.stopForTarget(this.thread.target, this.thread);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop this thread.
|
||||
*/
|
||||
stopThisScript () {
|
||||
this.thread.stopThisScript();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a specified procedure on this thread.
|
||||
* @param {string} procedureCode Procedure code for procedure to start.
|
||||
*/
|
||||
startProcedure (procedureCode) {
|
||||
this.sequencer.stepToProcedure(this.thread, procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get names for parameters for the given procedure.
|
||||
* @param {string} procedureCode Procedure code for procedure to query.
|
||||
* @return {Array.<string>} List of param names for a procedure.
|
||||
*/
|
||||
getProcedureParamNames (procedureCode) {
|
||||
return this.thread.target.blocks.getProcedureParamNames(procedureCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a procedure parameter value by its name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @param {*} paramValue The procedure's parameter value.
|
||||
*/
|
||||
pushParam (paramName, paramValue) {
|
||||
this.thread.pushParam(paramName, paramValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stored parameter value for a given parameter name.
|
||||
* @param {string} paramName The procedure's parameter name.
|
||||
* @return {*} The parameter's current stored value.
|
||||
*/
|
||||
getParam (paramName) {
|
||||
return this.thread.getParam(paramName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all relevant hats.
|
||||
* @param {!string} requestedHat Opcode of hats to start.
|
||||
* @param {object=} optMatchFields Optionally, fields to match on the hat.
|
||||
* @param {Target=} optTarget Optionally, a target to restrict to.
|
||||
* @return {Array.<Thread>} List of threads started by this function.
|
||||
*/
|
||||
startHats (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query a named IO device.
|
||||
* @param {string} device The name of like the device, like keyboard.
|
||||
* @param {string} func The name of the device's function to query.
|
||||
* @param {Array.<*>} args Arguments to pass to the device's function.
|
||||
* @return {*} The expected output for the device's function.
|
||||
*/
|
||||
ioQuery (device, func, args) {
|
||||
// Find the I/O device and execute the query/function call.
|
||||
if (
|
||||
this.sequencer.runtime.ioDevices[device] &&
|
||||
this.sequencer.runtime.ioDevices[device][func]) {
|
||||
const devObject = this.sequencer.runtime.ioDevices[device];
|
||||
return devObject[func].apply(devObject, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BlockUtility;
|
|
@ -164,9 +164,9 @@ class Blocks {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the procedure definition for a given name.
|
||||
* Get names of parameters for the given procedure.
|
||||
* @param {?string} name Name of procedure to query.
|
||||
* @return {?string} ID of procedure definition.
|
||||
* @return {?Array.<string>} List of param names for a procedure.
|
||||
*/
|
||||
getProcedureParamNames (name) {
|
||||
for (const id in this._blocks) {
|
||||
|
|
|
@ -1,14 +1,82 @@
|
|||
const BlockUtility = require('./block-utility');
|
||||
const log = require('../util/log');
|
||||
const Thread = require('./thread');
|
||||
const {Map} = require('immutable');
|
||||
|
||||
/**
|
||||
* Single BlockUtility instance reused by execute for every pritimive ran.
|
||||
* @const
|
||||
*/
|
||||
const blockUtility = new BlockUtility();
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const isPromise = function (value) {
|
||||
return value && value.then && typeof value.then === 'function';
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
typeof value.then === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle any reported value from the primitive, either directly returned
|
||||
* or after a promise resolves.
|
||||
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||
* @param {!Sequencer} sequencer Sequencer stepping the thread for the ran
|
||||
* primitive.
|
||||
* @param {!Thread} thread Thread containing the primitive.
|
||||
* @param {!string} currentBlockId Id of the block in its thread for value from
|
||||
* the primitive.
|
||||
* @param {!string} opcode opcode used to identify a block function primitive.
|
||||
* @param {!boolean} isHat Is the current block a hat?
|
||||
*/
|
||||
// @todo move this to callback attached to the thread when we have performance
|
||||
// metrics (dd)
|
||||
const handleReport = function (
|
||||
resolvedValue, sequencer, thread, currentBlockId, opcode, isHat) {
|
||||
thread.pushReportedValue(resolvedValue);
|
||||
if (isHat) {
|
||||
// Hat predicate was evaluated.
|
||||
if (sequencer.runtime.getIsEdgeActivatedHat(opcode)) {
|
||||
// If this is an edge-activated hat, only proceed if the value is
|
||||
// true and used to be false, or the stack was activated explicitly
|
||||
// via stack click
|
||||
if (!thread.stackClick) {
|
||||
const oldEdgeValue = sequencer.runtime.updateEdgeActivatedValue(
|
||||
currentBlockId,
|
||||
resolvedValue
|
||||
);
|
||||
const edgeWasActivated = !oldEdgeValue && resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
}
|
||||
} else if (!resolvedValue) {
|
||||
// Not an edge-activated hat: retire the thread
|
||||
// if predicate was false.
|
||||
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()) {
|
||||
if (thread.stackClick) {
|
||||
sequencer.runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
if (thread.updateMonitor) {
|
||||
sequencer.runtime.requestUpdateMonitor(Map({
|
||||
id: currentBlockId,
|
||||
value: String(resolvedValue)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -61,55 +129,6 @@ const execute = function (sequencer, thread) {
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any reported value from the primitive, either directly returned
|
||||
* or after a promise resolves.
|
||||
* @param {*} resolvedValue Value eventually returned from the primitive.
|
||||
*/
|
||||
// @todo move this to callback attached to the thread when we have performance
|
||||
// metrics (dd)
|
||||
const 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, or the stack was activated
|
||||
// explicitly via stack click
|
||||
if (!thread.stackClick) {
|
||||
const oldEdgeValue = runtime.updateEdgeActivatedValue(
|
||||
currentBlockId,
|
||||
resolvedValue
|
||||
);
|
||||
const edgeWasActivated = !oldEdgeValue && resolvedValue;
|
||||
if (!edgeWasActivated) {
|
||||
sequencer.retireThread(thread);
|
||||
}
|
||||
}
|
||||
} else if (!resolvedValue) {
|
||||
// Not an edge-activated hat: retire the thread
|
||||
// if predicate was false.
|
||||
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()) {
|
||||
if (thread.stackClick) {
|
||||
runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
if (thread.updateMonitor) {
|
||||
runtime.requestUpdateMonitor(Map({
|
||||
id: currentBlockId,
|
||||
value: String(resolvedValue)
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
// Hats and single-field shadows are implemented slightly differently
|
||||
// from regular blocks.
|
||||
// For hats: if they have an associated block function,
|
||||
|
@ -124,7 +143,7 @@ const execute = function (sequencer, thread) {
|
|||
const keys = Object.keys(fields);
|
||||
if (keys.length === 1 && Object.keys(inputs).length === 0) {
|
||||
// One field and no inputs - treat as arg.
|
||||
handleReport(fields[keys[0]].value);
|
||||
handleReport(fields[keys[0]].value, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
} else {
|
||||
log.warn(`Could not get implementation for opcode: ${opcode}`);
|
||||
}
|
||||
|
@ -186,49 +205,9 @@ const execute = function (sequencer, thread) {
|
|||
currentStackFrame.reported = {};
|
||||
|
||||
let primitiveReportedValue = null;
|
||||
primitiveReportedValue = blockFunction(argValues, {
|
||||
stackFrame: currentStackFrame.executionContext,
|
||||
target: target,
|
||||
yield: function () {
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
},
|
||||
startBranch: function (branchNum, isLoop) {
|
||||
sequencer.stepToBranch(thread, branchNum, isLoop);
|
||||
},
|
||||
stopAll: function () {
|
||||
runtime.stopAll();
|
||||
},
|
||||
stopOtherTargetThreads: function () {
|
||||
runtime.stopForTarget(target, thread);
|
||||
},
|
||||
stopThisScript: function () {
|
||||
thread.stopThisScript();
|
||||
},
|
||||
startProcedure: function (procedureCode) {
|
||||
sequencer.stepToProcedure(thread, procedureCode);
|
||||
},
|
||||
getProcedureParamNames: function (procedureCode) {
|
||||
return blockContainer.getProcedureParamNames(procedureCode);
|
||||
},
|
||||
pushParam: function (paramName, paramValue) {
|
||||
thread.pushParam(paramName, paramValue);
|
||||
},
|
||||
getParam: function (paramName) {
|
||||
return thread.getParam(paramName);
|
||||
},
|
||||
startHats: function (requestedHat, optMatchFields, optTarget) {
|
||||
return (
|
||||
runtime.startHats(requestedHat, optMatchFields, optTarget)
|
||||
);
|
||||
},
|
||||
ioQuery: function (device, func, args) {
|
||||
// Find the I/O device and execute the query/function call.
|
||||
if (runtime.ioDevices[device] && runtime.ioDevices[device][func]) {
|
||||
const devObject = runtime.ioDevices[device];
|
||||
return devObject[func].apply(devObject, args);
|
||||
}
|
||||
}
|
||||
});
|
||||
blockUtility.sequencer = sequencer;
|
||||
blockUtility.thread = thread;
|
||||
primitiveReportedValue = blockFunction(argValues, blockUtility);
|
||||
|
||||
if (typeof primitiveReportedValue === 'undefined') {
|
||||
// No value reported - potentially a command block.
|
||||
|
@ -244,7 +223,7 @@ const execute = function (sequencer, thread) {
|
|||
}
|
||||
// Promise handlers
|
||||
primitiveReportedValue.then(resolvedValue => {
|
||||
handleReport(resolvedValue);
|
||||
handleReport(resolvedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
if (typeof resolvedValue === 'undefined') {
|
||||
let stackFrame;
|
||||
let nextBlockId;
|
||||
|
@ -277,7 +256,7 @@ const execute = function (sequencer, thread) {
|
|||
thread.popStack();
|
||||
});
|
||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||
handleReport(primitiveReportedValue);
|
||||
handleReport(primitiveReportedValue, sequencer, thread, currentBlockId, opcode, isHat);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class Sequencer {
|
|||
let numActiveThreads = Infinity;
|
||||
// Whether `stepThreads` has run through a full single tick.
|
||||
let ranFirstTick = false;
|
||||
const doneThreads = [];
|
||||
const doneThreads = this.runtime.threads.map(() => null);
|
||||
// Conditions for continuing to stepping threads:
|
||||
// 1. We must have threads in the list, and some must be active.
|
||||
// 2. Time elapsed must be less than WORK_TIME.
|
||||
|
@ -54,11 +54,13 @@ class Sequencer {
|
|||
if (activeThread.stack.length === 0 ||
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread.
|
||||
if (doneThreads.indexOf(activeThread) < 0) {
|
||||
doneThreads.push(activeThread);
|
||||
}
|
||||
doneThreads[i] = activeThread;
|
||||
continue;
|
||||
}
|
||||
// A thread was removed, added or this thread was restarted.
|
||||
if (doneThreads[i] !== null) {
|
||||
doneThreads[i] = null;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
|
||||
!ranFirstTick) {
|
||||
// Clear single-tick yield from the last call of `stepThreads`.
|
||||
|
@ -82,12 +84,27 @@ class Sequencer {
|
|||
ranFirstTick = true;
|
||||
}
|
||||
// Filter inactive threads from `this.runtime.threads`.
|
||||
this.runtime.threads = this.runtime.threads.filter(thread => {
|
||||
if (doneThreads.indexOf(thread) > -1) {
|
||||
return false;
|
||||
numActiveThreads = 0;
|
||||
for (let i = 0; i < this.runtime.threads.length; i++) {
|
||||
const thread = this.runtime.threads[i];
|
||||
if (doneThreads[i] === null) {
|
||||
this.runtime.threads[numActiveThreads] = thread;
|
||||
numActiveThreads++;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
this.runtime.threads.length = numActiveThreads;
|
||||
|
||||
// Filter undefined and null values from `doneThreads`.
|
||||
let numDoneThreads = 0;
|
||||
for (let i = 0; i < doneThreads.length; i++) {
|
||||
const maybeThread = doneThreads[i];
|
||||
if (maybeThread !== null) {
|
||||
doneThreads[numDoneThreads] = maybeThread;
|
||||
numDoneThreads++;
|
||||
}
|
||||
}
|
||||
doneThreads.length = numDoneThreads;
|
||||
|
||||
return doneThreads;
|
||||
}
|
||||
|
||||
|
|
34
test/unit/blocks_sensing.js
Normal file
34
test/unit/blocks_sensing.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
const test = require('tap').test;
|
||||
const Sensing = require('../../src/blocks/scratch3_sensing');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
|
||||
test('getPrimitives', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
t.type(s.getPrimitives(), 'object');
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('ask and answer', t => {
|
||||
const rt = new Runtime();
|
||||
const s = new Sensing(rt);
|
||||
|
||||
const expectedQuestion = 'a question';
|
||||
const expectedAnswer = 'the answer';
|
||||
|
||||
// Test is written out of order because of promises, follow the (#) comments.
|
||||
rt.addListener('QUESTION', question => {
|
||||
// (2) Assert the question is correct, then emit the answer
|
||||
t.strictEqual(question, expectedQuestion);
|
||||
rt.emit('ANSWER', expectedAnswer);
|
||||
});
|
||||
|
||||
// (1) Emit the question.
|
||||
const promise = s.askAndWait({QUESTION: expectedQuestion});
|
||||
|
||||
// (3) Ask block resolves after the answer is emitted.
|
||||
promise.then(() => {
|
||||
t.strictEqual(s.getAnswer(), expectedAnswer);
|
||||
t.end();
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue