diff --git a/src/engine/execute.js b/src/engine/execute.js index 7dcf80d84..13742ec76 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,30 @@ 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. + 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_OPTION = { + 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..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}.`); } @@ -115,6 +122,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 +165,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); diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index d19175dca..40304ce2d 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -170,14 +170,23 @@ const generateVariableIdGetter = (function () { const globalBroadcastMsgStateGenerator = (function () { let broadcastMsgNameMap = {}; + const allBroadcastFields = []; + const emptyStringName = uid(); return function (topLevel) { if (topLevel) broadcastMsgNameMap = {}; return { - broadcastMsgMapUpdater: function (name) { + broadcastMsgMapUpdater: function (name, field) { + name = name.toLowerCase(); + if (name === '') { + name = emptyStringName; + } broadcastMsgNameMap[name] = `broadcastMsgId-${name}`; + allBroadcastFields.push(field); return broadcastMsgNameMap[name]; }, - globalBroadcastMsgs: broadcastMsgNameMap + globalBroadcastMsgs: broadcastMsgNameMap, + allBroadcastFields: allBroadcastFields, + emptyMsgName: emptyStringName }; }; }()); @@ -340,6 +349,31 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) { // all other targets have finished processing. if (target.isStage) { const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs; + 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}`; + // 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( @@ -494,6 +528,11 @@ 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 = ''; @@ -503,6 +542,23 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension 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) { + // 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; + } activeBlock.children.push({ id: inputUid, opcode: expectedArg.inputOp, @@ -529,8 +585,14 @@ 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 broadcastId = addBroadcastMsg(providedArg); + // 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; 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 } ] 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 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: {