Generalize fromInitAttributes loop

Add deepCompare function to utilities to easy and reliably compare js
variables.

Comments By: Langbart, Stan, asterix
Differential Revision: D4521
This was SVN commit r26583.
This commit is contained in:
bb
2022-03-06 21:01:36 +00:00
parent bc6ab4ac49
commit 2af020bab1
10 changed files with 143 additions and 41 deletions
@@ -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();
@@ -51,6 +51,69 @@ function heapsPermute(array, cloneFunc, callback)
}
}
/**
* 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)
*
@@ -68,19 +68,33 @@ class GameSettings
*/
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));
}
/**
@@ -37,10 +37,8 @@ GameSettings.prototype.Attributes.PlayerAI = class PlayerAI extends GameSetting
{
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)
@@ -27,10 +27,8 @@ GameSettings.prototype.Attributes.PlayerCiv = class PlayerCiv extends GameSettin
{
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);
}
@@ -32,10 +32,8 @@ GameSettings.prototype.Attributes.PlayerColor = class PlayerColor extends GameSe
{
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);
}
@@ -17,11 +17,8 @@ GameSettings.prototype.Attributes.PlayerCount = class PlayerCount extends GameSe
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 @@ GameSettings.prototype.Attributes.PlayerCount = class PlayerCount extends GameSe
{
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)
@@ -31,9 +31,7 @@ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSett
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;
@@ -27,10 +27,8 @@ GameSettings.prototype.Attributes.PlayerTeam = class PlayerTeam extends GameSett
{
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);
}
@@ -28,9 +28,7 @@ GameSettings.prototype.Attributes.StartingCamera = class StartingCamera extends
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;