From 413d113dda2c8ba738ab850be316f64be6123b96 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Fri, 8 Dec 2017 12:19:11 -0500 Subject: [PATCH 1/7] Adding support for blocks to be plugged into the input of broadcast blocks. SB2 Import to come. --- src/blocks/scratch3_event.js | 25 +++++++++++++++++++++---- src/engine/execute.js | 29 ++++++++++++++++++++++++++++- src/engine/target.js | 19 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 5ee2fab1d..3b4050b62 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -55,9 +55,27 @@ class Scratch3EventBlocks { return false; } + /** + * Helper function to process broadcast block input (whether it's + * input from the dropdown menu or from a plugged in input block) + * @param {object} args The given arguments for the broadcast blocks + * @param {object} util The utility associated with this block. + * @return {?Variable} The broadcast message variable that matches + * the provided input. + */ + processBroadcastInput_ (args, util) { + let broadcastInput; + if (args.BROADCAST_OPTION) { + broadcastInput = util.runtime.getTargetForStage().lookupBroadcastMsg( + args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); + } else { + broadcastInput = util.runtime.getTargetForStage().lookupBroadcastByInputValue(args.BROADCAST_INPUT.name); + } + return broadcastInput; + } + broadcast (args, util) { - const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( - args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); + const broadcastVar = this.processBroadcastInput_(args, util); if (broadcastVar) { const broadcastOption = broadcastVar.name; util.startHats('event_whenbroadcastreceived', { @@ -67,8 +85,7 @@ class Scratch3EventBlocks { } broadcastAndWait (args, util) { - const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( - args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); + const broadcastVar = this.processBroadcastInput_(args, util); if (broadcastVar) { const broadcastOption = broadcastVar.name; // Have we run before, starting threads? diff --git a/src/engine/execute.js b/src/engine/execute.js index 7dcf80d84..a4b9404d8 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -2,6 +2,7 @@ const BlockUtility = require('./block-utility'); const log = require('../util/log'); const Thread = require('./thread'); const {Map} = require('immutable'); +const cast = require('../util/cast'); /** * Single BlockUtility instance reused by execute for every pritimive ran. @@ -211,7 +212,33 @@ const execute = function (sequencer, thread) { currentStackFrame.waitingReporter = null; thread.popStack(); } - argValues[inputName] = currentStackFrame.reported[inputName]; + const inputValue = currentStackFrame.reported[inputName]; + if (inputName === 'BROADCAST_INPUT') { + const broadcastInput = inputs[inputName]; + // Check if something is plugged into the broadcast block, or + // if the shadow dropdown menu is being used. + // Differentiate between these two cases by giving argValues + // a 'BROADCAST_INPUT' field or a 'BROADCAST_OPTION' field + // respectively. + if (broadcastInput.block === broadcastInput.shadow) { + // Shadow dropdown menu is being used. + // Get the appropriate information out of it. + const shadow = blockContainer.getBlock(broadcastInput.shadow); + const broadcastField = shadow.fields.BROADCAST_OPTION; + argValues.BROADCAST_OPTION = { + id: broadcastField.id, + name: broadcastField.value + }; + } else { + // Something is plugged into the broadcast input. + // Cast it to a string. We don't need an id here. + argValues.BROADCAST_INPUT = { + name: cast.toString(inputValue) + }; + } + } else { + argValues[inputName] = inputValue; + } } // Add any mutation to args (e.g., for procedures). diff --git a/src/engine/target.js b/src/engine/target.js index 4e574177e..480b1e83c 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -115,6 +115,23 @@ class Target extends EventEmitter { } } + /** + * Look up a broadcast message with the given name and return the variable + * if it exists. Does not create a new broadcast message variable if + * it doesn't exist. + * @param {string} name Name of the variable. + * @return {?Variable} Variable object. + */ + lookupBroadcastByInputValue (name) { + const vars = this.variables; + for (const propName in vars) { + if ((vars[propName].type === Variable.BROADCAST_MESSAGE_TYPE) && + (vars[propName].name.toLowerCase() === name.toLowerCase())) { + return vars[propName]; + } + } + } + /** * Look up a variable object. * Search begins for local variables; then look for globals. @@ -141,7 +158,7 @@ class Target extends EventEmitter { * Search begins for local lists; then look for globals. * @param {!string} id Id of the list. * @param {!string} name Name of the list. - * @return {!List} List object. + * @return {!Varible} Variable object representing the found/created list. */ lookupOrCreateList (id, name) { const list = this.lookupVariableById(id); From c0e4ae455c42c49927d6a66d2fcb1213cc4eefe5 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Wed, 13 Dec 2017 13:41:22 -0500 Subject: [PATCH 2/7] Streamlining broadcast block execution. When arg information is provided to execute function, corresponding broadcast message is looked up by id, if provided, or if not, then name. If neither id nor name is provided, we can't look up the broadcast message and something is wrong, so an error is logged. We no longer need to differentiate between the shadow menu vs. plugged in input cases via the argValue name (e.g. 'BROADCAST_OPTION' vs. 'BROADCAST_INPUT'). This refactor also helps with sb2 import, coming in the next commit. --- src/blocks/scratch3_event.js | 25 ++++--------------------- src/engine/execute.js | 5 +---- src/engine/target.js | 13 ++++++++++--- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 3b4050b62..5ee2fab1d 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -55,27 +55,9 @@ class Scratch3EventBlocks { return false; } - /** - * Helper function to process broadcast block input (whether it's - * input from the dropdown menu or from a plugged in input block) - * @param {object} args The given arguments for the broadcast blocks - * @param {object} util The utility associated with this block. - * @return {?Variable} The broadcast message variable that matches - * the provided input. - */ - processBroadcastInput_ (args, util) { - let broadcastInput; - if (args.BROADCAST_OPTION) { - broadcastInput = util.runtime.getTargetForStage().lookupBroadcastMsg( - args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); - } else { - broadcastInput = util.runtime.getTargetForStage().lookupBroadcastByInputValue(args.BROADCAST_INPUT.name); - } - return broadcastInput; - } - broadcast (args, util) { - const broadcastVar = this.processBroadcastInput_(args, util); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( + args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); if (broadcastVar) { const broadcastOption = broadcastVar.name; util.startHats('event_whenbroadcastreceived', { @@ -85,7 +67,8 @@ class Scratch3EventBlocks { } broadcastAndWait (args, util) { - const broadcastVar = this.processBroadcastInput_(args, util); + const broadcastVar = util.runtime.getTargetForStage().lookupBroadcastMsg( + args.BROADCAST_OPTION.id, args.BROADCAST_OPTION.name); if (broadcastVar) { const broadcastOption = broadcastVar.name; // Have we run before, starting threads? diff --git a/src/engine/execute.js b/src/engine/execute.js index a4b9404d8..13742ec76 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -217,9 +217,6 @@ const execute = function (sequencer, thread) { const broadcastInput = inputs[inputName]; // Check if something is plugged into the broadcast block, or // if the shadow dropdown menu is being used. - // Differentiate between these two cases by giving argValues - // a 'BROADCAST_INPUT' field or a 'BROADCAST_OPTION' field - // respectively. if (broadcastInput.block === broadcastInput.shadow) { // Shadow dropdown menu is being used. // Get the appropriate information out of it. @@ -232,7 +229,7 @@ const execute = function (sequencer, thread) { } else { // Something is plugged into the broadcast input. // Cast it to a string. We don't need an id here. - argValues.BROADCAST_INPUT = { + argValues.BROADCAST_OPTION = { name: cast.toString(inputValue) }; } diff --git a/src/engine/target.js b/src/engine/target.js index 480b1e83c..9861c455d 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -98,12 +98,19 @@ class Target extends EventEmitter { * if it exists. * @param {string} id Id of the variable. * @param {string} name Name of the variable. - * @return {!Variable} Variable object. + * @return {?Variable} Variable object. */ lookupBroadcastMsg (id, name) { - const broadcastMsg = this.lookupVariableById(id); + let broadcastMsg; + if (id) { + broadcastMsg = this.lookupVariableById(id); + } else if (name) { + broadcastMsg = this.lookupBroadcastByInputValue(name); + } else { + log.error('Cannot find broadcast message if neither id nor name are provided.'); + } if (broadcastMsg) { - if (broadcastMsg.name !== name) { + if (name && (broadcastMsg.name.toLowerCase() !== name.toLowerCase())) { log.error(`Found broadcast message with id: ${id}, but` + `its name, ${broadcastMsg.name} did not match expected name ${name}.`); } From 154987fbb064f64c99067d8ca2046b25c15f50ad Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Wed, 13 Dec 2017 17:15:18 -0500 Subject: [PATCH 3/7] SB2 Import functionality for broadcast blocks with pluggable inputs. --- src/serialization/sb2.js | 37 +++++++++++++++++++++++++++----- src/serialization/sb2_specmap.js | 10 +++++---- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index d19175dca..a8b750b3d 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -170,12 +170,17 @@ const generateVariableIdGetter = (function () { const globalBroadcastMsgStateGenerator = (function () { let broadcastMsgNameMap = {}; + const emptyStringName = uid(); return function (topLevel) { if (topLevel) broadcastMsgNameMap = {}; return { broadcastMsgMapUpdater: function (name) { + name = name.toLowerCase(); + if (name === '') { + name = emptyStringName; + } broadcastMsgNameMap[name] = `broadcastMsgId-${name}`; - return broadcastMsgNameMap[name]; + return {name: name, id: broadcastMsgNameMap[name]}; }, globalBroadcastMsgs: broadcastMsgNameMap }; @@ -494,15 +499,33 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension if (shadowObscured) { fieldValue = '#990000'; } + } else if (expectedArg.inputOp === 'event_broadcast_menu') { + fieldName = 'BROADCAST_OPTION'; + if (shadowObscured) { + fieldValue = ''; + } } else if (shadowObscured) { // Filled drop-down menu. fieldValue = ''; } const fields = {}; fields[fieldName] = { - name: fieldName, - value: fieldValue + name: fieldName }; + // event_broadcast_menus have some extra properties to add to the + // field and a different value than the rest + if (expectedArg.inputOp === 'event_broadcast_menu') { + if (!shadowObscured) { + const broadcastInfo = addBroadcastMsg(fieldValue); + fields[fieldName].id = broadcastInfo.id; + // Re-assign the value, because the name could have changed + // if the scratch2 message was an empty string + fields[fieldName].value = broadcastInfo.name; + } + fields[fieldName].variableType = expectedArg.variableType; + } else { + fields[fieldName].value = fieldValue; + } activeBlock.children.push({ id: inputUid, opcode: expectedArg.inputOp, @@ -530,8 +553,12 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg); } else if (expectedArg.fieldName === 'BROADCAST_OPTION') { // add the name in this field to the broadcast msg name map - const broadcastId = addBroadcastMsg(providedArg); - activeBlock.fields[expectedArg.fieldName].id = broadcastId; + const broadcastInfo = addBroadcastMsg(providedArg); + activeBlock.fields[expectedArg.fieldName].id = broadcastInfo.id; + // Need to reassign field value using the sb3 name from broadcastInfo + // because the sb2 message name (e.g. providedArg) could have changed + // if the original (providedArg) was an empty string + activeBlock.fields[expectedArg.fieldName].value = broadcastInfo.name; } const varType = expectedArg.variableType; if (typeof varType === 'string') { diff --git a/src/serialization/sb2_specmap.js b/src/serialization/sb2_specmap.js index cc0a56964..b829fc174 100644 --- a/src/serialization/sb2_specmap.js +++ b/src/serialization/sb2_specmap.js @@ -665,8 +665,9 @@ const specMap = { opcode: 'event_broadcast', argMap: [ { - type: 'field', - fieldName: 'BROADCAST_OPTION', + type: 'input', + inputOp: 'event_broadcast_menu', + inputName: 'BROADCAST_INPUT', variableType: Variable.BROADCAST_MESSAGE_TYPE } ] @@ -675,8 +676,9 @@ const specMap = { opcode: 'event_broadcastandwait', argMap: [ { - type: 'field', - fieldName: 'BROADCAST_OPTION', + type: 'input', + inputOp: 'event_broadcast_menu', + inputName: 'BROADCAST_INPUT', variableType: Variable.BROADCAST_MESSAGE_TYPE } ] From bcbe91064370bbe52f21cc0a66b165d3cf72b7f2 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Fri, 15 Dec 2017 11:02:26 -0500 Subject: [PATCH 4/7] Translating empty string messages from sb2 into messageN, where N is an integer >= n such that messageN is a fresh name. --- src/serialization/sb2.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index a8b750b3d..313768dc2 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -170,19 +170,23 @@ const generateVariableIdGetter = (function () { const globalBroadcastMsgStateGenerator = (function () { let broadcastMsgNameMap = {}; + const allBroadcastInputsAndFields = []; const emptyStringName = uid(); return function (topLevel) { if (topLevel) broadcastMsgNameMap = {}; return { - broadcastMsgMapUpdater: function (name) { + broadcastMsgMapUpdater: function (name, block) { name = name.toLowerCase(); if (name === '') { name = emptyStringName; } broadcastMsgNameMap[name] = `broadcastMsgId-${name}`; + allBroadcastInputsAndFields.push(block); return {name: name, id: broadcastMsgNameMap[name]}; }, - globalBroadcastMsgs: broadcastMsgNameMap + globalBroadcastMsgs: broadcastMsgNameMap, + allBroadcastInputsAndFields: allBroadcastInputsAndFields, + emptyMsgName: emptyStringName }; }; }()); @@ -345,6 +349,23 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) { // all other targets have finished processing. if (target.isStage) { const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs; + const allBroadcastMsgInputsAndFields = globalBroadcastMsgObj.allBroadcastInputsAndFields; + const emptyName = globalBroadcastMsgObj.emptyMsgName; + if (allBroadcastMsgs[emptyName]) { + // look through allBroadcastMsgs to see if 'messageN' is used + let currIndex = 1; + while (allBroadcastMsgs[`message${currIndex}`]) { + currIndex += 1; + } + const newEmptyMsgName = `message${currIndex}`; + allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[emptyName]; + delete allBroadcastMsgs[emptyName]; + for (let i = 0; i < allBroadcastMsgInputsAndFields.length; i++) { + if (allBroadcastMsgInputsAndFields[i].value === emptyName) { + allBroadcastMsgInputsAndFields[i].value = newEmptyMsgName; + } + } + } for (const msgName in allBroadcastMsgs) { const msgId = allBroadcastMsgs[msgName]; const newMsg = new Variable( @@ -516,7 +537,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension // field and a different value than the rest if (expectedArg.inputOp === 'event_broadcast_menu') { if (!shadowObscured) { - const broadcastInfo = addBroadcastMsg(fieldValue); + const broadcastInfo = addBroadcastMsg(fieldValue, fields[fieldName]); fields[fieldName].id = broadcastInfo.id; // Re-assign the value, because the name could have changed // if the scratch2 message was an empty string @@ -553,7 +574,7 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg); } else if (expectedArg.fieldName === 'BROADCAST_OPTION') { // add the name in this field to the broadcast msg name map - const broadcastInfo = addBroadcastMsg(providedArg); + const broadcastInfo = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]); activeBlock.fields[expectedArg.fieldName].id = broadcastInfo.id; // Need to reassign field value using the sb3 name from broadcastInfo // because the sb2 message name (e.g. providedArg) could have changed From 720c22db0e8dacf717f7c916bc58162a13428247 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Fri, 15 Dec 2017 14:00:53 -0500 Subject: [PATCH 5/7] Cleanup/refactoring. --- src/serialization/sb2.js | 72 ++++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 313768dc2..40304ce2d 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -170,22 +170,22 @@ const generateVariableIdGetter = (function () { const globalBroadcastMsgStateGenerator = (function () { let broadcastMsgNameMap = {}; - const allBroadcastInputsAndFields = []; + const allBroadcastFields = []; const emptyStringName = uid(); return function (topLevel) { if (topLevel) broadcastMsgNameMap = {}; return { - broadcastMsgMapUpdater: function (name, block) { + broadcastMsgMapUpdater: function (name, field) { name = name.toLowerCase(); if (name === '') { name = emptyStringName; } broadcastMsgNameMap[name] = `broadcastMsgId-${name}`; - allBroadcastInputsAndFields.push(block); - return {name: name, id: broadcastMsgNameMap[name]}; + allBroadcastFields.push(field); + return broadcastMsgNameMap[name]; }, globalBroadcastMsgs: broadcastMsgNameMap, - allBroadcastInputsAndFields: allBroadcastInputsAndFields, + allBroadcastFields: allBroadcastFields, emptyMsgName: emptyStringName }; }; @@ -349,23 +349,31 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) { // all other targets have finished processing. if (target.isStage) { const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs; - const allBroadcastMsgInputsAndFields = globalBroadcastMsgObj.allBroadcastInputsAndFields; - const emptyName = globalBroadcastMsgObj.emptyMsgName; - if (allBroadcastMsgs[emptyName]) { - // look through allBroadcastMsgs to see if 'messageN' is used + const allBroadcastMsgFields = globalBroadcastMsgObj.allBroadcastFields; + const oldEmptyMsgName = globalBroadcastMsgObj.emptyMsgName; + if (allBroadcastMsgs[oldEmptyMsgName]) { + // Find a fresh 'messageN' let currIndex = 1; while (allBroadcastMsgs[`message${currIndex}`]) { currIndex += 1; } const newEmptyMsgName = `message${currIndex}`; - allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[emptyName]; - delete allBroadcastMsgs[emptyName]; - for (let i = 0; i < allBroadcastMsgInputsAndFields.length; i++) { - if (allBroadcastMsgInputsAndFields[i].value === emptyName) { - allBroadcastMsgInputsAndFields[i].value = newEmptyMsgName; + // Add the new empty message name to the broadcast message + // name map, and assign it the old id. + // Then, delete the old entry in map. + allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[oldEmptyMsgName]; + delete allBroadcastMsgs[oldEmptyMsgName]; + // Now update all the broadcast message fields with + // the new empty message name. + for (let i = 0; i < allBroadcastMsgFields.length; i++) { + if (allBroadcastMsgFields[i].value === '') { + allBroadcastMsgFields[i].value = newEmptyMsgName; } } } + // Traverse the broadcast message name map and create + // broadcast messages as variables on the stage (which is this + // target). for (const msgName in allBroadcastMsgs) { const msgId = allBroadcastMsgs[msgName]; const newMsg = new Variable( @@ -531,21 +539,25 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension } const fields = {}; fields[fieldName] = { - name: fieldName + name: fieldName, + value: fieldValue }; // event_broadcast_menus have some extra properties to add to the // field and a different value than the rest if (expectedArg.inputOp === 'event_broadcast_menu') { if (!shadowObscured) { - const broadcastInfo = addBroadcastMsg(fieldValue, fields[fieldName]); - fields[fieldName].id = broadcastInfo.id; - // Re-assign the value, because the name could have changed - // if the scratch2 message was an empty string - fields[fieldName].value = broadcastInfo.name; + // Need to update the broadcast message name map with + // the value of this field. + // Also need to provide the fields[fieldName] object, + // so that we can later update its value property, e.g. + // if sb2 message name is empty string, we will later + // replace this field's value with messageN + // once we can traverse through all the existing message names + // and come up with a fresh messageN. + const broadcastId = addBroadcastMsg(fieldValue, fields[fieldName]); + fields[fieldName].id = broadcastId; } fields[fieldName].variableType = expectedArg.variableType; - } else { - fields[fieldName].value = fieldValue; } activeBlock.children.push({ id: inputUid, @@ -573,13 +585,15 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension // Add `id` property to variable fields activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg); } else if (expectedArg.fieldName === 'BROADCAST_OPTION') { - // add the name in this field to the broadcast msg name map - const broadcastInfo = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]); - activeBlock.fields[expectedArg.fieldName].id = broadcastInfo.id; - // Need to reassign field value using the sb3 name from broadcastInfo - // because the sb2 message name (e.g. providedArg) could have changed - // if the original (providedArg) was an empty string - activeBlock.fields[expectedArg.fieldName].value = broadcastInfo.name; + // Add the name in this field to the broadcast msg name map. + // Also need to provide the fields[fieldName] object, + // so that we can later update its value property, e.g. + // if sb2 message name is empty string, we will later + // replace this field's value with messageN + // once we can traverse through all the existing message names + // and come up with a fresh messageN. + const broadcastId = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]); + activeBlock.fields[expectedArg.fieldName].id = broadcastId; } const varType = expectedArg.variableType; if (typeof varType === 'string') { From 6fc554daa5a2600f96b78f9233724d2401ac5615 Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Thu, 21 Dec 2017 18:34:19 -0500 Subject: [PATCH 6/7] Implicit deletion behavior for broadcast blocks. --- src/virtual-machine.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index e0bb9961b..7b997dbdd 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -8,6 +8,7 @@ const sb2 = require('./serialization/sb2'); const sb3 = require('./serialization/sb3'); const StringUtil = require('./util/string-util'); const formatMessage = require('format-message'); +const Variable = require('./engine/variable'); const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); @@ -677,6 +678,35 @@ class VirtualMachine extends EventEmitter { * of the current editing target's blocks. */ emitWorkspaceUpdate () { + // Create a list of broadcast message Ids according to the stage variables + const stageVariables = this.runtime.getTargetForStage().variables; + let messageIds = []; + for (const varId in stageVariables) { + if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) { + messageIds.push(varId); + } + } + // Go through all blocks on all targets, removing referenced + // broadcast ids from the list. + for (let i = 0; i < this.runtime.targets.length; i++) { + const currTarget = this.runtime.targets[i]; + const currBlocks = currTarget.blocks._blocks; + for (const blockId in currBlocks) { + if (currBlocks[blockId].fields.BROADCAST_OPTION) { + const id = currBlocks[blockId].fields.BROADCAST_OPTION.id; + const index = messageIds.indexOf(id); + if (index !== -1) { + messageIds = messageIds.slice(0, index) + .concat(messageIds.slice(index + 1)); + } + } + } + } + // Anything left in messageIds is not referenced by a block, so delete it. + for (let i = 0; i < messageIds.length; i++) { + const id = messageIds[i]; + delete this.runtime.getTargetForStage().variables[id]; + } const variableMap = Object.assign({}, this.runtime.getTargetForStage().variables, this.editingTarget.variables From 29c08c3583985bd1b9af6f867bdf179a17ca0ced Mon Sep 17 00:00:00 2001 From: Karishma Chadha Date: Thu, 21 Dec 2017 18:44:29 -0500 Subject: [PATCH 7/7] Fixing unit test. --- test/unit/virtual-machine.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit/virtual-machine.js b/test/unit/virtual-machine.js index 40bd6bb54..8dd283cf6 100644 --- a/test/unit/virtual-machine.js +++ b/test/unit/virtual-machine.js @@ -287,12 +287,18 @@ test('emitWorkspaceUpdate', t => { global: { toXML: () => 'global' } + }, + blocks: { + toXML: () => 'blocks' } }, { variables: { unused: { toXML: () => 'unused' } + }, + blocks: { + toXML: () => 'blocks' } }, { variables: {