Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/blocks/scratch3_looks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const Clone = require('../util/clone');
const RenderedTarget = require('../sprites/rendered-target');
const uid = require('../util/uid');
const StageLayering = require('../engine/stage-layering');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');

/**
* @typedef {object} BubbleState - the bubble state associated with a particular target.
Expand Down Expand Up @@ -272,10 +273,10 @@ class Scratch3LooksBlocks {
},
looks_costumenumbername: {
isSpriteSpecific: true,
getId: targetId => `${targetId}_costumenumbername`
getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_costumenumbername`, fields)
},
looks_backdropnumbername: {
getId: () => 'backdropnumbername'
getId: (_, fields) => getMonitorIdForBlockWithArgs('backdropnumbername', fields)
}
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/blocks/scratch3_sensing.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Cast = require('../util/cast');
const Timer = require('../util/timer');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');

class Scratch3SensingBlocks {
constructor (runtime) {
Expand Down Expand Up @@ -89,7 +90,7 @@ class Scratch3SensingBlocks {
// This is different from the default toolbox xml id in order to support
// importing multiple monitors from the same opcode from sb2 files,
// something that is not currently supported in scratch 3.
getId: (_, param) => `current_${param}`
getId: (_, fields) => getMonitorIdForBlockWithArgs('current', fields) // _${param}`
}
};
}
Expand Down
41 changes: 38 additions & 3 deletions src/engine/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const {Map} = require('immutable');
const BlocksExecuteCache = require('./blocks-execute-cache');
const log = require('../util/log');
const Variable = require('./variable');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');

/**
* @fileoverview
Expand Down Expand Up @@ -523,11 +524,20 @@ class Blocks {
changeBlock (args, optRuntime) {
// Validate
if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return;
const block = this._blocks[args.id];
let block = this._blocks[args.id];
if (typeof block === 'undefined') return;
const wasMonitored = block.isMonitored;
switch (args.element) {
case 'field':
// TODO when the field of a monitored block changes,
// update the checkbox in the flyout based on whether
// a monitor for that current combination of selected parameters exists
// e.g.
// 1. check (current [v year])
// 2. switch dropdown in flyout block to (current [v minute])
// 3. the checkbox should become unchecked if we're not already
// monitoring current minute


// Update block value
if (!block.fields[args.name]) return;
if (args.name === 'VARIABLE' || args.name === 'LIST' ||
Expand Down Expand Up @@ -559,11 +569,36 @@ class Blocks {
block.mutation = mutationAdapter(args.value);
break;
case 'checkbox': {
block.isMonitored = args.value;
if (!optRuntime) {
break;
}

// A checkbox usually has a one to one correspondence with the monitor
// block but in the case of monitored reporters that have arguments,
// map the old id to a new id, creating a new monitor block if necessary
if (block.fields && Object.keys(block.fields).length > 0 &&
block.opcode !== 'data_variable' && block.opcode !== 'data_listcontents') {

// This block has an argument which needs to get separated out into
// multiple monitor blocks with ids based on the selected argument
const newId = getMonitorIdForBlockWithArgs(block.id, block.fields);
// Note: we're not just constantly creating a longer and longer id everytime we check
// the checkbox because we're using the id of the block in the flyout as the base

// check if a block with the new id already exists, otherwise create
let newBlock = optRuntime.monitorBlocks.getBlock(newId);
if (!newBlock) {
newBlock = JSON.parse(JSON.stringify(block));
newBlock.id = newId;
optRuntime.monitorBlocks.createBlock(newBlock);
}

block = newBlock; // Carry on through the rest of this code with newBlock
}

const wasMonitored = block.isMonitored;
block.isMonitored = args.value;

// Variable blocks may be sprite specific depending on the owner of the variable
let isSpriteLocalVariable = false;
if (block.opcode === 'data_variable') {
Expand Down
34 changes: 29 additions & 5 deletions src/serialization/sb2.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,13 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
} else if (object.cmd === 'contentsOfList:') {
block.id = getVariableId(object.param, Variable.LIST_TYPE);
} else if (runtime.monitorBlockInfo.hasOwnProperty(block.opcode)) {
block.id = runtime.monitorBlockInfo[block.opcode].getId(target.id, object.param);
block.id = runtime.monitorBlockInfo[block.opcode].getId(target.id, block.fields);
} else {
// If the opcode can't be found in the runtime monitorBlockInfo,
// then default to using the block opcode as the id instead.
// This is for extension monitors, and assumes that extension monitors
// cannot be sprite specific.
block.id = block.opcode;
}

// Block needs a targetId if it is targetting something other than the stage
Expand All @@ -306,10 +312,19 @@ const parseMonitorObject = (object, runtime, targets, extensions) => {
// 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]);
const existingMonitorBlock = runtime.monitorBlocks._blocks[block.id];
if (existingMonitorBlock) {
// A monitor block already exists if the toolbox has been loaded and
// the monitor block is not target specific (because the block gets recycled).
// Update the existing block with the relevant monitor information.
existingMonitorBlock.isMonitored = object.visible;
existingMonitorBlock.targetId = block.targetId;
} else {
// 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.
Expand Down Expand Up @@ -1013,6 +1028,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
value: providedArg
};

if (expectedArg.fieldName === 'CURRENTMENU') {
// In 3.0, the field value of the `sensing_current` block
// is in all caps.
activeBlock.fields[expectedArg.fieldName].value = providedArg.toUpperCase();
if (providedArg === 'day of week') {
activeBlock.fields[expectedArg.fieldName].value = 'DAYOFWEEK';
}
}

if (expectedArg.fieldName === 'VARIABLE') {
// Add `id` property to variable fields
activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg, Variable.SCALAR_TYPE);
Expand Down
33 changes: 33 additions & 0 deletions src/util/get-monitor-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Returns a string representing a unique id for a monitored block
* where a single reporter block can have more than one monitor
* (and therefore more than one monitor block) associated
* with it (e.g. when reporter blocks have inputs).
* @param {string} baseId The base id to use for the different monitor blocks
* @param {object} fields The monitor block's fields object.
*/
// TODO this function should eventually be the single place where all monitor
// IDs are obtained given an opcode for the reporter block and the list of
// selected parameters.
const getMonitorIdForBlockWithArgs = function (id, fields) {
let fieldString = '';
for (const fieldKey in fields) {
let fieldValue = fields[fieldKey].value;
if (fieldKey === 'CURRENTMENU') {
// The 'sensing_current' block has field values in all caps.
// However, when importing from scratch 2.0, these
// could have gotten imported as lower case field values.
// Normalize the field value here so that we don't ever
// end up with a different monitor ID representing the same
// block configuration
// Note: we are not doing this for every block field that comes into
// this function so as not to make the faulty assumption that block
// field values coming in would be unique after being made lower case
fieldValue = fieldValue.toLowerCase();
}
fieldString += `_${fieldValue}`;
}
return `${id}${fieldString}`;
};

