mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-02 17:20:32 -04:00
800 lines
34 KiB
JavaScript
800 lines
34 KiB
JavaScript
const EventEmitter = require('events');
|
|
|
|
const Blocks = require('./blocks');
|
|
const Variable = require('../engine/variable');
|
|
const Comment = require('../engine/comment');
|
|
const uid = require('../util/uid');
|
|
const {Map} = require('immutable');
|
|
const log = require('../util/log');
|
|
const StringUtil = require('../util/string-util');
|
|
const VariableUtil = require('../util/variable-util');
|
|
|
|
/**
|
|
* @fileoverview
|
|
* A Target is an abstract "code-running" object for the Scratch VM.
|
|
* Examples include sprites/clones or potentially physical-world devices.
|
|
*/
|
|
|
|
class Target extends EventEmitter {
|
|
|
|
/**
|
|
* @param {Runtime} runtime Reference to the runtime.
|
|
* @param {?Blocks} blocks Blocks instance for the blocks owned by this target.
|
|
* @param {?Object.<string, Comment>} comments Array of comments owned by this target.
|
|
* @constructor
|
|
*/
|
|
constructor (runtime, blocks, comments) {
|
|
super();
|
|
|
|
if (!blocks) {
|
|
blocks = new Blocks(runtime);
|
|
}
|
|
|
|
/**
|
|
* Reference to the runtime.
|
|
* @type {Runtime}
|
|
*/
|
|
this.runtime = runtime;
|
|
/**
|
|
* A unique ID for this target.
|
|
* @type {string}
|
|
*/
|
|
this.id = uid();
|
|
/**
|
|
* Blocks run as code for this target.
|
|
* @type {!Blocks}
|
|
*/
|
|
this.blocks = blocks;
|
|
/**
|
|
* Dictionary of variables and their values for this target.
|
|
* Key is the variable id.
|
|
* @type {Object.<string,*>}
|
|
*/
|
|
this.variables = {};
|
|
/**
|
|
* Dictionary of comments for this target.
|
|
* Key is the comment id.
|
|
* @type {Object.<string,*>}
|
|
*/
|
|
this.comments = comments || {};
|
|
/**
|
|
* Dictionary of custom state for this target.
|
|
* This can be used to store target-specific custom state for blocks which need it.
|
|
* TODO: do we want to persist this in SB3 files?
|
|
* @type {Object.<string,*>}
|
|
*/
|
|
this._customState = {};
|
|
|
|
/**
|
|
* Currently known values for edge-activated hats.
|
|
* Keys are block ID for the hat; values are the currently known values.
|
|
* @type {Object.<string, *>}
|
|
*/
|
|
this._edgeActivatedHatValues = {};
|
|
}
|
|
|
|
/**
|
|
* Called when the project receives a "green flag."
|
|
* @abstract
|
|
*/
|
|
onGreenFlag () {}
|
|
|
|
/**
|
|
* Return a human-readable name for this target.
|
|
* Target implementations should override this.
|
|
* @abstract
|
|
* @returns {string} Human-readable name for the target.
|
|
*/
|
|
getName () {
|
|
return this.id;
|
|
}
|
|
|
|
/**
|
|
* Update an edge-activated hat block value.
|
|
* @param {!string} blockId ID of hat to store value for.
|
|
* @param {*} newValue Value to store for edge-activated hat.
|
|
* @return {*} The old value for the edge-activated hat.
|
|
*/
|
|
updateEdgeActivatedValue (blockId, newValue) {
|
|
const oldValue = this._edgeActivatedHatValues[blockId];
|
|
this._edgeActivatedHatValues[blockId] = newValue;
|
|
return oldValue;
|
|
}
|
|
|
|
hasEdgeActivatedValue (blockId) {
|
|
return Object.prototype.hasOwnProperty.call(this._edgeActivatedHatValues, blockId);
|
|
}
|
|
|
|
/**
|
|
* Clear all edge-activaed hat values.
|
|
*/
|
|
clearEdgeActivatedValues () {
|
|
this._edgeActivatedHatValues = {};
|
|
}
|
|
|
|
/**
|
|
* Look up a variable object, first by id, and then by name if the id is not found.
|
|
* Create a new variable if both lookups fail.
|
|
* @param {string} id Id of the variable.
|
|
* @param {string} name Name of the variable.
|
|
* @return {!Variable} Variable object.
|
|
*/
|
|
lookupOrCreateVariable (id, name) {
|
|
let variable = this.lookupVariableById(id);
|
|
if (variable) return variable;
|
|
|
|
variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE);
|
|
if (variable) return variable;
|
|
|
|
// No variable with this name exists - create it locally.
|
|
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
|
this.variables[id] = newVariable;
|
|
return newVariable;
|
|
}
|
|
|
|
/**
|
|
* Look up a broadcast message object with the given id and return it
|
|
* if it exists.
|
|
* @param {string} id Id of the variable.
|
|
* @param {string} name Name of the variable.
|
|
* @return {?Variable} Variable object.
|
|
*/
|
|
lookupBroadcastMsg (id, name) {
|
|
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 (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}.`);
|
|
}
|
|
if (broadcastMsg.type !== Variable.BROADCAST_MESSAGE_TYPE) {
|
|
log.error(`Found variable with id: ${id}, but its type ${broadcastMsg.type}` +
|
|
`did not match expected type ${Variable.BROADCAST_MESSAGE_TYPE}`);
|
|
}
|
|
return broadcastMsg;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @param {string} id Id of the variable.
|
|
* @param {string} name Name of the variable.
|
|
* @return {!Variable} Variable object.
|
|
*/
|
|
lookupVariableById (id) {
|
|
// If we have a local copy, return it.
|
|
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
|
return this.variables[id];
|
|
}
|
|
// If the stage has a global copy, return it.
|
|
if (this.runtime && !this.isStage) {
|
|
const stage = this.runtime.getTargetForStage();
|
|
if (stage && Object.prototype.hasOwnProperty.call(stage.variables, id)) {
|
|
return stage.variables[id];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Look up a variable object by its name and variable type.
|
|
* Search begins with local variables; then global variables if a local one
|
|
* was not found.
|
|
* @param {string} name Name of the variable.
|
|
* @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE.
|
|
* @param {?bool} skipStage Optional flag to skip checking the stage
|
|
* @return {?Variable} Variable object if found, or null if not.
|
|
*/
|
|
lookupVariableByNameAndType (name, type, skipStage) {
|
|
if (typeof name !== 'string') return;
|
|
if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
|
|
skipStage = skipStage || false;
|
|
|
|
for (const varId in this.variables) {
|
|
const currVar = this.variables[varId];
|
|
if (currVar.name === name && currVar.type === type) {
|
|
return currVar;
|
|
}
|
|
}
|
|
|
|
if (!skipStage && this.runtime && !this.isStage) {
|
|
const stage = this.runtime.getTargetForStage();
|
|
if (stage) {
|
|
for (const varId in stage.variables) {
|
|
const currVar = stage.variables[varId];
|
|
if (currVar.name === name && currVar.type === type) {
|
|
return currVar;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Look up a list object for this target, and create it if one doesn't exist.
|
|
* Search begins for local lists; then look for globals.
|
|
* @param {!string} id Id of the list.
|
|
* @param {!string} name Name of the list.
|
|
* @return {!Varible} Variable object representing the found/created list.
|
|
*/
|
|
lookupOrCreateList (id, name) {
|
|
let list = this.lookupVariableById(id);
|
|
if (list) return list;
|
|
|
|
list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE);
|
|
if (list) return list;
|
|
|
|
// No variable with this name exists - create it locally.
|
|
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
|
this.variables[id] = newList;
|
|
return newList;
|
|
}
|
|
|
|
/**
|
|
* Creates a variable with the given id and name and adds it to the
|
|
* dictionary of variables.
|
|
* @param {string} id Id of variable
|
|
* @param {string} name Name of variable.
|
|
* @param {string} type Type of variable, '', 'broadcast_msg', or 'list'
|
|
* @param {boolean} isCloud Whether the variable to create has the isCloud flag set.
|
|
* Additional checks are made that the variable can be created as a cloud variable.
|
|
*/
|
|
createVariable (id, name, type, isCloud) {
|
|
if (!Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
|
const newVariable = new Variable(id, name, type, false);
|
|
if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) {
|
|
newVariable.isCloud = true;
|
|
this.runtime.addCloudVariable();
|
|
this.runtime.ioDevices.cloud.requestCreateVariable(newVariable);
|
|
}
|
|
this.variables[id] = newVariable;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a comment with the given properties.
|
|
* @param {string} id Id of the comment.
|
|
* @param {string} blockId Optional id of the block the comment is attached
|
|
* to if it is a block comment.
|
|
* @param {string} text The text the comment contains.
|
|
* @param {number} x The x coordinate of the comment on the workspace.
|
|
* @param {number} y The y coordinate of the comment on the workspace.
|
|
* @param {number} width The width of the comment when it is full size
|
|
* @param {number} height The height of the comment when it is full size
|
|
* @param {boolean} minimized Whether the comment is minimized.
|
|
*/
|
|
createComment (id, blockId, text, x, y, width, height, minimized) {
|
|
if (!Object.prototype.hasOwnProperty.call(this.comments, id)) {
|
|
const newComment = new Comment(id, text, x, y,
|
|
width, height, minimized);
|
|
if (blockId) {
|
|
newComment.blockId = blockId;
|
|
const blockWithComment = this.blocks.getBlock(blockId);
|
|
if (blockWithComment) {
|
|
blockWithComment.comment = id;
|
|
} else {
|
|
log.warn(`Could not find block with id ${blockId
|
|
} associated with commentId: ${id}`);
|
|
}
|
|
}
|
|
this.comments[id] = newComment;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renames the variable with the given id to newName.
|
|
* @param {string} id Id of variable to rename.
|
|
* @param {string} newName New name for the variable.
|
|
*/
|
|
renameVariable (id, newName) {
|
|
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
|
const variable = this.variables[id];
|
|
if (variable.id === id) {
|
|
const oldName = variable.name;
|
|
variable.name = newName;
|
|
|
|
if (this.runtime) {
|
|
if (variable.isCloud && this.isStage) {
|
|
this.runtime.ioDevices.cloud.requestRenameVariable(oldName, newName);
|
|
}
|
|
|
|
if (variable.type === Variable.SCALAR_TYPE) {
|
|
// sensing__of may be referencing to this variable.
|
|
// Change the reference.
|
|
let blockUpdated = false;
|
|
this.runtime.targets.forEach(t => {
|
|
blockUpdated = t.blocks.updateSensingOfReference(
|
|
oldName,
|
|
newName,
|
|
this.isStage ? '_stage_' : this.getName()
|
|
) || blockUpdated;
|
|
});
|
|
// Request workspace change only if sensing_of blocks were actually updated.
|
|
if (blockUpdated) this.runtime.requestBlocksUpdate();
|
|
}
|
|
|
|
const blocks = this.runtime.monitorBlocks;
|
|
blocks.changeBlock({
|
|
id: id,
|
|
element: 'field',
|
|
name: variable.type === Variable.LIST_TYPE ? 'LIST' : 'VARIABLE',
|
|
value: id
|
|
}, this.runtime);
|
|
const monitorBlock = blocks.getBlock(variable.id);
|
|
if (monitorBlock) {
|
|
this.runtime.requestUpdateMonitor(Map({
|
|
id: id,
|
|
params: blocks._getBlockParams(monitorBlock)
|
|
}));
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the variable with the given id from the dictionary of variables.
|
|
* @param {string} id Id of variable to delete.
|
|
*/
|
|
deleteVariable (id) {
|
|
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
|
// Get info about the variable before deleting it
|
|
const deletedVariableName = this.variables[id].name;
|
|
const deletedVariableWasCloud = this.variables[id].isCloud;
|
|
delete this.variables[id];
|
|
if (this.runtime) {
|
|
if (deletedVariableWasCloud && this.isStage) {
|
|
this.runtime.ioDevices.cloud.requestDeleteVariable(deletedVariableName);
|
|
this.runtime.removeCloudVariable();
|
|
}
|
|
this.runtime.monitorBlocks.deleteBlock(id);
|
|
this.runtime.requestRemoveMonitor(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove this target's monitors from the runtime state and remove the
|
|
* target-specific monitored blocks (e.g. local variables, global variables for the stage, x-position).
|
|
* NOTE: This does not delete any of the stage monitors like backdrop name.
|
|
*/
|
|
deleteMonitors () {
|
|
this.runtime.requestRemoveMonitorByTargetId(this.id);
|
|
let targetSpecificMonitorBlockIds;
|
|
if (this.isStage) {
|
|
// This only deletes global variables and not other stage monitors like backdrop number.
|
|
targetSpecificMonitorBlockIds = Object.keys(this.variables);
|
|
} else {
|
|
targetSpecificMonitorBlockIds = Object.keys(this.runtime.monitorBlocks._blocks)
|
|
.filter(key => this.runtime.monitorBlocks._blocks[key].targetId === this.id);
|
|
}
|
|
for (const blockId of targetSpecificMonitorBlockIds) {
|
|
this.runtime.monitorBlocks.deleteBlock(blockId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a clone of the variable with the given id from the dictionary of
|
|
* this target's variables.
|
|
* @param {string} id Id of variable to duplicate.
|
|
* @param {boolean=} optKeepOriginalId Optional flag to keep the original variable ID
|
|
* for the duplicate variable. This is necessary when cloning a sprite, for example.
|
|
* @return {?Variable} The duplicated variable, or null if
|
|
* the original variable was not found.
|
|
*/
|
|
duplicateVariable (id, optKeepOriginalId) {
|
|
if (Object.prototype.hasOwnProperty.call(this.variables, id)) {
|
|
const originalVariable = this.variables[id];
|
|
const newVariable = new Variable(
|
|
optKeepOriginalId ? id : null, // conditionally keep original id or generate a new one
|
|
originalVariable.name,
|
|
originalVariable.type,
|
|
originalVariable.isCloud
|
|
);
|
|
if (newVariable.type === Variable.LIST_TYPE) {
|
|
newVariable.value = originalVariable.value.slice(0);
|
|
} else {
|
|
newVariable.value = originalVariable.value;
|
|
}
|
|
return newVariable;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Duplicate the dictionary of this target's variables as part of duplicating.
|
|
* this target or making a clone.
|
|
* @param {object=} optBlocks Optional block container for the target being duplicated.
|
|
* If provided, new variables will be generated with new UIDs and any variable references
|
|
* in this blocks container will be updated to refer to the corresponding new IDs.
|
|
* @return {object} The duplicated dictionary of variables
|
|
*/
|
|
duplicateVariables (optBlocks) {
|
|
let allVarRefs;
|
|
if (optBlocks) {
|
|
allVarRefs = optBlocks.getAllVariableAndListReferences();
|
|
}
|
|
return Object.keys(this.variables).reduce((accum, varId) => {
|
|
const newVariable = this.duplicateVariable(varId, !optBlocks);
|
|
accum[newVariable.id] = newVariable;
|
|
if (optBlocks && allVarRefs) {
|
|
const currVarRefs = allVarRefs[varId];
|
|
if (currVarRefs) {
|
|
this.mergeVariables(varId, newVariable.id, currVarRefs);
|
|
}
|
|
}
|
|
return accum;
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* Post/edit sprite info.
|
|
* @param {object} data An object with sprite info data to set.
|
|
* @abstract
|
|
*/
|
|
postSpriteInfo () {}
|
|
|
|
/**
|
|
* Retrieve custom state associated with this target and the provided state ID.
|
|
* @param {string} stateId - specify which piece of state to retrieve.
|
|
* @returns {*} the associated state, if any was found.
|
|
*/
|
|
getCustomState (stateId) {
|
|
return this._customState[stateId];
|
|
}
|
|
|
|
/**
|
|
* Store custom state associated with this target and the provided state ID.
|
|
* @param {string} stateId - specify which piece of state to store on this target.
|
|
* @param {*} newValue - the state value to store.
|
|
*/
|
|
setCustomState (stateId, newValue) {
|
|
this._customState[stateId] = newValue;
|
|
}
|
|
|
|
/**
|
|
* Call to destroy a target.
|
|
* @abstract
|
|
*/
|
|
dispose () {
|
|
this._customState = {};
|
|
|
|
if (this.runtime) {
|
|
this.runtime.removeExecutable(this);
|
|
}
|
|
}
|
|
|
|
// Variable Conflict Resolution Helpers
|
|
|
|
/**
|
|
* Get the names of all the variables of the given type that are in scope for this target.
|
|
* For targets that are not the stage, this includes any target-specific
|
|
* variables as well as any stage variables unless the skipStage flag is true.
|
|
* For the stage, this is all stage variables.
|
|
* @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE
|
|
* @param {?bool} skipStage Optional flag to skip the stage.
|
|
* @return {Array<string>} A list of variable names
|
|
*/
|
|
getAllVariableNamesInScopeByType (type, skipStage) {
|
|
if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
|
|
skipStage = skipStage || false;
|
|
const targetVariables = Object.values(this.variables)
|
|
.filter(v => v.type === type)
|
|
.map(variable => variable.name);
|
|
if (skipStage || this.isStage || !this.runtime) {
|
|
return targetVariables;
|
|
}
|
|
const stage = this.runtime.getTargetForStage();
|
|
const stageVariables = stage.getAllVariableNamesInScopeByType(type);
|
|
return targetVariables.concat(stageVariables);
|
|
}
|
|
|
|
/**
|
|
* Merge variable references with another variable.
|
|
* @param {string} idToBeMerged ID of the variable whose references need to be updated
|
|
* @param {string} idToMergeWith ID of the variable that the old references should be replaced with
|
|
* @param {?Array<Object>} optReferencesToUpdate Optional context of the change.
|
|
* Defaults to all the blocks in this target.
|
|
* @param {?string} optNewName New variable name to merge with. The old
|
|
* variable name in the references being updated should be replaced with this new name.
|
|
* If this parameter is not provided or is '', no name change occurs.
|
|
*/
|
|
mergeVariables (idToBeMerged, idToMergeWith, optReferencesToUpdate, optNewName) {
|
|
const referencesToChange = optReferencesToUpdate ||
|
|
// TODO should there be a separate helper function that traverses the blocks
|
|
// for all references for a given ID instead of doing the below..?
|
|
this.blocks.getAllVariableAndListReferences()[idToBeMerged];
|
|
|
|
VariableUtil.updateVariableIdentifiers(referencesToChange, idToMergeWith, optNewName);
|
|
}
|
|
|
|
/**
|
|
* Share a local variable (and given references for that variable) to the stage.
|
|
* @param {string} varId The ID of the variable to share.
|
|
* @param {Array<object>} varRefs The list of variable references being shared,
|
|
* that reference the given variable ID. The names and IDs of these variable
|
|
* references will be updated to refer to the new (or pre-existing) global variable.
|
|
*/
|
|
shareLocalVariableToStage (varId, varRefs) {
|
|
if (!this.runtime) return;
|
|
const variable = this.variables[varId];
|
|
if (!variable) {
|
|
log.warn(`Cannot share a local variable to the stage if it's not local.`);
|
|
return;
|
|
}
|
|
const stage = this.runtime.getTargetForStage();
|
|
// If a local var is being shared with the stage,
|
|
// sharing will make the variable global, resulting in a conflict
|
|
// with the existing local variable. Preemptively Resolve this conflict
|
|
// by renaming the new global variable.
|
|
|
|
// First check if we've already done the local to global transition for this
|
|
// variable. If we have, merge it with the global variable we've already created.
|
|
const varIdForStage = `StageVarFromLocal_${varId}`;
|
|
let stageVar = stage.lookupVariableById(varIdForStage);
|
|
// If a global var doesn't already exist, create a new one with a fresh name.
|
|
// Use the ID we created above so that we can lookup this new variable in the
|
|
// future if we decide to share this same variable again.
|
|
if (!stageVar) {
|
|
const varName = variable.name;
|
|
const varType = variable.type;
|
|
|
|
const newStageName = `Stage: ${varName}`;
|
|
stageVar = this.runtime.createNewGlobalVariable(newStageName, varIdForStage, varType);
|
|
}
|
|
// Update all variable references to use the new name and ID
|
|
this.mergeVariables(varId, stageVar.id, varRefs, stageVar.name);
|
|
}
|
|
|
|
/**
|
|
* Share a local variable with a sprite, merging with one of the same name and
|
|
* type if it already exists on the sprite, or create a new one.
|
|
* @param {string} varId Id of the variable to share
|
|
* @param {Target} sprite The sprite to share the variable with
|
|
* @param {Array<object>} varRefs A list of all the variable references currently being shared.
|
|
*/
|
|
shareLocalVariableToSprite (varId, sprite, varRefs) {
|
|
if (!this.runtime) return;
|
|
if (this.isStage) return;
|
|
const variable = this.variables[varId];
|
|
if (!variable) {
|
|
log.warn(`Tried to call 'shareLocalVariableToSprite' with a non-local variable.`);
|
|
return;
|
|
}
|
|
const varName = variable.name;
|
|
const varType = variable.type;
|
|
// Check if the receiving sprite already has a variable of the same name and type
|
|
// and use the existing variable, otherwise create a new one.
|
|
const existingLocalVar = sprite.lookupVariableByNameAndType(varName, varType);
|
|
let newVarId;
|
|
if (existingLocalVar) {
|
|
newVarId = existingLocalVar.id;
|
|
} else {
|
|
const newVar = new Variable(null, varName, varType);
|
|
newVarId = newVar.id;
|
|
sprite.variables[newVarId] = newVar;
|
|
}
|
|
|
|
// Merge with the local variable on the new sprite.
|
|
this.mergeVariables(varId, newVarId, varRefs);
|
|
}
|
|
|
|
/**
|
|
* Given a list of variable referencing fields, shares those variables with
|
|
* the target with the provided id, resolving any variable conflicts that arise
|
|
* using the following rules:
|
|
*
|
|
* If this target is the stage, exit. There are no conflicts that arise
|
|
* from sharing variables from the stage to another sprite. The variables
|
|
* already exist globally, so no further action is needed.
|
|
*
|
|
* If a variable being referenced is a global variable, do nothing. The
|
|
* global variable already exists so no further action is needed.
|
|
*
|
|
* If a variable being referenced is local, and
|
|
* 1) The receiving target is a sprite:
|
|
* create a new local variable or merge with an existing local variable
|
|
* of the same name and type. Update all the referencing fields
|
|
* for the original variable to reference the new variable.
|
|
* 2) The receiving target is the stage:
|
|
* Create a new global variable with a fresh name and update all the referencing
|
|
* fields to reference the new variable.
|
|
*
|
|
* @param {Array<object>} blocks The blocks containing
|
|
* potential conflicting references to variables.
|
|
* @param {Target} receivingTarget The target receiving the variables
|
|
*/
|
|
resolveVariableSharingConflictsWithTarget (blocks, receivingTarget) {
|
|
if (this.isStage) return;
|
|
|
|
// Get all the variable references in the given list of blocks
|
|
const allVarListRefs = this.blocks.getAllVariableAndListReferences(blocks);
|
|
|
|
// For all the variables being referenced, check for which ones are local
|
|
// to this target, and resolve conflicts based on whether the receiving target
|
|
// is a sprite (with a conflicting local variable) or whether it is
|
|
// the stage (which cannot have local variables)
|
|
for (const varId in allVarListRefs) {
|
|
const currVar = this.variables[varId];
|
|
if (!currVar) continue; // The current variable is global, there shouldn't be any conflicts here, skip it.
|
|
|
|
// Get the list of references for the current variable id
|
|
const currVarListRefs = allVarListRefs[varId];
|
|
|
|
if (receivingTarget.isStage) {
|
|
this.shareLocalVariableToStage(varId, currVarListRefs);
|
|
} else {
|
|
this.shareLocalVariableToSprite(varId, receivingTarget, currVarListRefs);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fixes up variable references in this target avoiding conflicts with
|
|
* pre-existing variables in the same scope.
|
|
* This is used when uploading this target as a new sprite into an existing
|
|
* project, where the new sprite may contain references
|
|
* to variable names that already exist as global variables in the project
|
|
* (and thus are in scope for variable references in the given sprite).
|
|
*
|
|
* If this target has a block that references an existing global variable and that
|
|
* variable *does not* exist in this target (e.g. it was a global variable in the
|
|
* project the sprite was originally exported from), merge the variables. This entails
|
|
* fixing the variable references in this sprite to reference the id of the pre-existing global variable.
|
|
*
|
|
* If this target has a block that references an existing global variable and that
|
|
* variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
|
|
* then the local variable is renamed to distinguish itself from the pre-existing variable.
|
|
* All blocks that reference the local variable will be updated to use the new name.
|
|
*/
|
|
// TODO (#1360) This function is too long, add some helpers for the different chunks and cases...
|
|
fixUpVariableReferences () {
|
|
if (!this.runtime) return; // There's no runtime context to conflict with
|
|
if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
|
|
const stage = this.runtime.getTargetForStage();
|
|
if (!stage || !stage.variables) return;
|
|
|
|
const renameConflictingLocalVar = (id, name, type) => {
|
|
const conflict = stage.lookupVariableByNameAndType(name, type);
|
|
if (conflict) {
|
|
const newName = StringUtil.unusedName(
|
|
`${this.getName()}: ${name}`,
|
|
this.getAllVariableNamesInScopeByType(type));
|
|
this.renameVariable(id, newName);
|
|
return newName;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const allReferences = this.blocks.getAllVariableAndListReferences();
|
|
const unreferencedLocalVarIds = [];
|
|
if (Object.keys(this.variables).length > 0) {
|
|
for (const localVarId in this.variables) {
|
|
if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
|
|
if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
|
|
}
|
|
}
|
|
const conflictIdsToReplace = Object.create(null);
|
|
const conflictNamesToReplace = Object.create(null);
|
|
|
|
// Cache the list of all variable names by type so that we don't need to
|
|
// re-calculate this in every iteration of the following loop.
|
|
const varNamesByType = {};
|
|
const allVarNames = type => {
|
|
const namesOfType = varNamesByType[type];
|
|
if (namesOfType) return namesOfType;
|
|
varNamesByType[type] = this.runtime.getAllVarNamesOfType(type);
|
|
return varNamesByType[type];
|
|
};
|
|
|
|
for (const varId in allReferences) {
|
|
// We don't care about which var ref we get, they should all have the same var info
|
|
const varRef = allReferences[varId][0];
|
|
const varName = varRef.referencingField.value;
|
|
const varType = varRef.type;
|
|
if (this.lookupVariableById(varId)) {
|
|
// Found a variable with the id in either the target or the stage,
|
|
// figure out which one.
|
|
if (Object.prototype.hasOwnProperty.call(this.variables, varId)) {
|
|
// If the target has the variable, then check whether the stage
|
|
// has one with the same name and type. If it does, then rename
|
|
// this target specific variable so that there is a distinction.
|
|
const newVarName = renameConflictingLocalVar(varId, varName, varType);
|
|
|
|
if (newVarName) {
|
|
// We are not calling this.blocks.updateBlocksAfterVarRename
|
|
// here because it will search through all the blocks. We already
|
|
// have access to all the references for this var id.
|
|
allReferences[varId].map(ref => {
|
|
ref.referencingField.value = newVarName;
|
|
return ref;
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// We didn't find the referenced variable id anywhere,
|
|
// Treat it as a reference to a global variable (from the original
|
|
// project this sprite was exported from).
|
|
// Check for whether a global variable of the same name and type exists,
|
|
// and if so, track it to merge with the existing global in a second pass of the blocks.
|
|
const existingVar = stage.lookupVariableByNameAndType(varName, varType);
|
|
if (existingVar) {
|
|
if (!conflictIdsToReplace[varId]) {
|
|
conflictIdsToReplace[varId] = existingVar.id;
|
|
}
|
|
} else {
|
|
// A global variable with the same name did not already exist,
|
|
// create a new one such that it does not conflict with any
|
|
// names of local variables of the same type.
|
|
const allNames = allVarNames(varType);
|
|
const freshName = StringUtil.unusedName(varName, allNames);
|
|
stage.createVariable(varId, freshName, varType);
|
|
if (!conflictNamesToReplace[varId]) {
|
|
conflictNamesToReplace[varId] = freshName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Rename any local variables that were missed above because they aren't
|
|
// referenced by any blocks
|
|
for (const id in unreferencedLocalVarIds) {
|
|
const varId = unreferencedLocalVarIds[id];
|
|
const name = this.variables[varId].name;
|
|
const type = this.variables[varId].type;
|
|
renameConflictingLocalVar(varId, name, type);
|
|
}
|
|
// Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has
|
|
// blocks referencing some variable that the sprite does not own, and this
|
|
// variable conflicts with a global var)
|
|
// In this case, we want to merge the new variable referenes with the
|
|
// existing global variable
|
|
for (const conflictId in conflictIdsToReplace) {
|
|
const existingId = conflictIdsToReplace[conflictId];
|
|
const referencesToUpdate = allReferences[conflictId];
|
|
this.mergeVariables(conflictId, existingId, referencesToUpdate);
|
|
}
|
|
|
|
// Handle global var conflicts existing local vars (e.g a sprite is uploaded,
|
|
// and has blocks referencing some variable that the sprite does not own, and this
|
|
// variable conflcits with another sprite's local var).
|
|
// In this case, we want to go through the variable references and update
|
|
// the name of the variable in that reference.
|
|
for (const conflictId in conflictNamesToReplace) {
|
|
const newName = conflictNamesToReplace[conflictId];
|
|
const referencesToUpdate = allReferences[conflictId];
|
|
referencesToUpdate.map(ref => {
|
|
ref.referencingField.value = newName;
|
|
return ref;
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
module.exports = Target;
|