Merge branch 'develop' of https://github.com/LLK/scratch-vm into storage-no-cache

# Conflicts:
#	package-lock.json
This commit is contained in:
Ray Schamp 2018-10-24 12:09:06 +01:00
commit 902aee48ee
16 changed files with 815 additions and 135 deletions

739
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -54,7 +54,7 @@
"adm-zip": "0.4.11",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.4",
"copy-webpack-plugin": "^4.5.2",
"copy-webpack-plugin": "^4.5.4",
"docdash": "^1.0.0",
"eslint": "^5.3.0",
"eslint-config-scratch": "^5.0.0",

View file

@ -27,20 +27,22 @@ class Scratch3ProcedureBlocks {
call (args, util) {
if (!util.stackFrame.executed) {
const procedureCode = args.mutation.proccode;
const paramNamesAndIds = util.getProcedureParamNamesAndIds(procedureCode);
const paramNamesIdsAndDefaults = util.getProcedureParamNamesIdsAndDefaults(procedureCode);
// If null, procedure could not be found, which can happen if custom
// block is dragged between sprites without the definition.
// Match Scratch 2.0 behavior and noop.
if (paramNamesAndIds === null) {
if (paramNamesIdsAndDefaults === null) {
return;
}
const [paramNames, paramIds] = paramNamesAndIds;
const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;
for (let i = 0; i < paramIds.length; i++) {
if (args.hasOwnProperty(paramIds[i])) {
util.pushParam(paramNames[i], args[paramIds[i]]);
} else {
util.pushParam(paramNames[i], paramDefaults[i]);
}
}

View file

@ -2,6 +2,12 @@ const MathUtil = require('../util/math-util');
const Cast = require('../util/cast');
const Clone = require('../util/clone');
/**
* Occluded boolean value to make its use more understandable.
* @const {boolean}
*/
const STORE_WAITING = true;
class Scratch3SoundBlocks {
constructor (runtime) {
/**
@ -10,10 +16,16 @@ class Scratch3SoundBlocks {
*/
this.runtime = runtime;
this.waitingSounds = {};
// Clear sound effects on green flag and stop button events.
this.stopAllSounds = this.stopAllSounds.bind(this);
this._stopWaitingSoundsForTarget = this._stopWaitingSoundsForTarget.bind(this);
this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this);
if (this.runtime) {
this.runtime.on('PROJECT_STOP_ALL', this.stopAllSounds);
this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
this.runtime.on('STOP_FOR_TARGET', this._stopWaitingSoundsForTarget);
this.runtime.on('PROJECT_START', this._clearEffectsForAllTargets);
}
@ -141,21 +153,44 @@ class Scratch3SoundBlocks {
playSound (args, util) {
// Don't return the promise, it's the only difference for AndWait
this.playSoundAndWait(args, util);
this._playSound(args, util);
}
playSoundAndWait (args, util) {
return this._playSound(args, util, STORE_WAITING);
}
_playSound (args, util, storeWaiting) {
const index = this._getSoundIndex(args.SOUND_MENU, util);
if (index >= 0) {
const {target} = util;
const {sprite} = target;
const {soundId} = sprite.sounds[index];
if (sprite.soundBank) {
if (storeWaiting === STORE_WAITING) {
this._addWaitingSound(target.id, soundId);
} else {
this._removeWaitingSound(target.id, soundId);
}
return sprite.soundBank.playSound(target, soundId);
}
}
}
_addWaitingSound (targetId, soundId) {
if (!this.waitingSounds[targetId]) {
this.waitingSounds[targetId] = new Set();
}
this.waitingSounds[targetId].add(soundId);
}
_removeWaitingSound (targetId, soundId) {
if (!this.waitingSounds[targetId]) {
return;
}
this.waitingSounds[targetId].delete(soundId);
}
_getSoundIndex (soundName, util) {
// if the sprite has no sounds, return -1
const len = util.target.sprite.sounds.length;
@ -201,6 +236,20 @@ class Scratch3SoundBlocks {
_stopAllSoundsForTarget (target) {
if (target.sprite.soundBank) {
target.sprite.soundBank.stopAllSounds(target);
if (this.waitingSounds[target.id]) {
this.waitingSounds[target.id].clear();
}
}
}
_stopWaitingSoundsForTarget (target) {
if (target.sprite.soundBank) {
if (this.waitingSounds[target.id]) {
for (const soundId of this.waitingSounds[target.id].values()) {
target.sprite.soundBank.stop(target, soundId);
}
this.waitingSounds[target.id].clear();
}
}
}

View file

@ -104,7 +104,7 @@ class BlockUtility {
}
/**
* Get names for parameters for the given procedure.
* Get names and ids of parameters for the given procedure.
* @param {string} procedureCode Procedure code for procedure to query.
* @return {Array.<string>} List of param names for a procedure.
*/
@ -112,6 +112,15 @@ class BlockUtility {
return this.thread.target.blocks.getProcedureParamNamesAndIds(procedureCode);
}
/**
* Get names, ids, and defaults of parameters for the given procedure.
* @param {string} procedureCode Procedure code for procedure to query.
* @return {Array.<string>} List of param names for a procedure.
*/
getProcedureParamNamesIdsAndDefaults (procedureCode) {
return this.thread.target.blocks.getProcedureParamNamesIdsAndDefaults(procedureCode);
}
/**
* Store a procedure parameter value by its name.
* @param {string} paramName The procedure's parameter name.

View file

@ -230,11 +230,20 @@ class Blocks {
}
/**
* Get names of parameters for the given procedure.
* Get names and ids of parameters for the given procedure.
* @param {?string} name Name of procedure to query.
* @return {?Array.<string>} List of param names for a procedure.
*/
getProcedureParamNamesAndIds (name) {
return this.getProcedureParamNamesIdsAndDefaults(name).slice(0, 2);
}
/**
* Get names, ids, and defaults of parameters for the given procedure.
* @param {?string} name Name of procedure to query.
* @return {?Array.<string>} List of param names for a procedure.
*/
getProcedureParamNamesIdsAndDefaults (name) {
const cachedNames = this._cache.procedureParamNames[name];
if (typeof cachedNames !== 'undefined') {
return cachedNames;
@ -247,7 +256,9 @@ class Blocks {
block.mutation.proccode === name) {
const names = JSON.parse(block.mutation.argumentnames);
const ids = JSON.parse(block.mutation.argumentids);
this._cache.procedureParamNames[name] = [names, ids];
const defaults = JSON.parse(block.mutation.argumentdefaults);
this._cache.procedureParamNames[name] = [names, ids, defaults];
return this._cache.procedureParamNames[name];
}
}

View file

@ -1604,14 +1604,6 @@ class Runtime extends EventEmitter {
return count;
}
/**
*
* @return {OrderedMap} The current state of monitor blocks.
*/
getMonitorState () {
return this._monitorState;
}
/**
* Queue monitor blocks to sequencer to be run.
*/

View file

@ -179,8 +179,7 @@ class Scratch3VideoSensingBlocks {
if (stage) {
return stage.videoState;
}
// Default to off to prevent a flash of video while the project is loading
return VideoState.OFF;
return VideoState.ON;
}
set globalVideoState (state) {

View file

@ -334,55 +334,6 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
}));
};
const confirmTargetExtensions = function (targets, extensions) {
// Ensure only extensions used by blocks or visible monitors are enabled
const extensionsInUse = new Set();
for (const ext of extensions.extensionIDs) {
let extensionConfirmed = false;
for (const target of targets) {
const targetBlocks = Object.entries(target.blocks._blocks);
// Make sure there is a block that uses the currently set extensions
extensionConfirmed = targetBlocks.some(block => {
const opcode = block[1].opcode;
if (opcode && (ext === opcode.split('_')[0])) {
return true;
}
return false;
});
if (extensionConfirmed) {
extensionsInUse.add(ext);
break;
}
const monitorState = target.runtime.getMonitorState();
// Check if a visible monitor uses this extension
extensionConfirmed = monitorState.some(monitor => {
if (!monitor.visible) {
return false;
}
if (monitor.opcode && (ext === monitor.opcode.split('_')[0])) {
return true;
}
return false;
});
if (extensionConfirmed) {
extensionsInUse.add(ext);
break;
}
}
}
extensions.extensionIDs = extensionsInUse;
return extensions;
};
/**
* Parse a single "Scratch object" and create all its in-memory VM objects.
* TODO: parse the "info" section, especially "savedExtensions"
@ -654,8 +605,6 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
if (object.info.hasOwnProperty('videoOn')) {
if (object.info.videoOn) {
target.videoState = RenderedTarget.VIDEO_STATE.ON;
} else {
target.videoState = RenderedTarget.VIDEO_STATE.OFF;
}
}
}
@ -744,9 +693,6 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
for (let n = 0; n < deferredMonitors.length; n++) {
parseMonitorObject(deferredMonitors[n], runtime, targets, extensions);
}
extensions = confirmTargetExtensions(targets, extensions);
return targets;
})
);
@ -766,6 +712,7 @@ const reorderParsedTargets = function (targets) {
return targets;
};
/**
* Top-level handler. Parse provided JSON,
* and process the top-level object (the stage object).

View file

@ -1074,9 +1074,6 @@ class RenderedTarget extends Target {
*/
onStopAll () {
this.clearEffects();
if (this.sprite.soundBank) {
this.sprite.soundBank.stopAllSounds();
}
}
/**

View file

@ -16,7 +16,7 @@ test('calling a custom block with no definition does not throw', t => {
}
};
const util = {
getProcedureParamNamesAndIds: () => null,
getProcedureParamNamesIdsAndDefaults: () => null,
stackFrame: {
executed: false
}

View file

@ -67,7 +67,7 @@ test('ask and stop all dismisses question', t => {
test('ask and stop other scripts dismisses if it is the last question', t => {
const rt = new Runtime();
const s = new Sensing(rt);
const util = {target: {visible: false}, thread: {}};
const util = {target: {visible: false, sprite: {}}, thread: {}};
const expectedQuestion = 'a question';
@ -94,8 +94,8 @@ test('ask and stop other scripts dismisses if it is the last question', t => {
test('ask and stop other scripts asks next question', t => {
const rt = new Runtime();
const s = new Sensing(rt);
const util = {target: {visible: false}, thread: {}};
const util2 = {target: {visible: false}, thread: {}};
const util = {target: {visible: false, sprite: {}}, thread: {}};
const util2 = {target: {visible: false, sprite: {}}, thread: {}};
const expectedQuestion = 'a question';
const nextQuestion = 'a followup';

View file

@ -102,50 +102,3 @@ test('Ordering', t => {
t.end();
});
});
test('Extensions used in blocks serialized', t => {
const uri = path.resolve(__dirname, '../fixtures/extensions-in-blocks.sb2');
const json = extractProjectJson(uri);
const rt = new Runtime();
const expectedExtensionIDs = new Set(['videoSensing', 'wedo2', 'pen']);
// Make sure any extensions loaded in a block are added to the project
sb2.deserialize(json, rt).then(({extensions}) => {
t.deepEquals(extensions.extensionIDs, expectedExtensionIDs);
t.end();
});
});
test('Extensions used in visible monitors serialized', t => {
const uri = path.resolve(__dirname, '../fixtures/extensions-in-monitors.sb2');
const json = extractProjectJson(uri);
const rt = new Runtime();
sb2.deserialize(json, rt).then(({extensions}) => {
const monitorState = rt.getMonitorState();
const monitor = monitorState.first();
const monitorOpcode = monitor.opcode.split('_')[0];
t.ok(extensions.extensionIDs.has(monitorOpcode));
t.equals(monitor.visible, true);
t.end();
});
});
test('No extensions serialized', t => {
// This test project has had the video motion monitor checked, saved,
// then unchecked and then saved
const uri = path.resolve(__dirname, '../fixtures/extensions-not-serialized.sb2');
const json = extractProjectJson(uri);
const rt = new Runtime();
sb2.deserialize(json, rt).then(({extensions}) => {
const monitorState = rt.getMonitorState();
const monitor = monitorState.first();
const monitorOpcode = monitor.opcode.split('_')[0];
t.equals(monitor.visible, false);
t.notOk(extensions.extensionIDs.has(monitorOpcode));
t.end();
});
});