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

View file

@ -258,9 +258,17 @@ class Scratch3LooksBlocks {
getMonitored () {
return {
looks_size: {isSpriteSpecific: true},
looks_costumenumbername: {isSpriteSpecific: true},
looks_backdropnumbername: {}
looks_size: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_size`
},
looks_costumenumbername: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_costumenumbername`
},
looks_backdropnumbername: {
getId: () => 'backdropnumbername'
}
};
}

View file

@ -46,9 +46,18 @@ class Scratch3MotionBlocks {
getMonitored () {
return {
motion_xposition: {isSpriteSpecific: true},
motion_yposition: {isSpriteSpecific: true},
motion_direction: {isSpriteSpecific: true}
motion_xposition: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_xposition`
},
motion_yposition: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_yposition`
},
motion_direction: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_direction`
}
};
}

View file

@ -74,10 +74,18 @@ class Scratch3SensingBlocks {
getMonitored () {
return {
sensing_answer: {},
sensing_loudness: {},
sensing_timer: {},
sensing_current: {}
sensing_answer: {
getId: () => 'answer'
},
sensing_loudness: {
getId: () => 'loudness'
},
sensing_timer: {
getId: () => 'timer'
},
sensing_current: {
getId: (_, param) => `current_${param}`
}
};
}

View file

@ -132,7 +132,9 @@ class Scratch3SoundBlocks {
getMonitored () {
return {
sound_volume: {}
sound_volume: {
getId: () => 'volume'
}
};
}

View file

@ -441,22 +441,34 @@ class Blocks {
break;
}
const isSpriteSpecific = optRuntime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
optRuntime.monitorBlockInfo[block.opcode].isSpriteSpecific;
// Variable blocks may be sprite specific depending on the owner of the variable
let isSpriteLocalVariable = false;
if (block.opcode === 'data_variable') {
isSpriteLocalVariable = !optRuntime.getEditingTarget().isStage &&
optRuntime.getEditingTarget().variables[block.fields.VARIABLE.id];
} else if (block.opcode === 'data_listcontents') {
isSpriteLocalVariable = !optRuntime.getEditingTarget().isStage &&
optRuntime.getEditingTarget().variables[block.fields.LIST.id];
}
const isSpriteSpecific = isSpriteLocalVariable ||
(optRuntime.monitorBlockInfo.hasOwnProperty(block.opcode) &&
optRuntime.monitorBlockInfo[block.opcode].isSpriteSpecific);
block.targetId = isSpriteSpecific ? optRuntime.getEditingTarget().id : null;
if (wasMonitored && !block.isMonitored) {
optRuntime.requestRemoveMonitor(block.id);
} else if (!wasMonitored && block.isMonitored) {
optRuntime.requestAddMonitor(MonitorRecord({
// @todo(vm#564) this will collide if multiple sprites use same block
id: block.id,
targetId: block.targetId,
spriteName: block.targetId ? optRuntime.getTargetById(block.targetId).getName() : null,
opcode: block.opcode,
params: this._getBlockParams(block),
// @todo(vm#565) for numerical values with decimals, some countries use comma
value: ''
value: '',
mode: block.opcode === 'data_listcontents' ? 'list' : 'default'
}));
}
break;

View file

@ -1,14 +1,22 @@
const {Record} = require('immutable');
const MonitorRecord = Record({
id: null,
id: null, // Block Id
/** Present only if the monitor is sprite-specific, such as x position */
spriteName: null,
/** Present only if the monitor is sprite-specific, such as x position */
targetId: null,
opcode: null,
value: null,
params: null
params: null,
mode: 1, // 1=default, 2=big, 3=slider
sliderMin: 0,
sliderMax: 100,
x: 0,
y: 0,
width: 0,
height: 0,
visible: true
});
module.exports = MonitorRecord;

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: [