diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index 92500ac03..0f47d9cfc 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -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' + } }; } diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index ce3f1ac46..0e3420422 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -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` + } }; } diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 93b8fb26b..0c71876ee 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -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}` + } }; } diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index c4bb6039e..bb9e61838 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -132,7 +132,9 @@ class Scratch3SoundBlocks { getMonitored () { return { - sound_volume: {} + sound_volume: { + getId: () => 'volume' + } }; } diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 9af98db59..b72901558 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -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; diff --git a/src/engine/monitor-record.js b/src/engine/monitor-record.js index d42b878f5..309e5126f 100644 --- a/src/engine/monitor-record.js +++ b/src/engine/monitor-record.js @@ -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; diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 4c7c31862..f50e7f1ba 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -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.} 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; }) diff --git a/src/serialization/sb2_specmap.js b/src/serialization/sb2_specmap.js index 797dce08b..e22025285 100644 --- a/src/serialization/sb2_specmap.js +++ b/src/serialization/sb2_specmap.js @@ -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: [