/* * 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; _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.
* 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.
* 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 = []; 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; }, }; } }