Index: ps/trunk/binaries/data/mods/public/globalscripts/tests/test_deepCompare.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/tests/test_deepCompare.js +++ ps/trunk/binaries/data/mods/public/globalscripts/tests/test_deepCompare.js @@ -0,0 +1,39 @@ +function test_deepCompare() +{ + TS_ASSERT(deepCompare({}, {})); + TS_ASSERT(deepCompare([], [])); + TS_ASSERT(deepCompare({ "foo": NaN }, { "foo": NaN })); + TS_ASSERT(!deepCompare({ "foo": Infinity }, { "foo": NaN })); + TS_ASSERT(!deepCompare({ "foo": NaN }, { "foo": Infinity })); + TS_ASSERT(!deepCompare({ "foo": NaN }, { "bar": NaN })); + TS_ASSERT(!deepCompare({ "foo": NaN }, { "foo": NaN, "bar": NaN })); + TS_ASSERT(!deepCompare({ "foo": NaN, "bar": NaN }, { "foo": NaN })); + + TS_ASSERT(deepCompare(undefined, undefined)); + TS_ASSERT(deepCompare([undefined], [undefined])); + TS_ASSERT(deepCompare({ "foo": undefined }, { "foo": undefined })); + TS_ASSERT(!deepCompare({ "foo": undefined }, {})); + + // Ordering in objects does not matter. + TS_ASSERT(deepCompare({ "foo": NaN, "bar": NaN }, { "foo": NaN, "bar": NaN })); + TS_ASSERT(deepCompare({ "foo": NaN, "bar": NaN }, { "bar": NaN, "foo": NaN })); + + // Test some other JS structures. + TS_ASSERT(deepCompare(new Set(), new Set())); + TS_ASSERT(deepCompare(new Map(), new Map())); + TS_ASSERT(!deepCompare(new Set(), new Map())); + TS_ASSERT(!deepCompare(new Set([0]), new Set([1]))); + TS_ASSERT(!deepCompare(new Set([undefined]), new Set([null]))); + TS_ASSERT(deepCompare(new Set([NaN]), new Set([NaN]))); + TS_ASSERT(deepCompare(new Set([0, 0, 0]), new Set([0]))); + + // Ordering in arrays is relevant. + TS_ASSERT(deepCompare([1, 2, 3], [1, 2, 3])); + TS_ASSERT(!deepCompare([1, 2, 3], [3, 1, 2])); + + // Some nestling. + TS_ASSERT(deepCompare({ "foo": new Set([1, 2, { "baz": Infinity }]), "bar": [new Set([9]), { "foo": [0, 1, 2] }] }, + { "foo": new Set([1, 2, { "baz": Infinity }]), "bar": [new Set([9]), { "foo": [0, 1, 2] }] })); +} + +test_deepCompare(); Index: ps/trunk/binaries/data/mods/public/globalscripts/utility.js =================================================================== --- ps/trunk/binaries/data/mods/public/globalscripts/utility.js +++ ps/trunk/binaries/data/mods/public/globalscripts/utility.js @@ -52,6 +52,69 @@ } /** + * Compare two variables recursively. This compares better than a quick + * JSON.stringify check since we also check undefineds, Sets and the like. + * + * @param first - Any javascript instance. + * @param second - Any javascript instance. + * @return {boolean} Whether first and second are equal. + */ +function deepCompare(first, second) +{ + // If the value of either variable is empty we can instantly compare + // them and check for equality. + if (first === null || first === undefined || + second === null || second === undefined) + return first === second; + + // Make sure both variables have the same type. + if (first.constructor !== second.constructor) + return false; + + // We know that the variables are of the same type so all we need to do is + // check what type one of the objects is, and then compare them. + + // Check numbers seperately. Make sure this works with NaN, Infinity etc. + if (typeof first == "number") + return uneval(first) === uneval(second); + + // Functions and RegExps must have the same reference to be equal. + if (first instanceof Function || first instanceof RegExp) + return first === second; + + // If we are comparing simple objects, we can just compare them. + if (first === second || first.valueOf() === second.valueOf()) + return true; + + // Dates would have equal valueOf if they are equal. + if (first instanceof Date) + return false; + + // From now we will assume we have some kind of objects so that + // we can do a recursive check of the keys and values. + if (!(first instanceof Object) || !(second instanceof Object)) + return false; + + // We cannot iterate over Sets, so collapse them to Arrays. + if (first instanceof Set) + { + first = Array.from(first); + second = Array.from(second); + } + + // Objects need the same number of keys in order to be equal. + const firstKeys = Object.keys(first); + if (firstKeys.length != Object.keys(second).length) + return false; + + // Make sure that all the object keys on this level of the object are the same. + // Finally, we pass all the values of our of each object recursively to + // make sure everything matches. + return Object.keys(second).every(i => firstKeys.includes(i)) && + firstKeys.every(i => deepCompare(first[i], second[i])); +} + +/** * Removes prefixing path from a path or filename, leaving just the file's name (with extension) * * ie. a/b/c/file.ext -> file.ext Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/GameSettings.js @@ -68,19 +68,33 @@ */ fromInitAttributes(attribs) { - // Settings depend on the map, but some settings - // may also be illegal for a given map. - // It would be good to validate, but just bulk-accept at the moment. - // There is some light order-dependency between settings. - // First deserialize the map, then the player #, then victory conditions, then the rest. - // TODO: there's a DAG in there. - this.map.fromInitAttributes(attribs); - this.playerCount.fromInitAttributes(attribs); - this.victoryConditions.fromInitAttributes(attribs); - for (let comp in this) - if (this[comp].fromInitAttributes && - comp !== "map" && comp !== "playerCount" && comp !== "victoryConditions") - this[comp].fromInitAttributes(attribs); + // Settings may depend on eachother. Some selections of settings + // might be illegal. So keep looping through all settings until + // we find something stable. + const components = Object.keys(this); + + // To check in the loop below if something change we just compare + // the entire component. However, we must ignore the "settings" + // keyword to avoid cyclic objects. + const getComponentData = comp => Object.keys(this[comp]).map(key => key == "settings" ? undefined : this[comp][key]); + + // When we have looped components.length + 1 times, we are considered stuck. + for (let i = 0; i <= components.length; ++i) + { + // Re-init if any setting was changed, to make sure dependencies are cleared. + let reInit = false; + for (const comp in this) + { + const oldSettings = clone(getComponentData(comp)); + if (this[comp].fromInitAttributes) + this[comp].fromInitAttributes(attribs); + reInit = reInit || !deepCompare(oldSettings, getComponentData(comp)); + } + if (!reInit) + return; + } + + throw new Error("Infinite loop initializing attributes detected, components: " + uneval(components)); } /** Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerAI.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerAI.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerAI.js @@ -37,10 +37,8 @@ { if (!this.getLegacySetting(attribs, "PlayerData")) return; - let pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (let i in pData) + const pData = this.getLegacySetting(attribs, "PlayerData"); + for (let i = 0; i < this.values.length; ++i) { // Also covers the "" case. if (!pData[i] || !pData[i].AI) Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCiv.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCiv.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCiv.js @@ -27,10 +27,8 @@ { if (!this.getLegacySetting(attribs, "PlayerData")) return; - let pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (let i in pData) + const pData = this.getLegacySetting(attribs, "PlayerData"); + for (let i = 0; i < this.values.length; ++i) if (pData[i] && pData[i].Civ) this.setValue(i, pData[i].Civ); } Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerColor.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerColor.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerColor.js @@ -32,10 +32,8 @@ { if (!this.getLegacySetting(attribs, "PlayerData")) return; - let pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (let i in pData) + const pData = this.getLegacySetting(attribs, "PlayerData"); + for (let i = 0; i < this.values.length; ++i) if (pData[i] && pData[i].Color) this.setColor(i, pData[i].Color); } Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCount.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCount.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerCount.js @@ -17,11 +17,8 @@ fromInitAttributes(attribs) { - if (!this.getLegacySetting(attribs, "PlayerData")) - return; - let pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.nbPlayers !== pData.length) - this.nbPlayers = pData.length; + if (this.settings.map.map !== undefined) + this.reloadFromLegacy(attribs); } onMapTypeChange(old) @@ -44,7 +41,8 @@ { if (this.settings.map.type != "random") { - this.nbPlayers = this.settings.map.data.settings.PlayerData.length; + if (this.settings.map.data && this.settings.map.data.settings && this.settings.map.data.settings.PlayerData) + this.nbPlayers = this.settings.map.data.settings.PlayerData.length; return; } if (!data || !data.settings || data.settings.PlayerData === undefined) Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerName.js @@ -31,9 +31,7 @@ if (!this.getLegacySetting(attribs, "PlayerData")) return; const pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (const i in pData) + for (let i = 0; i < this.values.length; ++i) if (pData[i] && pData[i].Name !== undefined) { this.values[i] = pData[i].Name; Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerTeam.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerTeam.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/PlayerTeam.js @@ -27,10 +27,8 @@ { if (!this.getLegacySetting(attribs, "PlayerData")) return; - let pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (let i in pData) + const pData = this.getLegacySetting(attribs, "PlayerData"); + for (let i = 0; i < this.values.length; ++i) if (pData[i] && pData[i].Team !== undefined) this.setValue(i, pData[i].Team); } Index: ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js +++ ps/trunk/binaries/data/mods/public/gui/gamesettings/attributes/StartingCamera.js @@ -28,9 +28,7 @@ if (!this.getLegacySetting(attribs, "PlayerData")) return; const pData = this.getLegacySetting(attribs, "PlayerData"); - if (this.values.length < pData.length) - this._resize(pData.length); - for (const i in pData) + for (let i = 0; i < this.values.length; ++i) if (pData[i] && pData[i].StartingCamera !== undefined) { this.values[i] = pData[i].StartingCamera;