mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-03-14 01:09:51 -04:00
Merge branch 'develop' of https://github.com/LLK/scratch-vm into storage-no-cache
# Conflicts: # package-lock.json
This commit is contained in:
commit
902aee48ee
16 changed files with 815 additions and 135 deletions
package-lock.jsonpackage.json
src
blocks
engine
extensions/scratch3_video_sensing
serialization
sprites
test
739
package-lock.json
generated
739
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -54,7 +54,7 @@
|
||||||
"adm-zip": "0.4.11",
|
"adm-zip": "0.4.11",
|
||||||
"babel-eslint": "^10.0.1",
|
"babel-eslint": "^10.0.1",
|
||||||
"babel-loader": "^8.0.4",
|
"babel-loader": "^8.0.4",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.4",
|
||||||
"docdash": "^1.0.0",
|
"docdash": "^1.0.0",
|
||||||
"eslint": "^5.3.0",
|
"eslint": "^5.3.0",
|
||||||
"eslint-config-scratch": "^5.0.0",
|
"eslint-config-scratch": "^5.0.0",
|
||||||
|
|
|
@ -27,20 +27,22 @@ class Scratch3ProcedureBlocks {
|
||||||
call (args, util) {
|
call (args, util) {
|
||||||
if (!util.stackFrame.executed) {
|
if (!util.stackFrame.executed) {
|
||||||
const procedureCode = args.mutation.proccode;
|
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
|
// If null, procedure could not be found, which can happen if custom
|
||||||
// block is dragged between sprites without the definition.
|
// block is dragged between sprites without the definition.
|
||||||
// Match Scratch 2.0 behavior and noop.
|
// Match Scratch 2.0 behavior and noop.
|
||||||
if (paramNamesAndIds === null) {
|
if (paramNamesIdsAndDefaults === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [paramNames, paramIds] = paramNamesAndIds;
|
const [paramNames, paramIds, paramDefaults] = paramNamesIdsAndDefaults;
|
||||||
|
|
||||||
for (let i = 0; i < paramIds.length; i++) {
|
for (let i = 0; i < paramIds.length; i++) {
|
||||||
if (args.hasOwnProperty(paramIds[i])) {
|
if (args.hasOwnProperty(paramIds[i])) {
|
||||||
util.pushParam(paramNames[i], args[paramIds[i]]);
|
util.pushParam(paramNames[i], args[paramIds[i]]);
|
||||||
|
} else {
|
||||||
|
util.pushParam(paramNames[i], paramDefaults[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,12 @@ const MathUtil = require('../util/math-util');
|
||||||
const Cast = require('../util/cast');
|
const Cast = require('../util/cast');
|
||||||
const Clone = require('../util/clone');
|
const Clone = require('../util/clone');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Occluded boolean value to make its use more understandable.
|
||||||
|
* @const {boolean}
|
||||||
|
*/
|
||||||
|
const STORE_WAITING = true;
|
||||||
|
|
||||||
class Scratch3SoundBlocks {
|
class Scratch3SoundBlocks {
|
||||||
constructor (runtime) {
|
constructor (runtime) {
|
||||||
/**
|
/**
|
||||||
|
@ -10,10 +16,16 @@ class Scratch3SoundBlocks {
|
||||||
*/
|
*/
|
||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
|
|
||||||
|
this.waitingSounds = {};
|
||||||
|
|
||||||
// Clear sound effects on green flag and stop button events.
|
// 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);
|
this._clearEffectsForAllTargets = this._clearEffectsForAllTargets.bind(this);
|
||||||
if (this.runtime) {
|
if (this.runtime) {
|
||||||
|
this.runtime.on('PROJECT_STOP_ALL', this.stopAllSounds);
|
||||||
this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
|
this.runtime.on('PROJECT_STOP_ALL', this._clearEffectsForAllTargets);
|
||||||
|
this.runtime.on('STOP_FOR_TARGET', this._stopWaitingSoundsForTarget);
|
||||||
this.runtime.on('PROJECT_START', this._clearEffectsForAllTargets);
|
this.runtime.on('PROJECT_START', this._clearEffectsForAllTargets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,21 +153,44 @@ class Scratch3SoundBlocks {
|
||||||
|
|
||||||
playSound (args, util) {
|
playSound (args, util) {
|
||||||
// Don't return the promise, it's the only difference for AndWait
|
// Don't return the promise, it's the only difference for AndWait
|
||||||
this.playSoundAndWait(args, util);
|
this._playSound(args, util);
|
||||||
}
|
}
|
||||||
|
|
||||||
playSoundAndWait (args, util) {
|
playSoundAndWait (args, util) {
|
||||||
|
return this._playSound(args, util, STORE_WAITING);
|
||||||
|
}
|
||||||
|
|
||||||
|
_playSound (args, util, storeWaiting) {
|
||||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const {target} = util;
|
const {target} = util;
|
||||||
const {sprite} = target;
|
const {sprite} = target;
|
||||||
const {soundId} = sprite.sounds[index];
|
const {soundId} = sprite.sounds[index];
|
||||||
if (sprite.soundBank) {
|
if (sprite.soundBank) {
|
||||||
|
if (storeWaiting === STORE_WAITING) {
|
||||||
|
this._addWaitingSound(target.id, soundId);
|
||||||
|
} else {
|
||||||
|
this._removeWaitingSound(target.id, soundId);
|
||||||
|
}
|
||||||
return sprite.soundBank.playSound(target, 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) {
|
_getSoundIndex (soundName, util) {
|
||||||
// if the sprite has no sounds, return -1
|
// if the sprite has no sounds, return -1
|
||||||
const len = util.target.sprite.sounds.length;
|
const len = util.target.sprite.sounds.length;
|
||||||
|
@ -201,6 +236,20 @@ class Scratch3SoundBlocks {
|
||||||
_stopAllSoundsForTarget (target) {
|
_stopAllSoundsForTarget (target) {
|
||||||
if (target.sprite.soundBank) {
|
if (target.sprite.soundBank) {
|
||||||
target.sprite.soundBank.stopAllSounds(target);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
* @param {string} procedureCode Procedure code for procedure to query.
|
||||||
* @return {Array.<string>} List of param names for a procedure.
|
* @return {Array.<string>} List of param names for a procedure.
|
||||||
*/
|
*/
|
||||||
|
@ -112,6 +112,15 @@ class BlockUtility {
|
||||||
return this.thread.target.blocks.getProcedureParamNamesAndIds(procedureCode);
|
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.
|
* Store a procedure parameter value by its name.
|
||||||
* @param {string} paramName The procedure's parameter name.
|
* @param {string} paramName The procedure's parameter name.
|
||||||
|
|
|
@ -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.
|
* @param {?string} name Name of procedure to query.
|
||||||
* @return {?Array.<string>} List of param names for a procedure.
|
* @return {?Array.<string>} List of param names for a procedure.
|
||||||
*/
|
*/
|
||||||
getProcedureParamNamesAndIds (name) {
|
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];
|
const cachedNames = this._cache.procedureParamNames[name];
|
||||||
if (typeof cachedNames !== 'undefined') {
|
if (typeof cachedNames !== 'undefined') {
|
||||||
return cachedNames;
|
return cachedNames;
|
||||||
|
@ -247,7 +256,9 @@ class Blocks {
|
||||||
block.mutation.proccode === name) {
|
block.mutation.proccode === name) {
|
||||||
const names = JSON.parse(block.mutation.argumentnames);
|
const names = JSON.parse(block.mutation.argumentnames);
|
||||||
const ids = JSON.parse(block.mutation.argumentids);
|
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];
|
return this._cache.procedureParamNames[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1604,14 +1604,6 @@ class Runtime extends EventEmitter {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @return {OrderedMap} The current state of monitor blocks.
|
|
||||||
*/
|
|
||||||
getMonitorState () {
|
|
||||||
return this._monitorState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue monitor blocks to sequencer to be run.
|
* Queue monitor blocks to sequencer to be run.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -179,8 +179,7 @@ class Scratch3VideoSensingBlocks {
|
||||||
if (stage) {
|
if (stage) {
|
||||||
return stage.videoState;
|
return stage.videoState;
|
||||||
}
|
}
|
||||||
// Default to off to prevent a flash of video while the project is loading
|
return VideoState.ON;
|
||||||
return VideoState.OFF;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set globalVideoState (state) {
|
set globalVideoState (state) {
|
||||||
|
|
|
@ -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.
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||||
* TODO: parse the "info" section, especially "savedExtensions"
|
* 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.hasOwnProperty('videoOn')) {
|
||||||
if (object.info.videoOn) {
|
if (object.info.videoOn) {
|
||||||
target.videoState = RenderedTarget.VIDEO_STATE.ON;
|
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++) {
|
for (let n = 0; n < deferredMonitors.length; n++) {
|
||||||
parseMonitorObject(deferredMonitors[n], runtime, targets, extensions);
|
parseMonitorObject(deferredMonitors[n], runtime, targets, extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
extensions = confirmTargetExtensions(targets, extensions);
|
|
||||||
|
|
||||||
return targets;
|
return targets;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -766,6 +712,7 @@ const reorderParsedTargets = function (targets) {
|
||||||
return targets;
|
return targets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-level handler. Parse provided JSON,
|
* Top-level handler. Parse provided JSON,
|
||||||
* and process the top-level object (the stage object).
|
* and process the top-level object (the stage object).
|
||||||
|
|
|
@ -1074,9 +1074,6 @@ class RenderedTarget extends Target {
|
||||||
*/
|
*/
|
||||||
onStopAll () {
|
onStopAll () {
|
||||||
this.clearEffects();
|
this.clearEffects();
|
||||||
if (this.sprite.soundBank) {
|
|
||||||
this.sprite.soundBank.stopAllSounds();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -16,7 +16,7 @@ test('calling a custom block with no definition does not throw', t => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const util = {
|
const util = {
|
||||||
getProcedureParamNamesAndIds: () => null,
|
getProcedureParamNamesIdsAndDefaults: () => null,
|
||||||
stackFrame: {
|
stackFrame: {
|
||||||
executed: false
|
executed: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
test('ask and stop other scripts dismisses if it is the last question', t => {
|
||||||
const rt = new Runtime();
|
const rt = new Runtime();
|
||||||
const s = new Sensing(rt);
|
const s = new Sensing(rt);
|
||||||
const util = {target: {visible: false}, thread: {}};
|
const util = {target: {visible: false, sprite: {}}, thread: {}};
|
||||||
|
|
||||||
const expectedQuestion = 'a question';
|
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 => {
|
test('ask and stop other scripts asks next question', t => {
|
||||||
const rt = new Runtime();
|
const rt = new Runtime();
|
||||||
const s = new Sensing(rt);
|
const s = new Sensing(rt);
|
||||||
const util = {target: {visible: false}, thread: {}};
|
const util = {target: {visible: false, sprite: {}}, thread: {}};
|
||||||
const util2 = {target: {visible: false}, thread: {}};
|
const util2 = {target: {visible: false, sprite: {}}, thread: {}};
|
||||||
|
|
||||||
const expectedQuestion = 'a question';
|
const expectedQuestion = 'a question';
|
||||||
const nextQuestion = 'a followup';
|
const nextQuestion = 'a followup';
|
||||||
|
|
|
@ -102,50 +102,3 @@ test('Ordering', t => {
|
||||||
t.end();
|
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
Loading…
Reference in a new issue