module.exports = getMonitorIdForBlockWithArgs;
Binary file modified test/fixtures/monitors.sb2
Binary file not shown.
51 changes: 46 additions & 5 deletions test/integration/monitors.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ test('importing sb2 project with monitors', t => {
// All monitors should create threads that finish during the step and
// are revoved from runtime.threads.
t.equal(threads.length, 0);
t.equal(vm.runtime._lastStepDoneThreads.length, 5);
t.equal(vm.runtime._lastStepDoneThreads.length, 8);
// There should be one additional hidden monitor that is in the monitorState but
// does not start a thread.
t.equal(vm.runtime._monitorState.size, 6);
t.equal(vm.runtime._monitorState.size, 9);

const stage = vm.runtime.targets[0];
const target = vm.runtime.targets[1];
Expand Down Expand Up @@ -56,11 +56,13 @@ test('importing sb2 project with monitors', t => {
t.equal(monitorRecord.opcode, 'data_listcontents');
t.equal(monitorRecord.mode, 'list');
t.equal(monitorRecord.visible, true);
t.equal(monitorRecord.width, 102); // Make sure these are imported from lists.
t.equal(monitorRecord.height, 202);
t.equal(monitorRecord.width, 104); // Make sure these are imported from lists.
t.equal(monitorRecord.height, 204);

// Backdrop name monitor is visible, not sprite specific
monitorRecord = vm.runtime._monitorState.get('backdropnumbername');
// should get imported with id that references the name parameter
// via '_name' at the end since the 3.0 block has a dropdown.
monitorRecord = vm.runtime._monitorState.get('backdropnumbername_name');
t.equal(monitorRecord.opcode, 'looks_backdropnumbername');
t.equal(monitorRecord.mode, 'default');
t.equal(monitorRecord.visible, true);
Expand All @@ -75,6 +77,45 @@ test('importing sb2 project with monitors', t => {
t.equal(monitorRecord.spriteName, 'Sprite1');
t.equal(monitorRecord.targetId, target.id);


let monitorId;
let monitorBlock;

// The monitor IDs for the sensing_current block should be unique
// to the parameter that is selected on the block being monitored.
// The paramater portion of the id should be lowercase even
// though the field value on the block is uppercase.

monitorId = 'current_date';
monitorRecord = vm.runtime._monitorState.get(monitorId);
t.equal(monitorRecord.opcode, 'sensing_current');
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DATE');
t.equal(monitorRecord.mode, 'default');
t.equal(monitorRecord.visible, true);
t.equal(monitorRecord.spriteName, null);
t.equal(monitorRecord.targetId, null);

monitorId = 'current_minute';
monitorRecord = vm.runtime._monitorState.get(monitorId);
t.equal(monitorRecord.opcode, 'sensing_current');
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
t.equal(monitorBlock.fields.CURRENTMENU.value, 'MINUTE');
t.equal(monitorRecord.mode, 'default');
t.equal(monitorRecord.visible, true);
t.equal(monitorRecord.spriteName, null);
t.equal(monitorRecord.targetId, null);

monitorId = 'current_dayofweek';
monitorRecord = vm.runtime._monitorState.get(monitorId);
t.equal(monitorRecord.opcode, 'sensing_current');
monitorBlock = vm.runtime.monitorBlocks.getBlock(monitorId);
t.equal(monitorBlock.fields.CURRENTMENU.value, 'DAYOFWEEK');
t.equal(monitorRecord.mode, 'default');
t.equal(monitorRecord.visible, true);
t.equal(monitorRecord.spriteName, null);
t.equal(monitorRecord.targetId, null);

t.end();
process.nextTick(process.exit);
});
Expand Down