mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
Merge pull request #860 from kchadha/broadcast-input-functionality
Broadcast Inputs and Implicit Message Deletion
This commit is contained in:
commit
9d5bbdbf3a
6 changed files with 161 additions and 13 deletions
|
@ -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).
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -287,12 +287,18 @@ test('emitWorkspaceUpdate', t => {
|
|||
global: {
|
||||
toXML: () => 'global'
|
||||
}
|
||||
},
|
||||
blocks: {
|
||||
toXML: () => 'blocks'
|
||||
}
|
||||
}, {
|
||||
variables: {
|
||||
unused: {
|
||||
toXML: () => 'unused'
|
||||
}
|
||||
},
|
||||
blocks: {
|
||||
toXML: () => 'blocks'
|
||||
}
|
||||
}, {
|
||||
variables: {
|
||||
|
|
Loading…
Reference in a new issue