mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
Load extensions when loading a project or sprite
- Accumulate extension info while deserializing JSON - Install extensions (if any) before installing target(s) - Merged 'install' steps of the "add sprite" and "load project" paths
This commit is contained in:
parent
c5d3e2dbb4
commit
0af3de9bf0
4 changed files with 306 additions and 55 deletions
|
@ -78,15 +78,16 @@ const flatten = function (blocks) {
|
|||
* or a list of blocks in an argument (e.g., move [pick random...]).
|
||||
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {Array.<object>} Scratch VM-format block list.
|
||||
*/
|
||||
const parseBlockList = function (blockList, getVariableId) {
|
||||
const parseBlockList = function (blockList, getVariableId, extensions) {
|
||||
const resultingList = [];
|
||||
let previousBlock = null; // For setting next.
|
||||
for (let i = 0; i < blockList.length; i++) {
|
||||
const block = blockList[i];
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const parsedBlock = parseBlock(block, getVariableId);
|
||||
const parsedBlock = parseBlock(block, getVariableId, extensions);
|
||||
if (typeof parsedBlock === 'undefined') continue;
|
||||
if (previousBlock) {
|
||||
parsedBlock.parent = previousBlock.id;
|
||||
|
@ -104,14 +105,15 @@ const parseBlockList = function (blockList, getVariableId) {
|
|||
* @param {!object} scripts Scripts object from SB2 JSON.
|
||||
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
*/
|
||||
const parseScripts = function (scripts, blocks, getVariableId) {
|
||||
const parseScripts = function (scripts, blocks, getVariableId, extensions) {
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const script = scripts[i];
|
||||
const scriptX = script[0];
|
||||
const scriptY = script[1];
|
||||
const blockList = script[2];
|
||||
const parsedBlockList = parseBlockList(blockList, getVariableId);
|
||||
const parsedBlockList = parseBlockList(blockList, getVariableId, extensions);
|
||||
if (parsedBlockList[0]) {
|
||||
// Adjust script coordinates to account for
|
||||
// larger block size in scratch-blocks.
|
||||
|
@ -155,12 +157,14 @@ const generateVariableIdGetter = (function () {
|
|||
|
||||
/**
|
||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @param {boolean} topLevel Whether this is the top-level object (stage).
|
||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
||||
* TODO: parse the "info" section, especially "savedExtensions"
|
||||
* @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher.
|
||||
* @param {!Runtime} runtime - Runtime object to load all structures into.
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @param {boolean} topLevel - Whether this is the top-level object (stage).
|
||||
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime, topLevel) {
|
||||
const parseScratchObject = function (object, runtime, extensions, topLevel) {
|
||||
if (!object.hasOwnProperty('objName')) {
|
||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||
// @todo
|
||||
|
@ -228,7 +232,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
|||
|
||||
// If included, parse any and all scripts/blocks on the object.
|
||||
if (object.hasOwnProperty('scripts')) {
|
||||
parseScripts(object.scripts, blocks, getVariableId);
|
||||
parseScripts(object.scripts, blocks, getVariableId, extensions);
|
||||
}
|
||||
|
||||
if (object.hasOwnProperty('lists')) {
|
||||
|
@ -287,7 +291,7 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
|||
const childrenPromises = [];
|
||||
if (object.children) {
|
||||
for (let m = 0; m < object.children.length; m++) {
|
||||
childrenPromises.push(parseScratchObject(object.children[m], runtime, false));
|
||||
childrenPromises.push(parseScratchObject(object.children[m], runtime, extensions, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,31 +316,42 @@ const parseScratchObject = function (object, runtime, topLevel) {
|
|||
* @param {!object} json SB2-format JSON to load.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @param {boolean=} optForceSprite If set, treat as sprite (Sprite2).
|
||||
* @return {?Promise} Promise that resolves to the loaded targets when ready.
|
||||
* @return {Promise.<ImportedProject>} Promise that resolves to the loaded targets when ready.
|
||||
*/
|
||||
const sb2import = function (json, runtime, optForceSprite) {
|
||||
return parseScratchObject(
|
||||
json,
|
||||
runtime,
|
||||
!optForceSprite
|
||||
);
|
||||
const extensions = {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
return parseScratchObject(json, runtime, extensions, !optForceSprite)
|
||||
.then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a single SB2 JSON-formatted block and its children.
|
||||
* @param {!object} sb2block SB2 JSON-formatted block.
|
||||
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
||||
* @return {object} Scratch VM format block.
|
||||
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {object} Scratch VM format block, or null if unsupported object.
|
||||
*/
|
||||
const parseBlock = function (sb2block, getVariableId) {
|
||||
const parseBlock = function (sb2block, getVariableId, extensions) {
|
||||
// First item in block object is the old opcode (e.g., 'forward:').
|
||||
const oldOpcode = sb2block[0];
|
||||
// Convert the block using the specMap. See sb2specmap.js.
|
||||
if (!oldOpcode || !specMap[oldOpcode]) {
|
||||
log.warn('Couldn\'t find SB2 block: ', oldOpcode);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const blockMetadata = specMap[oldOpcode];
|
||||
// If the block is from an extension, record it.
|
||||
const dotIndex = blockMetadata.opcode.indexOf('.');
|
||||
if (dotIndex >= 0) {
|
||||
const extension = blockMetadata.opcode.substring(0, dotIndex);
|
||||
extensions.extensionIDs.add(extension);
|
||||
}
|
||||
// Block skeleton.
|
||||
const activeBlock = {
|
||||
id: uid(), // Generate a new block unique ID.
|
||||
|
@ -373,10 +388,10 @@ const parseBlock = function (sb2block, getVariableId) {
|
|||
let innerBlocks;
|
||||
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
||||
// Block list occupies the input.
|
||||
innerBlocks = parseBlockList(providedArg, getVariableId);
|
||||
innerBlocks = parseBlockList(providedArg, getVariableId, extensions);
|
||||
} else {
|
||||
// Single block occupies the input.
|
||||
innerBlocks = [parseBlock(providedArg, getVariableId)];
|
||||
innerBlocks = [parseBlock(providedArg, getVariableId, extensions)];
|
||||
}
|
||||
let previousBlock = null;
|
||||
for (let j = 0; j < innerBlocks.length; j++) {
|
||||
|
|
|
@ -21,6 +21,24 @@
|
|||
* properties. By hand, I matched the opcode name to the 3.0 opcode.
|
||||
* Finally, I filled in the expected arguments as below.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SB2SpecMap_blockInfo
|
||||
* @property {string} opcode - the Scratch 3.0 block opcode. Use 'extensionID.opcode' for extension opcodes.
|
||||
* @property {Array.<SB2SpecMap_argInfo>} argMap - metadata for this block's arguments.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SB2SpecMap_argInfo
|
||||
* @property {string} type - the type of this arg (such as 'input' or 'field')
|
||||
* @property {string} inputOp - the scratch-blocks shadow type for this arg
|
||||
* @property {string} inputName - the name this argument will take when provided to the block implementation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mapping of Scratch 2.0 opcode to Scratch 3.0 block metadata.
|
||||
* @type {object.<SB2SpecMap_blockInfo>}
|
||||
*/
|
||||
const specMap = {
|
||||
'forward:': {
|
||||
opcode: 'motion_movesteps',
|
||||
|
@ -1376,4 +1394,179 @@ const specMap = {
|
|||
argMap: []
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add to the specMap entries for an opcode from a Scratch 2.0 extension. Two entries will be made with the same
|
||||
* metadata; this is done to support projects saved by both older and newer versions of the Scratch 2.0 editor.
|
||||
* @param {string} sb2Extension - the Scratch 2.0 name of the extension
|
||||
* @param {string} sb2Opcode - the Scratch 2.0 opcode
|
||||
* @param {SB2SpecMap_blockInfo} blockInfo - the Scratch 3.0 block info
|
||||
*/
|
||||
const addExtensionOp = function (sb2Extension, sb2Opcode, blockInfo) {
|
||||
/**
|
||||
* This string separates the name of an extension and the name of an opcode in more recent Scratch 2.0 projects.
|
||||
* Earlier projects used '.' as a separator, up until we added the 'LEGO WeDo 2.0' extension...
|
||||
* @type {string}
|
||||
*/
|
||||
const sep = '\u001F'; // Unicode Unit Separator
|
||||
|
||||
// make one entry for projects saved by recent versions of the Scratch 2.0 editor
|
||||
specMap[`${sb2Extension}${sep}${sb2Opcode}`] = blockInfo;
|
||||
|
||||
// make a second for projects saved by older versions of the Scratch 2.0 editor
|
||||
specMap[`${sb2Extension}.${sb2Opcode}`] = blockInfo;
|
||||
};
|
||||
|
||||
const weDo2 = 'LEGO WeDo 2.0';
|
||||
|
||||
addExtensionOp(weDo2, 'motorOnFor', {
|
||||
opcode: 'wedo2.motorOnFor',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'DURATION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'motorOn', {
|
||||
opcode: 'wedo2.motorOn',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'MOTOR_ID'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'motorOff', {
|
||||
opcode: 'wedo2.motorOff',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'MOTOR_ID'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'startMotorPower', {
|
||||
opcode: 'wedo2.startMotorPower',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'POWER'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'setMotorDirection', {
|
||||
opcode: 'wedo2.setMotorDirection',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'MOTOR_ID'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'setLED', {
|
||||
opcode: 'wedo2.setLightHue',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'HUE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'playNote', {
|
||||
opcode: 'wedo2.playNoteFor',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'NOTE'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'DURATION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'whenDistance', {
|
||||
opcode: 'wedo2.whenDistance',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'OP'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'math_number',
|
||||
inputName: 'REFERENCE'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'whenTilted', {
|
||||
opcode: 'wedo2.whenTilted',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'getDistance', {
|
||||
opcode: 'wedo2.motorOn'
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'isTilted', {
|
||||
opcode: 'wedo2.motorOn',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
addExtensionOp(weDo2, 'getTilt', {
|
||||
opcode: 'getTiltAngle',
|
||||
argMap: [
|
||||
{
|
||||
type: 'input',
|
||||
inputOp: 'text',
|
||||
inputName: 'DIRECTION'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = specMap;
|
||||
|
|
|
@ -13,6 +13,18 @@ const List = require('../engine/list');
|
|||
const {loadCostume} = require('../import/load-costume.js');
|
||||
const {loadSound} = require('../import/load-sound.js');
|
||||
|
||||
/**
|
||||
* @typedef {object} ImportedProject
|
||||
* @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
|
||||
* @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ImportedExtensionsInfo
|
||||
* @property {Set.<string>} extensionIDs - the ID of each extension actually in use by blocks in this project.
|
||||
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serializes the specified VM runtime.
|
||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||
|
@ -41,13 +53,14 @@ const serialize = function (runtime) {
|
|||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @return {?Target} Target created (stage or sprite).
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime) {
|
||||
const parseScratchObject = function (object, runtime, extensions) {
|
||||
if (!object.hasOwnProperty('name')) {
|
||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||
// @todo
|
||||
return;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
// Blocks container for this object.
|
||||
const blocks = new Blocks();
|
||||
|
@ -61,7 +74,14 @@ const parseScratchObject = function (object, runtime) {
|
|||
}
|
||||
if (object.hasOwnProperty('blocks')) {
|
||||
for (const blockId in object.blocks) {
|
||||
blocks.createBlock(object.blocks[blockId]);
|
||||
const blockJSON = object.blockType[blockId];
|
||||
blocks.createBlock(blockJSON);
|
||||
|
||||
const dotIndex = blockJSON.opcode.indexOf('.');
|
||||
if (dotIndex >= 0) {
|
||||
const extensionId = blockJSON.opcode.substring(0, dotIndex);
|
||||
extensions.extensionIDs.add(extensionId);
|
||||
}
|
||||
}
|
||||
// console.log(blocks);
|
||||
}
|
||||
|
@ -155,14 +175,23 @@ const parseScratchObject = function (object, runtime) {
|
|||
};
|
||||
|
||||
/**
|
||||
* Deserializes the specified representation of a VM runtime and loads it into
|
||||
* the provided runtime instance.
|
||||
* @param {object} json JSON representation of a VM runtime.
|
||||
* @param {Runtime} runtime Runtime instance
|
||||
* @returns {Promise} Promise that resolves to the list of targets after the project is deserialized
|
||||
* Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
|
||||
* TODO: parse extension info (also, design extension info storage...)
|
||||
* @param {object} json - JSON representation of a VM runtime.
|
||||
* @param {Runtime} runtime - Runtime instance
|
||||
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
|
||||
*/
|
||||
const deserialize = function (json, runtime) {
|
||||
return Promise.all((json.targets || []).map(target => parseScratchObject(target, runtime)));
|
||||
const extensions = {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
return Promise.all(
|
||||
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions))
|
||||
).then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -33,7 +33,7 @@ class VirtualMachine extends EventEmitter {
|
|||
/**
|
||||
* The "currently editing"/selected target ID for the VM.
|
||||
* Block events from any Blockly workspace are routed to this target.
|
||||
* @type {!string}
|
||||
* @type {Target}
|
||||
*/
|
||||
this.editingTarget = null;
|
||||
// Runtime emits are passed along as VM emits.
|
||||
|
@ -228,19 +228,41 @@ class VirtualMachine extends EventEmitter {
|
|||
deserializer = sb2;
|
||||
}
|
||||
|
||||
return deserializer.deserialize(json, this.runtime).then(targets => {
|
||||
this.clear();
|
||||
for (let n = 0; n < targets.length; n++) {
|
||||
if (targets[n] !== null) {
|
||||
this.runtime.targets.push(targets[n]);
|
||||
targets[n].updateAllDrawableProperties();
|
||||
}
|
||||
return deserializer.deserialize(json, this.runtime)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets.
|
||||
* @param {Array.<Target>} targets - the targets to be installed
|
||||
* @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
|
||||
* @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
|
||||
*/
|
||||
installTargets (targets, extensions, wholeProject) {
|
||||
const extensionPromises = [];
|
||||
extensions.extensionIDs.forEach(extensionID => {
|
||||
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
||||
const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
|
||||
extensionPromises.push(this.extensionManager.loadExtensionURL(extensionURL));
|
||||
}
|
||||
});
|
||||
|
||||
targets = targets.filter(target => !!target);
|
||||
|
||||
Promise.all(extensionPromises).then(() => {
|
||||
if (wholeProject) {
|
||||
this.clear();
|
||||
}
|
||||
targets.forEach(target => {
|
||||
this.runtime.targets.push(target);
|
||||
(/** @type RenderedTarget */ target).updateAllDrawableProperties();
|
||||
});
|
||||
// Select the first target for editing, e.g., the first sprite.
|
||||
if (this.runtime.targets.length > 1) {
|
||||
this.editingTarget = this.runtime.targets[1];
|
||||
if (wholeProject && (targets.length > 1)) {
|
||||
this.editingTarget = targets[1];
|
||||
} else {
|
||||
this.editingTarget = this.runtime.targets[0];
|
||||
this.editingTarget = targets[0];
|
||||
}
|
||||
|
||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||
|
@ -267,17 +289,9 @@ class VirtualMachine extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// Select new sprite.
|
||||
return sb2.deserialize(json, this.runtime, true).then(targets => {
|
||||
this.runtime.targets.push(targets[0]);
|
||||
this.editingTarget = targets[0];
|
||||
this.editingTarget.updateAllDrawableProperties();
|
||||
|
||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||
this.emitTargetsUpdate();
|
||||
this.emitWorkspaceUpdate();
|
||||
this.runtime.setEditingTarget(this.editingTarget);
|
||||
});
|
||||
return sb2.deserialize(json, this.runtime, true)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue