Import monitors from sb2 files.

Paired with @kchadha on all of this.
This commit is contained in:
Paul Kaplan 2018-05-08 14:09:18 -04:00
parent 784705d46e
commit 4713f47fb7
8 changed files with 196 additions and 22 deletions
src/serialization

View file

@ -14,6 +14,7 @@ const uid = require('../util/uid');
const StringUtil = require('../util/string-util');
const specMap = require('./sb2_specmap');
const Variable = require('../engine/variable');
const MonitorRecord = require('../engine/monitor-record');
const {loadCostume} = require('../import/load-costume.js');
const {loadSound} = require('../import/load-sound.js');
@ -208,6 +209,104 @@ const globalBroadcastMsgStateGenerator = (function () {
};
}());
/**
* Parse a single monitor object and create all its in-memory VM objects.
* @param {!object} object - From-JSON "Scratch object"
* @param {!Runtime} runtime - (in/out) Runtime object to load monitor info into.
* @param {!Array.<Target>} targets - Targets have already been parsed.
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
*/
const parseMonitorObject = (object, runtime, targets, extensions) => {
let target = null;
// List blocks don't come in with their target name set.
// Find the target by searching for a target with matching variable name/type.
if (!object.hasOwnProperty('target')) {
for (let i = 0; i < targets.length; i++) {
const currTarget = targets[i];
const listVariables = Object.keys(currTarget.variables).filter(key => {
const variable = currTarget.variables[key];
return variable.type === Variable.LIST_TYPE && variable.name === object.listName;
});
if (listVariables.length > 0) {
target = currTarget; // Keep this target for later use
object.target = currTarget.getName(); // Set target name to normalize with other monitors
}
}
}
// Create a block for the monitor blocks container
target = target || targets.filter(t => t.getName() === object.target)[0];
if (!target) throw new Error('Cannot create monitor for target that cannot be found by name');
// Create var id getter to make block naming/parsing easier, variables already created.
const getVariableId = generateVariableIdGetter(target.id, false);
// eslint-disable-next-line no-use-before-define
const block = parseBlock(
[object.cmd, object.param], // Scratch 2 monitor blocks only have one param.
null, // `addBroadcastMsg`, not needed for monitor blocks.
getVariableId,
extensions
);
let isSpriteLocalVariable;
if (object.cmd === 'getVar:' || object.cmd === 'contentsOfList:') {
// These monitors are sprite-specific if they are not targetting the stage.
isSpriteLocalVariable = object.target.isStage;
// Variable getters have special block IDs for the toolbox that match the variable ID.
block.id = getVariableId(object.param);
}
block.id = runtime.monitorBlockInfo.hasOwnProperty(block.opcode) ?
runtime.monitorBlockInfo[block.opcode].getId(target.id, object.param) : block.id;
// Block needs a targetId if it is sprite specific or a local variable.
// Consult the monitorBlockInfo in the runtime for sprite-specificity.
const isSpriteSpecific = isSpriteLocalVariable ||
(runtime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
runtime.monitorBlockInfo[block.opcode].isSpriteSpecific);
block.targetId = isSpriteSpecific ? target.id : null;
// Property required for running monitored blocks.
block.isMonitored = object.visible;
// Blocks can be created with children, flatten and add to monitorBlocks.
const newBlocks = flatten([block]);
for (let i = 0; i < newBlocks.length; i++) {
runtime.monitorBlocks.createBlock(newBlocks[i]);
}
// Convert numbered mode into strings for better understandability.
switch (object.mode) {
case 1:
object.mode = 'default';
break;
case 2:
object.mode = 'large';
break;
case 3:
object.mode = 'slider';
break;
}
// Create a monitor record for the runtime's monitorState
runtime.requestAddMonitor(MonitorRecord({
id: block.id,
targetId: block.targetId,
spriteName: block.targetId ? object.target : null,
opcode: block.opcode,
params: runtime.monitorBlocks._getBlockParams(block),
value: '',
mode: object.mode,
sliderMin: object.sliderMin,
sliderMax: object.sliderMax,
x: object.x,
y: object.y,
width: object.width,
height: object.height,
visible: object.visible
}));
};
/**
* Parse a single "Scratch object" and create all its in-memory VM objects.
* TODO: parse the "info" section, especially "savedExtensions"
@ -220,10 +319,17 @@ const globalBroadcastMsgStateGenerator = (function () {
*/
const parseScratchObject = function (object, runtime, extensions, topLevel, zip) {
if (!object.hasOwnProperty('objName')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
return Promise.resolve(null);
if (object.hasOwnProperty('listName')) {
// Shim these objects so they can be processed as monitors
object.cmd = 'contentsOfList:';
object.param = object.listName;
object.mode = 'list';
}
// Defer parsing monitors until targets are all parsed
object.deferredMonitor = true;
return Promise.resolve(object);
}
// Blocks container for this object.
const blocks = new Blocks();
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
@ -332,7 +438,6 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
if (object.hasOwnProperty('lists')) {
for (let k = 0; k < object.lists.length; k++) {
const list = object.lists[k];
// @todo: monitor properties.
const newVariable = new Variable(
getVariableId(list.listName),
list.listName,
@ -455,8 +560,18 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
}
}
let targets = [target];
const deferredMonitors = [];
for (let n = 0; n < children.length; n++) {
targets = targets.concat(children[n]);
if (children[n]) {
if (children[n].deferredMonitor) {
deferredMonitors.push(children[n]);
} else {
targets = targets.concat(children[n]);
}
}
}
for (let n = 0; n < deferredMonitors.length; n++) {
parseMonitorObject(deferredMonitors[n], runtime, targets, extensions);
}
return targets;
})

View file

@ -1370,6 +1370,18 @@ const specMap = {
}
]
},
// Scratch 2 uses this alternative variable getter opcode only in monitors,
// blocks use the `readVariable` opcode above.
'getVar:': {
opcode: 'data_variable',
argMap: [
{
type: 'field',
fieldName: 'VARIABLE',
variableType: Variable.SCALAR_TYPE
}
]
},
'setVar:to:': {
opcode: 'data_setvariableto',
argMap: [