Files
GDevelop/GDJS/Runtime/variablescontainer.ts

494 lines
15 KiB
TypeScript

/*
* GDevelop JS Platform
* Copyright 2013-2016 Florian Rival (Florian.Rival@gmail.com). All rights reserved.
* This project is released under the MIT License.
*/
namespace gdjs {
/**
* VariablesContainer stores variables, usually for a a RuntimeGame, a RuntimeScene
* or a RuntimeObject.
* @category Core Engine > Variables
*/
export class VariablesContainer {
_variables: Hashtable<gdjs.Variable>;
_variablesArray: gdjs.Variable[] = [];
/**
* @param [initialVariablesData] Optional array containing representations of the base variables.
*/
constructor(initialVariablesData?: VariableData[]) {
this._variables = new Hashtable();
if (initialVariablesData !== undefined) {
this.initFrom(initialVariablesData);
}
}
/**
* Initialize variables from a container data.<br>
* If `keepOldVariables` is set to false (by default), all already existing variables will be
* erased, but the new variables will be accessible thanks to getFromIndex. <br>
* if `keepOldVariables` is set to true, already existing variables won't be erased and will be
* still accessible thanks to getFromIndex.
*
* @param data The array containing data used to initialize variables.
* @param [keepOldVariables] If set to true, already existing variables won't be erased.
*/
initFrom(data: VariableData[], keepOldVariables?: Boolean) {
if (keepOldVariables === undefined) {
keepOldVariables = false;
}
if (!keepOldVariables) {
VariablesContainer._deletedVars = VariablesContainer._deletedVars || [];
// @ts-ignore
this._variables.keys(VariablesContainer._deletedVars);
}
const that = this;
let i = 0;
for (let j = 0; j < data.length; ++j) {
const varData = data[j];
if (!varData.name) continue;
//Get the variable:
const variable = that.get(varData.name);
variable.reinitialize(varData);
if (!keepOldVariables) {
//Register the variable in the extra array to ensure a fast lookup using getFromIndex.
if (i < that._variablesArray.length) {
that._variablesArray[i] = variable;
} else {
that._variablesArray.push(variable);
}
++i;
//Remove the variable from the list of variables to be deleted.
const idx = VariablesContainer._deletedVars.indexOf(varData.name);
if (idx !== -1) {
VariablesContainer._deletedVars[idx] = undefined;
}
}
}
if (!keepOldVariables) {
this._variablesArray.length = i;
//If we do not want to keep the already existing variables,
//remove all the variables not assigned above.
//(Here, remove means flag the variable as not existing, to avoid garbage creation ).
for (
let i = 0, len = VariablesContainer._deletedVars.length;
i < len;
++i
) {
const variableName = VariablesContainer._deletedVars[i];
if (variableName !== undefined) {
this._variables.get(variableName).setUndefinedInContainer();
}
}
}
}
rebuildIndexFrom(data: VariableData[]) {
this._variablesArray.length = 0;
for (const variableData of data) {
if (variableData.name) {
const variable = this._variables.get(variableData.name);
this._variablesArray.push(variable);
}
}
}
/**
* Declare a new variable.
* This should only be used by generated code.
*
* @param name Variable name
* @param newVariable The variable to be declared
*/
_declare(name: string, newVariable: gdjs.Variable): void {
this._variables.put(name, newVariable);
this._variablesArray.push(newVariable);
}
/**
* Add a new variable.
* This can be costly, don't use in performance sensitive paths.
*
* @param name Variable name
* @param newVariable The variable to be added
*/
add(name: string, newVariable: gdjs.Variable) {
const oldVariable = this._variables.get(name);
// Variable is either already defined, considered as undefined
// in the container or missing in the container.
// Whatever the case, replace it by the new.
this._variables.put(name, newVariable);
if (oldVariable) {
// If variable is indexed, ensure that the variable as the index
// is replaced too. This can be costly (indexOf) but we assume `add` is not
// used in performance sensitive code.
const variableIndex = this._variablesArray.indexOf(oldVariable);
if (variableIndex !== -1) {
this._variablesArray[variableIndex] = newVariable;
}
}
}
/**
* Remove a variable.
* (the variable is not really removed from the container to avoid creating garbage, but marked as undefined)
* @param name Variable to be removed
*/
remove(name: string) {
const variable = this._variables.get(name);
if (variable) {
variable.setUndefinedInContainer();
}
}
/**
* Get a variable.
* @param name The variable's name
* @return The specified variable. If not found, an empty variable is added to the container.
*/
get(name: string): gdjs.Variable {
let variable = this._variables.get(name);
if (!variable) {
//Add automatically non-existing variables.
variable = new gdjs.Variable();
this._variables.put(name, variable);
} else {
if (
//Reuse variables removed before.
variable.isUndefinedInContainer()
) {
variable.reinitialize();
}
}
return variable;
}
/**
* Get a variable using its index. If you're unsure about how to use this method, prefer to use `get`.
* The index of a variable is its index in the data passed to initFrom.
*
* This method is generally used by events generated code to increase lookup speed for variables.
*
* @param id The variable index
* @return The specified variable. If not found, an empty variable is added to the container, but it
* should not happen.
*/
getFromIndex(id: number): gdjs.Variable {
if (id >= this._variablesArray.length) {
//Add automatically non-existing variables.
let variable = new gdjs.Variable();
this._variables.put('', variable);
return variable;
} else {
let variable: gdjs.Variable = this._variablesArray[id];
//Reuse variables removed before.
if (variable.isUndefinedInContainer()) {
variable.reinitialize();
}
return variable;
}
}
/**
* Check if a variable exists in the container.
* @param name The variable's name
* @return true if the variable exists.
*/
has(name: string): boolean {
const variable = this._variables.get(name);
return !!variable && !variable.isUndefinedInContainer();
}
/**
* Check if a variable exists in the container.
* @param variable The variable
* @return true if the variable exists.
*/
hasVariable(variable: gdjs.Variable): boolean {
const foundVariable = this._variablesArray.find((v) => v === variable);
return !!foundVariable && !foundVariable.isUndefinedInContainer();
}
getVariableNameInContainerByLoopingThroughAllVariables(
variable: gdjs.Variable
): string | null {
const variableItems = this._variables.items;
for (const variableName in variableItems) {
if (variableItems.hasOwnProperty(variableName)) {
if (variableItems[variableName] === variable) {
return variableName;
}
}
}
return null;
}
getVariablePathInContainerByLoopingThroughAllVariables(
variable: gdjs.Variable,
childrenToLookIn: Children | null = null
): string[] | null {
const variables = childrenToLookIn || this._variables.items;
for (const variableName in variables) {
if (variables.hasOwnProperty(variableName)) {
const variableItem = variables[variableName];
if (variableItem === variable) {
return [variableName];
} else if (variableItem.getType() === 'structure') {
const variableItemChildren = variableItem.getAllChildren();
const childPath =
this.getVariablePathInContainerByLoopingThroughAllVariables(
variable,
variableItemChildren
);
if (childPath) {
return [variableName, ...childPath];
}
}
}
}
return null;
}
getVariableFromPath(variablePath: string[]): gdjs.Variable | null {
let variableItems = this._variables.items;
for (let i = 0; i < variablePath.length; i++) {
const part = variablePath[i];
const nextVariable = variableItems[part];
if (!nextVariable) {
return null;
}
if (i === variablePath.length - 1) {
return nextVariable;
}
variableItems = nextVariable.getAllChildren();
}
return null;
}
static _deletedVars: Array<string | undefined> = [];
getNetworkSyncData(
syncOptions: GetNetworkSyncDataOptions
): VariableNetworkSyncData[] {
const networkSyncData: VariableNetworkSyncData[] = [];
const variableNames = [];
this._variables.keys(variableNames);
variableNames.forEach((variableName) => {
const variable = this._variables.get(variableName);
const variableSyncData = variable.getNetworkSyncData(syncOptions);
if (variableSyncData) {
networkSyncData.push({
name: variableName,
...variableSyncData,
});
}
});
return networkSyncData;
}
updateFromNetworkSyncData(
networkSyncData: VariableNetworkSyncData[],
options: UpdateFromNetworkSyncDataOptions
) {
const that = this;
for (let j = 0; j < networkSyncData.length; ++j) {
const variableSyncData = networkSyncData[j];
const variableData =
gdjs.Variable.getVariableDataFromNetworkSyncData(variableSyncData);
const variableName = variableData.name;
if (!variableName) continue;
const variable = that.get(variableName);
variable.updateFromNetworkSyncData(variableSyncData, options);
}
}
/**
* "Bad" variable container, used by events when no other valid container can be found.
* This container has no state and always returns the bad variable ( see VariablesContainer.badVariable ).
* @static
*/
static badVariablesContainer: VariablesContainer = {
_variables: new Hashtable(),
_variablesArray: [],
has: function () {
return false;
},
getFromIndex: function () {
return VariablesContainer.badVariable;
},
get: function () {
return VariablesContainer.badVariable;
},
remove: function () {
return;
},
add: function () {
return;
},
_declare: function () {
return;
},
initFrom: function () {
return;
},
getNetworkSyncData: function () {
return [];
},
updateFromNetworkSyncData: function () {
return;
},
hasVariable: function () {
return false;
},
getVariableNameInContainerByLoopingThroughAllVariables: function () {
return '';
},
getVariablePathInContainerByLoopingThroughAllVariables: function () {
return [];
},
getVariableFromPath: function () {
return null;
},
rebuildIndexFrom: function () {
return;
},
};
/**
* "Bad" variable, used by events when no other valid variable can be found.
* This variable has no state and always return 0 or the empty string.
* @static
*/
static badVariable: Variable = {
_type: 'number',
_bool: false,
_children: {},
_childrenArray: [],
_str: '',
_undefinedInContainer: true,
_value: 0,
_playerNumber: 0,
fromJSON: () => gdjs.VariablesContainer.badVariable,
toJSObject: () => 0,
fromJSObject: () => gdjs.VariablesContainer.badVariable,
reinitialize: () => {},
addChild: () => gdjs.VariablesContainer.badVariable,
castTo: () => {},
clearChildren: () => {},
clone: () => gdjs.VariablesContainer.badVariable,
getChildrenCount: () => 0,
replaceChildren: () => {},
replaceChildrenArray: () => {},
getType: function () {
return 'number';
},
isPrimitive: function () {
return true;
},
setValue: () => {},
toggle: () => {},
getValue: () => 0,
getChild: () => gdjs.VariablesContainer.badVariable,
getChildAt: () => gdjs.VariablesContainer.badVariable,
getChildNamed: () => gdjs.VariablesContainer.badVariable,
hasChild: function () {
return false;
},
isStructure: function () {
return false;
},
isNumber: function () {
return true;
},
removeChild: function () {
return;
},
setNumber: function () {
return;
},
setString: function () {
return;
},
setBoolean: function () {
return;
},
getAsString: function () {
return '0';
},
getAsNumber: function () {
return 0;
},
getAsNumberOrString: function () {
return 0;
},
getAsBoolean: function () {
return false;
},
getAllChildren: function () {
return {};
},
getAllChildrenArray: function () {
return [];
},
pushVariableCopy: () => {},
_pushVariable: () => {},
pushValue: () => {},
removeAtIndex: function () {
return;
},
add: function () {
return;
},
sub: function () {
return;
},
mul: function () {
return;
},
div: function () {
return;
},
concatenate: function () {
return;
},
concatenateString: function () {
return;
},
setUndefinedInContainer: function () {
return;
},
isUndefinedInContainer: function () {
return true;
},
getPlayerOwnership: function () {
return 0;
},
setPlayerOwnership: function () {
return;
},
disableSynchronization: function () {
return;
},
getNetworkSyncData: function () {
return undefined;
},
getStructureNetworkSyncData: function () {
return [];
},
updateFromNetworkSyncData: function () {
return;
},
};
}
}