Merge pull request #860 from kchadha/broadcast-input-functionality

Broadcast Inputs and Implicit Message Deletion
This commit is contained in:
Paul Kaplan 2017-12-27 11:47:56 -05:00 committed by GitHub
commit 9d5bbdbf3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 161 additions and 13 deletions

View file

@ -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).

View file

@ -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);

View file

@ -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;

View file

@ -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
}
]

View file

@ -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

View file

@ -287,12 +287,18 @@ test('emitWorkspaceUpdate', t => {
global: {
toXML: () => 'global'
}
},
blocks: {
toXML: () => 'blocks'
}
}, {
variables: {
unused: {
toXML: () => 'unused'
}
},
blocks: {
toXML: () => 'blocks'
}
}, {
variables: {