Index: binaries/data/mods/public/globalscripts/Technologies.js =================================================================== --- binaries/data/mods/public/globalscripts/Technologies.js +++ binaries/data/mods/public/globalscripts/Technologies.js @@ -9,17 +9,13 @@ * Returns modified property value modified by the applicable tech * modifications. * - * @param currentTechModifications Object with mapping of property names to - * modification arrays, retrieved from the intended player's TechnologyManager. - * @param classes Array contianing the class list of the template. - * @param propertyName String encoding the name of the value. - * @param propertyValue Number storing the original value. Can also be + * @param currentTechModifications array of modificiations + * @param classes Array containing the class list of the template. + * @param originalValue Number storing the original value. Can also be * non-numberic, but then only "replace" techs can be supported. */ -function GetTechModifiedProperty(currentTechModifications, classes, propertyName, propertyValue) +function GetTechModifiedProperty(modifications, classes, originalValue) { - let modifications = currentTechModifications[propertyName] || []; - let multiply = 1; let add = 0; @@ -34,13 +30,13 @@ else if (modification.add) add += modification.add; else - warn("GetTechModifiedProperty: modification format not recognised (modifying " + propertyName + "): " + uneval(modification)); + warn("GetTechModifiedProperty: modification format not recognised : " + uneval(modification)); } // Note, some components pass non-numeric values (for which only the "replace" modification makes sense) - if (typeof propertyValue == "number") - return propertyValue * multiply + add; - return propertyValue; + if (typeof originalValue == "number") + return originalValue * multiply + add; + return originalValue; } /** Index: binaries/data/mods/public/globalscripts/Templates.js =================================================================== --- binaries/data/mods/public/globalscripts/Templates.js +++ binaries/data/mods/public/globalscripts/Templates.js @@ -128,8 +128,8 @@ if (player) current_value = ApplyValueModificationsToTemplate(mod_key, current_value, player, template); - else if (modifiers) - current_value = GetTechModifiedProperty(modifiers, GetIdentityClasses(template.Identity), mod_key, current_value); + else if (modifiers && modifiers[mod_key]) + current_value = GetTechModifiedProperty(modifiers[mod_key], GetIdentityClasses(template.Identity), current_value); // Using .toFixed() to get around spidermonkey's treatment of numbers (3 * 1.1 = 3.3000000000000003 for instance). return +current_value.toFixed(8); Index: binaries/data/mods/public/simulation/components/AuraManager.js =================================================================== --- binaries/data/mods/public/simulation/components/AuraManager.js +++ /dev/null @@ -1,275 +0,0 @@ -function AuraManager() {} - -AuraManager.prototype.Schema = - ""; - -AuraManager.prototype.Init = function() -{ - this.modificationsCache = new Map(); - this.modifications = new Map(); - this.templateModificationsCache = new Map(); - this.templateModifications = new Map(); - - this.globalAuraSources = []; -}; - -AuraManager.prototype.RegisterGlobalAuraSource = function(ent) -{ - if (this.globalAuraSources.indexOf(ent) == -1) - this.globalAuraSources.push(ent); -}; - -AuraManager.prototype.UnregisterGlobalAuraSource = function(ent) -{ - let idx = this.globalAuraSources.indexOf(ent); - if (idx != -1) - this.globalAuraSources.splice(idx, 1); -}; - -AuraManager.prototype.ensureExists = function(name, value, id, key, defaultData) -{ - var cacheName = name + "Cache"; - var v = this[name].get(value); - if (!v) - { - v = new Map(); - this[name].set(value, v); - this[cacheName].set(value, new Map()); - } - - var i = v.get(id); - if (!i) - { - i = new Map(); - v.set(id, i); - this[cacheName].get(value).set(id, defaultData); - } - - var k = i.get(key); - if (!k) - { - k = {}; - i.set(key, k); - } - return k; -}; - -AuraManager.prototype.ApplyBonus = function(value, ents, newData, key) -{ - for (let ent of ents) - { - var data = this.ensureExists("modifications", value, ent, key, { "add":0, "multiply":1 }); - - if (data.count) - { - // this aura is already applied and the bonus shouldn't be given twice, - // just count the number of times it is applied - data.count++; - continue; - } - - // first time added this aura - data.multiply = newData.multiply; - data.add = newData.add; - data.count = 1; - - if (data.add) - this.modificationsCache.get(value).get(ent).add += data.add; - if (data.multiply) - this.modificationsCache.get(value).get(ent).multiply *= data.multiply; - - // post message to the entity to notify it about the change - Engine.PostMessage(ent, MT_ValueModification, { - "entities": [ent], - "component": value.split("/")[0], - "valueNames": [value] - }); - } -}; - -AuraManager.prototype.ApplyTemplateBonus = function(value, player, classes, newData, key) -{ - var data = this.ensureExists("templateModifications", value, player, key, new Map()); - - if (data.count) - { - // this aura is already applied and the bonus shouldn't be given twice, - // just count the number of times it is applied - data.count++; - return; - } - - // first time added this aura - data.multiply = newData.multiply; - data.add = newData.add; - data.count = 1; - - let cache = this.templateModificationsCache.get(value).get(player); - - // Do not use the classes array from the JSON file directly, since that is not synchronized - // See MatchesClassList for supported classes formats - for (let className of classes) - { - if (Array.isArray(className)) - className = className.join("+"); - - if (!cache.get(className)) - cache.set(className, new Map()); - - if (!cache.get(className).get(key)) - cache.get(className).set(key, { "add": 0, "multiply": 1 }); - - if (data.add) - cache.get(className).get(key).add += data.add; - if (data.multiply) - cache.get(className).get(key).multiply *= data.multiply; - } - - Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { - "player": player, - "component": value.split("/")[0], - "valueNames": [value] - }); -}; - -AuraManager.prototype.RemoveBonus = function(value, ents, key) -{ - var v = this.modifications.get(value); - if (!v) - return; - - for (let ent of ents) - { - var e = v.get(ent); - if (!e) - continue; - var data = e.get(key); - if (!data || !data.count) - continue; - - data.count--; - - if (data.count > 0) - continue; - - // out of last aura of this kind, remove modifications - if (data.add) - this.modificationsCache.get(value).get(ent).add -= data.add; - - if (data.multiply) - this.modificationsCache.get(value).get(ent).multiply /= data.multiply; - - // clean up the object - e.delete(key); - if (e.size == 0) - v.delete(ent); - - // post message to the entity to notify it about the change - Engine.PostMessage(ent, MT_ValueModification, { - "entities": [ent], - "component": value.split("/")[0], - "valueNames": [value] - }); - } -}; - -AuraManager.prototype.RemoveTemplateBonus = function(value, player, classes, key) -{ - var v = this.templateModifications.get(value); - if (!v) - return; - var p = v.get(player); - if (!p) - return; - var data = p.get(key); - if (!data || !data.count) - return; - - data.count--; - - if (data.count > 0) - return; - - for (let className of classes) - { - if (Array.isArray(className)) - className = className.join("+"); - - this.templateModificationsCache.get(value).get(player).get(className).delete(key); - - if (this.templateModificationsCache.get(value).get(player).get(className).size == 0) - this.templateModificationsCache.get(value).get(player).delete(className); - } - - // clean up the object - p.delete(key); - if (p.size == 0) - v.delete(player); - - Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { - "player": player, - "component": value.split("/")[0], - "valueNames": [value] - }); -}; - -AuraManager.prototype.ApplyModifications = function(valueName, value, ent) -{ - var v = this.modificationsCache.get(valueName); - if (!v) - return value; - var cache = v.get(ent); - if (!cache) - return value; - - value *= cache.multiply; - value += cache.add; - return value; -}; - -AuraManager.prototype.ApplyTemplateModifications = function(valueName, value, player, template) -{ - var v = this.templateModificationsCache.get(valueName); - if (!v) - return value; - var cache = v.get(player); - if (!cache) - return value; - - if (!template || !template.Identity) - return value; - var classes = GetIdentityClasses(template.Identity); - - var usedKeys = new Set(); - var add = 0; - var multiply = 1; - - for (let [className, mods] of cache) - { - if (!MatchesClassList(classes, [className])) - continue; - - for (let [key, mod] of mods) - { - // don't add an aura with the same key twice - if (usedKeys.has(key)) - continue; - add += mod.add; - multiply *= mod.multiply; - usedKeys.add(key); - } - } - return value * multiply + add; -}; - -AuraManager.prototype.OnGlobalOwnershipChanged = function(msg) -{ - for (let ent of this.globalAuraSources) - { - let cmpAuras = Engine.QueryInterface(ent, IID_Auras); - if (cmpAuras) - cmpAuras.RegisterGlobalOwnershipChanged(msg); - } -}; - -Engine.RegisterSystemComponentType(IID_AuraManager, "AuraManager", AuraManager); Index: binaries/data/mods/public/simulation/components/Auras.js =================================================================== --- binaries/data/mods/public/simulation/components/Auras.js +++ binaries/data/mods/public/simulation/components/Auras.js @@ -22,8 +22,8 @@ Auras.prototype.GetModifierIdentifier = function(name) { if (AuraTemplates.Get(name).stackable) - return name + this.entity; - return name; + return "aura/" + name + this.entity; + return "aura/" + name; }; Auras.prototype.GetDescriptions = function() @@ -218,9 +218,9 @@ targetUnitsClone[name] = this[name].targetUnits.slice(); if (this.IsGlobalAura(name)) - this.RemoveTemplateBonus(name); + this.RemoveTemplateAura(name); - this.RemoveBonus(name, this[name].targetUnits); + this.RemoveAura(name, this[name].targetUnits, this.IsGlobalAura(name)); if (this[name].rangeQuery) cmpRangeManager.DestroyActiveQuery(this[name].rangeQuery); @@ -242,36 +242,38 @@ if (this.IsGlobalAura(name)) { - this.ApplyTemplateBonus(name, affectedPlayers); - for (let player of affectedPlayers) - this.ApplyBonus(name, cmpRangeManager.GetEntitiesByPlayer(player)); + this.ApplyTemplateAura(name, affectedPlayers); + if (this.GetOverlayIcon(name)) + for (let player of affectedPlayers) + // Only apply icons since modifications are applied in ApplyTemplateAura above. + this.ApplyAura(name, cmpRangeManager.GetEntitiesByPlayer(player), true); continue; } if (this.IsPlayerAura(name)) { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); - this.ApplyBonus(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p))); + this.ApplyAura(name, affectedPlayers.map(p => cmpPlayerManager.GetPlayerByID(p))); continue; } if (!this.IsRangeAura(name)) { - this.ApplyBonus(name, targetUnitsClone[name]); + this.ApplyAura(name, targetUnitsClone[name]); continue; } needVisualizationUpdate = true; - if (this[name].isApplied) + if (this[name].isApplied && (this.IsRangeAura(name) || this.IsGlobalAura(name) && !!this.GetOverlayIcon(name))) { this[name].rangeQuery = cmpRangeManager.CreateActiveQuery( - this.entity, - 0, - this.GetRange(name), - affectedPlayers, - IID_Identity, - cmpRangeManager.GetEntityFlagMask("normal") + this.entity, + 0, + this.GetRange(name), + affectedPlayers, + IID_Identity, + cmpRangeManager.GetEntityFlagMask("normal") ); cmpRangeManager.EnableActiveQuery(this[name].rangeQuery); } @@ -301,8 +303,8 @@ { for (let name of this.GetAuraNames().filter(n => this[n] && msg.tag == this[n].rangeQuery)) { - this.ApplyBonus(name, msg.added); - this.RemoveBonus(name, msg.removed); + this.ApplyAura(name, msg.added); + this.RemoveAura(name, msg.removed); } }; @@ -310,87 +312,86 @@ { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonedUnitsAura(n))) { - this.ApplyBonus(name, msg.added); - this.RemoveBonus(name, msg.removed); + this.ApplyAura(name, msg.added); + this.RemoveAura(name, msg.removed); } }; -Auras.prototype.RegisterGlobalOwnershipChanged = function(msg) -{ - for (let name of this.GetAuraNames().filter(n => this.IsGlobalAura(n))) - { - let affectedPlayers = this.GetAffectedPlayers(name); - let wasApplied = affectedPlayers.indexOf(msg.from) != -1; - let willBeApplied = affectedPlayers.indexOf(msg.to) != -1; - if (wasApplied && !willBeApplied) - this.RemoveBonus(name, [msg.entity]); - if (willBeApplied && !wasApplied) - this.ApplyBonus(name, [msg.entity]); - } -}; - -Auras.prototype.ApplyFormationBonus = function(memberList) +Auras.prototype.ApplyFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) - this.ApplyBonus(name, memberList); + this.ApplyAura(name, memberList); }; -Auras.prototype.ApplyGarrisonBonus = function(structure) +Auras.prototype.ApplyGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) - this.ApplyBonus(name, [structure]); + this.ApplyAura(name, [structure]); }; -Auras.prototype.ApplyTemplateBonus = function(name, players) +Auras.prototype.ApplyTemplateAura = function(name, players) { if (!this[name].isApplied) return; if (!this.IsGlobalAura(name)) return; - var modifications = this.GetModifications(name); - var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - var classes = this.GetClasses(name); - cmpAuraManager.RegisterGlobalAuraSource(this.entity); + let derivedModifiers = DeriveModificationsFromTech({ + "modifications": this.GetModifications(name), + "affects": this.GetClasses(name) + }); + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); - for (let mod of modifications) - for (let player of players) - cmpAuraManager.ApplyTemplateBonus(mod.value, player, classes, mod, this.GetModifierIdentifier(name)); + let modifName = this.GetModifierIdentifier(name); + for (let player of players) + { + let playerId = cmpPlayerManager.GetPlayerByID(player); + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.AddModif(modifierPath, modifName, modifier, playerId); + } }; -Auras.prototype.RemoveFormationBonus = function(memberList) +Auras.prototype.RemoveFormationAura = function(memberList) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) - this.RemoveBonus(name, memberList); + this.RemoveAura(name, memberList); }; -Auras.prototype.RemoveGarrisonBonus = function(structure) +Auras.prototype.RemoveGarrisonAura = function(structure) { for (let name of this.GetAuraNames().filter(n => this.IsGarrisonAura(n))) - this.RemoveBonus(name, [structure]); + this.RemoveAura(name, [structure]); }; -Auras.prototype.RemoveTemplateBonus = function(name) +Auras.prototype.RemoveTemplateAura = function(name) { if (!this[name].isApplied) return; + if (!this.IsGlobalAura(name)) return; - var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - cmpAuraManager.UnregisterGlobalAuraSource(this.entity); - - var modifications = this.GetModifications(name); - var classes = this.GetClasses(name); - var players = this.GetAffectedPlayers(name); + let derivedModifiers = DeriveModificationsFromTech({ + "modifications": this.GetModifications(name), + "affects": this.GetClasses(name) + }); + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); - for (let mod of modifications) - for (let player of players) - cmpAuraManager.RemoveTemplateBonus(mod.value, player, classes, this.GetModifierIdentifier(name)); + let modifName = this.GetModifierIdentifier(name); + for (let player of this.GetAffectedPlayers(name)) + { + let playerId = cmpPlayerManager.GetPlayerByID(player); + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.RemoveModif(modifierPath, modifName, playerId); + } }; -Auras.prototype.ApplyBonus = function(name, ents) +Auras.prototype.ApplyAura = function(name, ents, skipModifications = false) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) @@ -401,24 +402,35 @@ if (!this[name].isApplied) return; - var modifications = this.GetModifications(name); - var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - - for (let mod of modifications) - cmpAuraManager.ApplyBonus(mod.value, validEnts, mod, this.GetModifierIdentifier(name)); // update status bars if this has an icon - if (!this.GetOverlayIcon(name)) + if (this.GetOverlayIcon(name)) + for (let ent of validEnts) + { + let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); + if (cmpStatusBars) + cmpStatusBars.AddAuraSource(this.entity, name); + } + + // Global aura modifications are handled at the player level by the modification manager. + if (skipModifications) return; + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + + let derivedModifiers = DeriveModificationsFromTech({ + "modifications": this.GetModifications(name), + "affects": this.GetClasses(name) + }); + + let modifName = this.GetModifierIdentifier(name); for (let ent of validEnts) - { - var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); - if (cmpStatusBars) - cmpStatusBars.AddAuraSource(this.entity, name); - } + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.AddModif(modifierPath, modifName, modifier, ent); + }; -Auras.prototype.RemoveBonus = function(name, ents) +Auras.prototype.RemoveAura = function(name, ents, skipModifications = false) { var validEnts = this.GiveMembersWithValidClass(name, ents); if (!validEnts.length) @@ -429,22 +441,31 @@ if (!this[name].isApplied) return; - var modifications = this.GetModifications(name); - var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - - for (let mod of modifications) - cmpAuraManager.RemoveBonus(mod.value, validEnts, this.GetModifierIdentifier(name)); - // update status bars if this has an icon - if (!this.GetOverlayIcon(name)) + if (this.GetOverlayIcon(name)) + for (let ent of validEnts) + { + let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); + if (cmpStatusBars) + cmpStatusBars.RemoveAuraSource(this.entity, name); + } + + // Global aura modifications are handled at the player level by the modification manager. + if (skipModifications) return; - for (let ent of validEnts) - { - var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); - if (cmpStatusBars) - cmpStatusBars.RemoveAuraSource(this.entity, name); - } + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + + let derivedModifiers = DeriveModificationsFromTech({ + "modifications": this.GetModifications(name), + "affects": this.GetClasses(name) + }); + + let modifName = this.GetModifierIdentifier(name); + for (let ent of ents) + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.RemoveModif(modifierPath, modifName, ent); }; Auras.prototype.OnOwnershipChanged = function(msg) @@ -484,7 +505,7 @@ { let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player); if (cmpPlayer && cmpPlayer.GetPlayerID() == msg.playerId || - this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1)) + this.GetAuraNames().some(name => this.GetAffectedPlayers(name).indexOf(msg.playerId) != -1)) this.Clean(); }; Index: binaries/data/mods/public/simulation/components/Formation.js =================================================================== --- binaries/data/mods/public/simulation/components/Formation.js +++ binaries/data/mods/public/simulation/components/Formation.js @@ -294,7 +294,7 @@ if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); - cmpAuras.ApplyFormationBonus(ents); + cmpAuras.ApplyFormationAura(ents); } } @@ -326,11 +326,11 @@ for (var ent of this.formationMembersWithAura) { var cmpAuras = Engine.QueryInterface(ent, IID_Auras); - cmpAuras.RemoveFormationBonus(ents); + cmpAuras.RemoveFormationAura(ents); // the unit with the aura is also removed from the formation if (ents.indexOf(ent) !== -1) - cmpAuras.RemoveFormationBonus(this.members); + cmpAuras.RemoveFormationAura(this.members); } this.formationMembersWithAura = this.formationMembersWithAura.filter(function(e) { return ents.indexOf(e) == -1; }); @@ -359,7 +359,7 @@ for (let ent of this.formationMembersWithAura) { let cmpAuras = Engine.QueryInterface(ent, IID_Auras); - cmpAuras.ApplyFormationBonus(ents); + cmpAuras.ApplyFormationAura(ents); } this.members = this.members.concat(ents); @@ -373,7 +373,7 @@ if (cmpAuras && cmpAuras.HasFormationAura()) { this.formationMembersWithAura.push(ent); - cmpAuras.ApplyFormationBonus(this.members); + cmpAuras.ApplyFormationAura(this.members); } } @@ -413,7 +413,7 @@ for (var ent of this.formationMembersWithAura) { var cmpAuras = Engine.QueryInterface(ent, IID_Auras); - cmpAuras.RemoveFormationBonus(this.members); + cmpAuras.RemoveFormationAura(this.members); } Index: binaries/data/mods/public/simulation/components/GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/GarrisonHolder.js @@ -266,7 +266,7 @@ let cmpAura = Engine.QueryInterface(entity, IID_Auras); if (cmpAura && cmpAura.HasGarrisonAura()) - cmpAura.ApplyGarrisonBonus(this.entity); + cmpAura.ApplyGarrisonAura(this.entity); Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added": [entity], "removed": [] }); return true; @@ -336,7 +336,7 @@ let cmpEntAura = Engine.QueryInterface(entity, IID_Auras); if (cmpEntAura && cmpEntAura.HasGarrisonAura()) - cmpEntAura.RemoveGarrisonBonus(this.entity); + cmpEntAura.RemoveGarrisonAura(this.entity); cmpEntPosition.JumpTo(pos.x, pos.z); cmpEntPosition.SetHeightOffset(0); Index: binaries/data/mods/public/simulation/components/ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/ModificationsManager.js @@ -0,0 +1,304 @@ +function ModificationsManager() {} + +ModificationsManager.prototype.Schema = + ""; + +ModificationsManager.prototype.Init = function() +{ + // TODO: + // - add a way to show an icon for a given modification ID + // > Note that aura code shows icons when the source is selected, so that's specific to them. + // - support stacking modifiers (MultiKeyMap handles it but not this manager). + + // The cache computes values lazily when they are needed. + // Helper functions remove items that have been changed to ensure we stay up-to-date. + this.cachedValues = new Map(); // Keyed by property name, entity ID, original values. + + // When changing global modifications, all entity-local caches are invalidated. This helps with that. + // TODO: it might be worth keying by classes here. + this.playerEntitiesCached = new Map(); // Keyed by player ID, property name, entity ID. + + this.modifsStorage = new MultiKeyMap(); // Keyed by property name, entity. + + this.modifsStorage._OnItemModified = (prim, sec, itemID) => this.ModificationsChanged.apply(this, [prim, sec, itemID]); +}; + +ModificationsManager.prototype.Serialize = function() +{ + // The value cache will be affected by property reads from the GUI and other places so we shouldn't serialize it. + // Furthermore it is cyclically self-referencing. + // We need to store the player for the Player-Entities cache. + let players = []; + this.playerEntitiesCached.forEach((_, player) => players.push(player)); + return { + "modifsStorage": this.modifsStorage.Serialize(), + "players": players + }; +}; + +ModificationsManager.prototype.Deserialize = function(data) +{ + this.Init(); + this.modifsStorage.Deserialize(data.modifsStorage); + data.players.forEach(player => this.playerEntitiesCached.set(player, new Map())); +}; + +/** + * Inform entities that we have changed possibly all values affected by that property. + * It's not hugely efficient and would be nice to batch. + * Invalidate caches where relevant. + */ +ModificationsManager.prototype.ModificationsChanged = function(propertyName, entity) +{ + this.InvalidateCache(propertyName, entity); + + let cmpPlayer = Engine.QueryInterface(entity, IID_Player); + if (cmpPlayer) + this.SendPlayerModificationMessages(propertyName, cmpPlayer.GetPlayerID()); + else + this.SendEntityModificationMessages(propertyName, entity); +}; + +ModificationsManager.prototype.SendEntityModificationMessages = function(propertyName, entity) +{ + Engine.PostMessage(entity, MT_ValueModification, { "entities": [entity], "component": propertyName.split("/")[0], "valueNames": [propertyName] }); +}; + +ModificationsManager.prototype.SendPlayerModificationMessages = function(propertyName, player) +{ + // TODO: it would be preferable to be able to batch this (i.e. one message for several properties) + Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": player, "component": propertyName.split("/")[0], "valueNames": [propertyName] }); + // AIInterface wants the entities potentially affected. + // TODO: improve on this + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + let ents = cmpRangeManager.GetEntitiesByPlayer(player); + Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": propertyName.split("/")[0], "valueNames": [propertyName] }); +}; + +ModificationsManager.prototype.InvalidatePlayerEntCache = function(propertyName, entity) +{ + if (!this.cachedValues.has(propertyName)) + return; + + if (!this.playerEntitiesCached.has(entity)) + return; + + if (!this.playerEntitiesCached.get(entity).has(propertyName)) + return; + + // Invalidate all local caches directly (for simplicity in ApplyModifications). + let entsMap = this.playerEntitiesCached.get(entity).get(propertyName); + entsMap.forEach(ent => this.cachedValues.get(propertyName).delete(ent)); + entsMap.clear(); +}; + +ModificationsManager.prototype.InvalidateCache = function(propertyName, entity) +{ + this.InvalidatePlayerEntCache(propertyName, entity); + + if (!this.cachedValues.has(propertyName)) + return; + if (this.cachedValues.get(propertyName).delete(entity)) + if (!this.cachedValues.get(propertyName).size) + this.cachedValues.delete(propertyName); +}; + +/** + * @returns originalValue after modifications. + */ +ModificationsManager.prototype.FetchModifiedProperty = function(classesList, propertyName, originalValue, target) +{ + return GetTechModifiedProperty(this.modifsStorage.GetItems(propertyName, target), classesList, originalValue); +}; + +ModificationsManager.prototype.GetCached = function(propertyName, originalValue, entity) +{ + if (!this.cachedValues.has(propertyName)) + return null; + if (!this.cachedValues.get(propertyName).has(entity)) + return null; + if (!this.cachedValues.get(propertyName).get(entity).has(originalValue)) + return null; + + return this.cachedValues.get(propertyName).get(entity).get(originalValue); +}; + +/** + * @returns originalValue after modifications + */ +ModificationsManager.prototype.Cache = function(classesList, propertyName, originalValue, entity) +{ + // Initialise the cache if necessary. + if (!this.cachedValues.has(propertyName)) + this.cachedValues.set(propertyName, new Map()); + if (!this.cachedValues.get(propertyName).has(entity)) + this.cachedValues.get(propertyName).set(entity, new Map()); + + let value = this.FetchModifiedProperty(classesList, propertyName, originalValue, entity); + this.cachedValues.get(propertyName).get(entity).set(originalValue, value); + return value; +}; + +/** + * Caching system in front of FetchModifiedProperty(), as calling that every time is quite slow. + * This recomputes lazily. + * Applies per-player modifications before per-entity modifications, so the latter take priority; + * @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed. + * @param originalValue - template/raw/before-modifications value. + Note that if this is supposed to be a number (i.e. you call add/multiply on it) + You must make sure to pass a number and not a string (by using + if necessary) + * @param ent - ID of the target entity + * @returns originalValue after the modifications + */ +ModificationsManager.prototype.ApplyModifications = function(propertyName, originalValue, entity) +{ + let cmpIdentity = Engine.QueryInterface(entity, IID_Identity); + if (!cmpIdentity) + return originalValue; + + // Sanitize input. + if (Array.isArray(originalValue)) + // Some code bits pass an array instead of a single value. + // Those bits should probably be changed, but it is supported in the meantime. + originalValue = originalValue.slice(); + else if (typeof originalValue == "object") + { + error("ModificationsManager ApplyModifications() called with an object instead of a number."); + return undefined; + } + + let newValue = this.GetCached(propertyName, originalValue, entity); + if (newValue !== null) + return newValue; + + // Get the entity ID of the player / owner of the entity, since we use that to store per-player modifications + // (this prevents conflicts between player ID and entity ID). + let ownerEntity = QueryOwnerEntityID(entity); + if (ownerEntity == entity) + ownerEntity = null; + + newValue = originalValue; + + // Apply player-wide modifications before entity-local modifications. + if (ownerEntity) + { + if (!this.playerEntitiesCached.get(ownerEntity).has(propertyName)) + this.playerEntitiesCached.get(ownerEntity).set(propertyName, new Set()); + this.playerEntitiesCached.get(ownerEntity).get(propertyName).add(entity); + newValue = this.FetchModifiedProperty(cmpIdentity.GetClassesList(), propertyName, newValue, ownerEntity); + } + newValue = this.Cache(cmpIdentity.GetClassesList(), propertyName, newValue, entity); + + return newValue; +}; + +/** + * Alternative version of ApplyModifications, applies to templates instead of entities. + * Only needs to handle global modifications. + */ +ModificationsManager.prototype.ApplyModificationsTemplate = function(propertyName, originalValue, template, player) +{ + if (!template || !template.Identity) + return originalValue; + + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + return this.FetchModifiedProperty(GetIdentityClasses(template.Identity), propertyName, originalValue, cmpPlayerManager.GetPlayerByID(player)); +}; + +/** + * For efficiency in InvalidateCache, keep playerEntitiesCached updated. + */ +ModificationsManager.prototype.OnGlobalPlayerEntityChanged = function(msg) +{ + if (msg.to != INVALID_PLAYER && !this.playerEntitiesCached.has(msg.to)) + this.playerEntitiesCached.set(msg.to, new Map()); + + if (msg.from != INVALID_PLAYER && this.playerEntitiesCached.has(msg.from)) + { + this.playerEntitiesCached.get(msg.from).forEach(propName => this.InvalidateCache(propName, msg.from)); + this.playerEntitiesCached.delete(msg.from); + } +}; + +/** + * Handle modifications when an entity changes owner. + * We do not retain the original modifications for now. + */ +ModificationsManager.prototype.OnGlobalOwnershipChanged = function(msg) +{ + if (msg.to == INVALID_PLAYER) + return; + + // Invalidate all caches. + for (let propName of this.cachedValues.keys()) + this.InvalidateCache(propName, msg.entity); + + let owner = QueryOwnerEntityID(msg.entity); + if (!owner) + return; + + let cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity); + if (!cmpIdentity) + return; + + let classes = cmpIdentity.GetClassesList(); + + // Warn entities that our values have changed. + // Local modifications will be added by the relevant components, so no need to check for them here. + let modifiedComponents = {}; + let playerModifs = this.modifsStorage.GetAllItems(owner); + for (let propertyName in playerModifs) + { + // We only need to find one one tech per component for a match. + let component = propertyName.split("/")[0]; + // Only inform if the modification actually applies to the entity as an optimisation. + // TODO: would it be better to call FetchModifiedProperty here and compare values? + playerModifs[propertyName].forEach(modif => { + if (!DoesModificationApply(modif, classes)) + return; + if (!modifiedComponents[component]) + modifiedComponents[component] = []; + modifiedComponents[component].push(propertyName); + }); + } + + for (let component in modifiedComponents) + Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); +}; + +/** + * The following functions simply proxy MultiKeyMap's interface. + */ +ModificationsManager.prototype.AddModif = function(primaryKey, ModifID, Modif, secondaryKey, stackable = false) { + return this.modifsStorage.AddItem(primaryKey, ModifID, Modif, secondaryKey, stackable); +}; + +ModificationsManager.prototype.AddModifs = function(ModifID, Modifs, secondaryKey, stackable = false) { + return this.modifsStorage.AddItems(ModifID, Modifs, secondaryKey, stackable); +}; + +ModificationsManager.prototype.RemoveModif = function(primaryKey, ModifID, secondaryKey, stackable = false) { + return this.modifsStorage.RemoveItem(primaryKey, ModifID, secondaryKey, stackable); +}; + +ModificationsManager.prototype.RemoveAllModifs = function(ModifID, secondaryKey, stackable = false) { + return this.modifsStorage.RemoveAllItems(ModifID, secondaryKey, stackable); +}; + +ModificationsManager.prototype.HasModif = function(primaryKey, ModifID, secondaryKey) { + return this.modifsStorage.HasItem(primaryKey, ModifID, secondaryKey); +}; + +ModificationsManager.prototype.HasAnyModif = function(ModifID, secondaryKey) { + return this.modifsStorage.HasAnyItem(ModifID, secondaryKey); +}; + +ModificationsManager.prototype.GetModifs = function(primaryKey, secondaryKey, stackable = false) { + return this.modifsStorage.GetItems(primaryKey, secondaryKey, stackable); +}; + +ModificationsManager.prototype.GetAllModifs = function(secondaryKey, stackable = false) { + return this.modifsStorage.GetAllItems(secondaryKey, stackable); +}; + +Engine.RegisterSystemComponentType(IID_ModificationsManager, "ModificationsManager", ModificationsManager); Index: binaries/data/mods/public/simulation/components/TechnologyManager.js =================================================================== --- binaries/data/mods/public/simulation/components/TechnologyManager.js +++ binaries/data/mods/public/simulation/components/TechnologyManager.js @@ -3,21 +3,6 @@ TechnologyManager.prototype.Schema = ""; -TechnologyManager.prototype.Serialize = function() -{ - // The modifications cache will be affected by property reads from the GUI and other places so we shouldn't - // serialize it. - - var ret = {}; - for (var i in this) - { - if (this.hasOwnProperty(i)) - ret[i] = this[i]; - } - ret.modificationCache = {}; - return ret; -}; - TechnologyManager.prototype.Init = function() { // Holds names of technologies that have been researched. @@ -29,16 +14,6 @@ // Holds technologies which are being researched currently (non-queued). this.researchStarted = new Set(); - // This stores the modifications to unit stats from researched technologies - // Example data: {"ResourceGatherer/Rates/food.grain": [ - // {"multiply": 1.15, "affects": ["FemaleCitizen", "Infantry Sword"]}, - // {"add": 2} - // ]} - this.modifications = {}; - this.modificationCache = {}; // Caches the values after technologies have been applied - // e.g. { "Attack/Melee/Damage/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...} - // where 5 and 7 are entity id's - this.classCounts = {}; // stores the number of entities of each Class this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e. // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...} @@ -204,31 +179,6 @@ this.typeCountsByClass[cls][template] += 1; } } - - // Newly created entity, check if any researched techs might apply - // (only do this for new entities because even if an entity is converted or captured, - // we want it to maintain whatever technologies previously applied) - if (msg.from == INVALID_PLAYER) - { - var modifiedComponents = {}; - for (var name in this.modifications) - { - // We only need to find one one tech per component for a match - var modifications = this.modifications[name]; - var component = name.split("/")[0]; - for (let modif of modifications) - if (DoesModificationApply(modif, classes)) - { - if (!modifiedComponents[component]) - modifiedComponents[component] = []; - modifiedComponents[component].push(name); - } - } - - // Send mesage(s) to the entity so it knows about researched techs - for (var component in modifiedComponents) - Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] }); - } } if (msg.from == playerID) { @@ -254,8 +204,6 @@ } } } - - this.clearModificationCache(msg.entity); } }; @@ -266,23 +214,16 @@ var modifiedComponents = {}; this.researchedTechs.add(tech); + // store the modifications in an easy to access structure let template = TechnologyTemplates.Get(tech); if (template.modifications) { + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); let derivedModifiers = DeriveModificationsFromTech(template); for (let modifierPath in derivedModifiers) - { - if (!this.modifications[modifierPath]) - this.modifications[modifierPath] = []; - this.modifications[modifierPath] = this.modifications[modifierPath].concat(derivedModifiers[modifierPath]); - - let component = modifierPath.split("/")[0]; - if (!modifiedComponents[component]) - modifiedComponents[component] = []; - modifiedComponents[component].push(modifierPath); - this.modificationCache[modifierPath] = {}; - } + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.AddModif(modifierPath, "tech/" + tech, modifier, this.entity); } if (template.replaces && template.replaces.length > 0) @@ -323,62 +264,6 @@ // always send research finished message Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech}); - - for (var component in modifiedComponents) - { - Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]}); - Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]}); - } - - if (tech.startsWith("phase") && !template.autoResearch) - { - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.PushNotification({ - "type": "phase", - "players": [playerID], - "phaseName": tech, - "phaseState": "completed" - }); - } -}; - -// Clears the cached data for an entity from the modifications cache -TechnologyManager.prototype.clearModificationCache = function(ent) -{ - for (var valueName in this.modificationCache) - delete this.modificationCache[valueName][ent]; -}; - -// Caching layer in front of ApplyModificationsWorker -// Note: be careful with the type of curValue, if it should be a numerical -// value and is derived from template data, you must convert the string -// from the template to a number using the + operator, before calling -// this function! -TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent) -{ - if (!this.modificationCache[valueName]) - this.modificationCache[valueName] = {}; - - if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue) - { - let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); - if (!cmpIdentity) - return curValue; - this.modificationCache[valueName][ent] = { - "origValue": curValue, - "newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), valueName, curValue) - }; - } - - return this.modificationCache[valueName][ent].newValue; -}; - -// Alternative version of ApplyModifications, applies to templates instead of entities -TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template) -{ - if (!template || !template.Identity) - return curValue; - return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue); }; /** Index: binaries/data/mods/public/simulation/components/interfaces/AuraManager.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/AuraManager.js +++ /dev/null @@ -1 +0,0 @@ -Engine.RegisterInterface("AuraManager"); Index: binaries/data/mods/public/simulation/components/interfaces/ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/interfaces/ModificationsManager.js @@ -0,0 +1 @@ +Engine.RegisterInterface("ModificationsManager"); Index: binaries/data/mods/public/simulation/components/tests/test_Attack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -3,9 +3,9 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Index: binaries/data/mods/public/simulation/components/tests/test_AuraManager.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_AuraManager.js +++ /dev/null @@ -1,43 +0,0 @@ -Engine.LoadComponentScript("interfaces/AuraManager.js"); -Engine.LoadComponentScript("AuraManager.js"); - -let value = "Component/Value"; -let player1 = 1; -let player2 = 2; -let ents1 = [25, 26, 27]; -let ents2 = [28, 29, 30]; -let ents3 = [31]; -let classes = ["class1", "class2"]; -let template = { "Identity" : { "Classes" : { "_string" : "class1 class3" } } }; - -let cmpAuraManager = ConstructComponent(SYSTEM_ENTITY, "AuraManager", {}); - -// Apply and remove a bonus -cmpAuraManager.ApplyBonus(value, ents1, { "add": 8 }, "key1"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 25), 18); -// It isn't apply to wrong entity -TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 28), 10); -cmpAuraManager.RemoveBonus(value, ents1, "key1"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 25), 10); - -// Apply 2 bonus with two different keys. Bonus should stack -cmpAuraManager.ApplyBonus(value, ents2, { "add": 8 }, "key1"); -cmpAuraManager.ApplyBonus(value, ents2, { "multiply": 3 }, "key2"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 28), 38); - -// With another operation ordering, the result must be the same -cmpAuraManager.ApplyBonus(value, ents3, { "multiply": 3 }, "key2"); -cmpAuraManager.ApplyBonus(value, ents3, { "add": 8 }, "key1"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyModifications(value, 10, 31), 38); - -// Apply bonus to templates -cmpAuraManager.ApplyTemplateBonus(value, player1, classes, { "add": 10 }, "key3"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 310); -cmpAuraManager.RemoveTemplateBonus(value, player1, classes, "key3"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 300); -cmpAuraManager.ApplyTemplateBonus(value, player2, classes, { "add": 10 }, "key3"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player2, template), 310); -cmpAuraManager.ApplyTemplateBonus(value, player1, classes, { "add": 10 }, "key3"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player1, template), 310); -cmpAuraManager.RemoveTemplateBonus(value, player2, classes, "key3"); -TS_ASSERT_EQUALS(cmpAuraManager.ApplyTemplateModifications(value, 300, player2, template), 300); Index: binaries/data/mods/public/simulation/components/tests/test_Auras.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Auras.js +++ binaries/data/mods/public/simulation/components/tests/test_Auras.js @@ -1,11 +1,12 @@ +Engine.LoadHelperScript("MultiKeyMap.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Auras.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/RangeOverlayManager.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("Auras.js"); -Engine.LoadComponentScript("AuraManager.js"); +Engine.LoadComponentScript("ModificationsManager.js"); var playerID = [0, 1, 2]; var playerEnt = [10, 11, 12]; @@ -89,7 +90,9 @@ "GetOwner": () => playerID[1] }); - ConstructComponent(SYSTEM_ENTITY, "AuraManager", {}); + let cmpModificationsManager = ConstructComponent(SYSTEM_ENTITY, "ModificationsManager", {}); + cmpModificationsManager.OnGlobalPlayerEntityChanged({ player: playerID[1], from: -1, to: playerEnt[1] }); + cmpModificationsManager.OnGlobalPlayerEntityChanged({ player: playerID[2], from: -1, to: playerEnt[2] }); let cmpAuras = ConstructComponent(sourceEnt, "Auras", { "_string": name }); test_function(name, cmpAuras); } @@ -125,17 +128,17 @@ testAuras("garrison", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasGarrisonAura(), true); - cmpAuras.ApplyGarrisonBonus(targetEnt); + cmpAuras.ApplyGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); - cmpAuras.RemoveGarrisonBonus(targetEnt); + cmpAuras.RemoveGarrisonAura(targetEnt); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); testAuras("formation", (name, cmpAuras) => { TS_ASSERT_EQUALS(cmpAuras.HasFormationAura(), true); - cmpAuras.ApplyFormationBonus([targetEnt]); + cmpAuras.ApplyFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 15); - cmpAuras.RemoveFormationBonus([targetEnt]); + cmpAuras.RemoveFormationAura([targetEnt]); TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Component/Value", 5, targetEnt), 5); }); Index: binaries/data/mods/public/simulation/components/tests/test_Capturable.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Capturable.js +++ binaries/data/mods/public/simulation/components/tests/test_Capturable.js @@ -1,11 +1,10 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/TerritoryDecay.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Capturable.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Damage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Damage.js +++ binaries/data/mods/public/simulation/components/tests/test_Damage.js @@ -5,7 +5,6 @@ Engine.LoadHelperScript("ValueModification.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/AttackDetection.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Damage.js"); Engine.LoadComponentScript("interfaces/DamageReceiver.js"); Engine.LoadComponentScript("interfaces/Health.js"); @@ -13,7 +12,7 @@ Engine.LoadComponentScript("interfaces/Player.js"); Engine.LoadComponentScript("interfaces/Promotion.js"); Engine.LoadComponentScript("interfaces/StatusEffectsReceiver.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("Attack.js"); Engine.LoadComponentScript("Damage.js"); Index: binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js +++ binaries/data/mods/public/simulation/components/tests/test_DeathDamage.js @@ -1,7 +1,6 @@ Engine.LoadHelperScript("DamageBonus.js"); Engine.LoadHelperScript("DamageTypes.js"); Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Damage.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); Index: binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js +++ binaries/data/mods/public/simulation/components/tests/test_GarrisonHolder.js @@ -3,11 +3,10 @@ Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("GarrisonHolder.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Auras.js"); Engine.LoadComponentScript("interfaces/Health.js"); Engine.LoadComponentScript("interfaces/ProductionQueue.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/UnitAI.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Health.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Health.js +++ binaries/data/mods/public/simulation/components/tests/test_Health.js @@ -1,5 +1,4 @@ -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); Index: binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_ModificationsManager.js @@ -0,0 +1,141 @@ +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); +Engine.LoadComponentScript("ModificationsManager.js"); +Engine.LoadHelperScript("MultiKeyMap.js"); +Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("ValueModification.js"); + +let cmpModificationsManager = ConstructComponent(SYSTEM_ENTITY, "ModificationsManager", {}); +cmpModificationsManager.Init(); + +// These should be different as that is the general case. +const PLAYER_ID_FOR_TEST = 2; +const PLAYER_ENTITY_ID = 3; + +AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "GetEntitiesByPlayer": function(a) { return []; } +}); + +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": (a) => PLAYER_ENTITY_ID +}); + +AddMock(PLAYER_ENTITY_ID, IID_Player, { + "GetPlayerID": () => PLAYER_ID_FOR_TEST +}); + +let entitiesToTest = [5, 6, 7, 8]; +for (let ent of entitiesToTest) + AddMock(ent, IID_Ownership, { + "GetOwner": () => PLAYER_ID_FOR_TEST + }); + +AddMock(5, IID_Identity, { + "GetClassesList": function() { return "Structure";} +}); +AddMock(6, IID_Identity, { + "GetClassesList": function() { return "Infantry";} +}); +AddMock(7, IID_Identity, { + "GetClassesList": function() { return "Unit";} +}); +AddMock(8, IID_Identity, { + "GetClassesList": function() { return "Structure Unit";} +}); + +// Sprinkle random serialisation cycles. +function SerializationCycle() +{ + let data = cmpModificationsManager.Serialize(); + cmpModificationsManager = ConstructComponent(SYSTEM_ENTITY, "ModificationsManager", {}); + cmpModificationsManager.Deserialize(data); +} + +cmpModificationsManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST, from: -1, to: PLAYER_ENTITY_ID }); + +cmpModificationsManager.AddModif("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, 10, "testLol"); + +cmpModificationsManager.AddModif("Test_A", "Test_A_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); +cmpModificationsManager.AddModif("Test_A", "Test_A_1", { "affects": ["Infantry"], "add": 5 }, PLAYER_ENTITY_ID); +cmpModificationsManager.AddModif("Test_A", "Test_A_2", { "affects": ["Unit"], "add": 3 }, PLAYER_ENTITY_ID); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10); +SerializationCycle(); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 6), 10); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 7), 8); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 8), 18); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 5); + +cmpModificationsManager.RemoveAllModifs("Test_A_0", PLAYER_ENTITY_ID); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 5); + +cmpModificationsManager.AddModifs("Test_A_0", { + "Test_A": { "affects": ["Structure"], "add": 10 }, + "Test_B": { "affects": ["Structure"], "add": 8 }, +}, PLAYER_ENTITY_ID); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 15); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_B", 5, 8), 13); + +// Add two local modifications, only the first should stick. +cmpModificationsManager.AddModif("Test_C", "Test_C_0", { "affects": ["Structure"], "add": 10 }, 5); +cmpModificationsManager.AddModif("Test_C", "Test_C_1", { "affects": ["Unit"], "add": 5 }, 5); + +SerializationCycle(); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 15); + +// test that local modifications are indeed applied after global managers +cmpModificationsManager.AddModif("Test_C", "Test_C_2", { "affects": ["Structure"], "replace": 0 }, 5); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); + +TS_ASSERT(!cmpModificationsManager.HasAnyModif("Test_C_3", PLAYER_ENTITY_ID)); + +SerializationCycle(); + +// check that things still work properly if we change global modifications +cmpModificationsManager.AddModif("Test_C", "Test_C_3", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 0); + +TS_ASSERT(cmpModificationsManager.HasAnyModif("Test_C_3", PLAYER_ENTITY_ID)); +TS_ASSERT(cmpModificationsManager.HasModif("Test_C", "Test_C_3", PLAYER_ENTITY_ID)); +TS_ASSERT(cmpModificationsManager.HasModif("Test_C", "Test_C_2", 5)); + +// test removal +cmpModificationsManager.RemoveModif("Test_C", "Test_C_2", 5); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, 5), 25); + +SerializationCycle(); + +TS_ASSERT(cmpModificationsManager.HasModif("Test_C", "Test_C_3", PLAYER_ENTITY_ID)); +TS_ASSERT(!cmpModificationsManager.HasModif("Test_C", "Test_C_2", 5)); + +////////////////////////////////////////// +// Test that entities keep local modifications but not global ones when changing owner. +AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + "GetPlayerByID": (a) => a == PLAYER_ID_FOR_TEST ? PLAYER_ENTITY_ID : PLAYER_ENTITY_ID + 1 +}); + +AddMock(PLAYER_ENTITY_ID + 1, IID_Player, { + "GetPlayerID": () => PLAYER_ID_FOR_TEST + 1 +}); + +cmpModificationsManager = ConstructComponent(SYSTEM_ENTITY, "ModificationsManager", {}); +cmpModificationsManager.Init(); + +cmpModificationsManager.AddModif("Test_D", "Test_D_0", { "affects": ["Structure"], "add": 10 }, PLAYER_ENTITY_ID); +cmpModificationsManager.AddModif("Test_D", "Test_D_1", { "affects": ["Structure"], "add": 1 }, PLAYER_ENTITY_ID + 1); +cmpModificationsManager.AddModif("Test_D", "Test_D_2", { "affects": ["Structure"], "add": 5 }, 5); + +cmpModificationsManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST, from: -1, to: PLAYER_ENTITY_ID }); +cmpModificationsManager.OnGlobalPlayerEntityChanged({ player: PLAYER_ID_FOR_TEST + 1, from: -1, to: PLAYER_ENTITY_ID + 1 }); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 25); +cmpModificationsManager.OnGlobalOwnershipChanged({ entity: 5, from: PLAYER_ID_FOR_TEST, to: PLAYER_ID_FOR_TEST + 1 }); +AddMock(5, IID_Ownership, { + "GetOwner": () => PLAYER_ID_FOR_TEST + 1 +}); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 16); Index: binaries/data/mods/public/simulation/components/tests/test_Pack.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Pack.js +++ binaries/data/mods/public/simulation/components/tests/test_Pack.js @@ -2,8 +2,7 @@ Engine.LoadHelperScript("Sound.js"); Engine.LoadHelperScript("Transform.js"); Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/Guard.js"); Index: binaries/data/mods/public/simulation/components/tests/test_Player.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Player.js +++ binaries/data/mods/public/simulation/components/tests/test_Player.js @@ -15,9 +15,8 @@ }; Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Player.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("Player.js"); var cmpPlayer = ConstructComponent(10, "Player", { Index: binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/tests/test_Technologies_effects.js @@ -0,0 +1,30 @@ +// TODO: Move this to a folder of tests for GlobalScripts (once one is created) + +// This tests the GetTechModifiedProperty function. +let add = [{ "add": 10, "affects": "Unit" }]; + +let add_add = [{ "add": 10, "affects": "Unit" }, { "add": 5, "affects": "Unit" }]; + +let add_mul_add = [{ "add": 10, "affects": "Unit" }, { "multiply": 2, "affects": "Unit" }, { "add": 5, "affects": "Unit" }]; + +let add_replace = [{ "add": 10, "affects": "Unit" }, { "replace": 10, "affects": "Unit" }]; + +let replace_add = [{ "replace": 10, "affects": "Unit" }, { "add": 10, "affects": "Unit" }]; + +let replace_replace = [{ "replace": 10, "affects": "Unit" }, { "replace": 30, "affects": "Unit" }]; + +let replace_nonnum = [{ "replace": "alpha", "affects": "Unit" }]; + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add, "Unit", 5), 15); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Unit", 5), 20); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Other", 5), 5); + +// Technologies work by multiplying then adding all. +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_mul_add, "Unit", 5), 25); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_replace, "Unit", 5), 10); + +// Only the first replace is taken into account +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_replace, "Unit", 5), 10); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Unit", "beta"), "alpha"); Index: binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js +++ binaries/data/mods/public/simulation/components/tests/test_Technologies_reqs.js @@ -55,14 +55,14 @@ TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "athen"), [{ "techs": ["tech_A"], "entities": [{ "class": "class_B", "number": 5, "check": "count" }] }]); // Multiple `civ`s -template.requirements = { "all": [{ "civ": "civ_A"}, { "civ": "civ_B"}, { "civ": "civ_C"}] }; +template.requirements = { "all": [{ "civ": "civ_A" }, { "civ": "civ_B" }, { "civ": "civ_C" }] }; TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_A"), []); TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_B"), []); TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_C"), []); TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_D"), false); // Multiple `notciv`s -template.requirements = { "all": [{ "notciv": "civ_A"}, { "notciv": "civ_B"}, { "notciv": "civ_C"}] }; +template.requirements = { "all": [{ "notciv": "civ_A" }, { "notciv": "civ_B" }, { "notciv": "civ_C" }] }; TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_A"), false); TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_B"), false); TS_ASSERT_UNEVAL_EQUALS(DeriveTechnologyRequirements(template, "civ_C"), false); Index: binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js +++ binaries/data/mods/public/simulation/components/tests/test_UpgradeModification.js @@ -13,8 +13,7 @@ return "" + schema + ""; } }; -Engine.LoadComponentScript("interfaces/AuraManager.js"); // Provides `IID_AuraManager`, tested for in helpers/ValueModification.js. -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); // Provides `IID_TechnologyManager`, used below. +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); // Provides `IID_ModificationsManager`, used below. Engine.LoadComponentScript("interfaces/Timer.js"); // Provides `IID_Timer`, used below. // What we're testing: @@ -89,19 +88,15 @@ "CancelTimer": () => {} // Called in components/Upgrade.js::CancelUpgrade(). }); -// Init Player: -AddMock(10, IID_Player, { - "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). - "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). - "GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime(). - "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). -}); -AddMock(10, IID_TechnologyManager, { - "ApplyModificationsTemplate": (valueName, curValue, template) => { +AddMock(SYSTEM_ENTITY, IID_ModificationsManager, { + "ApplyModificationsTemplate": (valueName, curValue, template, player) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToTemplate() // as part of Tests T2 and T5 below. let mods = isResearched ? templateTechModifications.with : templateTechModifications.without; - return GetTechModifiedProperty(mods, GetIdentityClasses(template.Identity), valueName, curValue); + + if (mods[valueName]) + return GetTechModifiedProperty(mods[valueName], GetIdentityClasses(template.Identity), curValue); + return curValue; }, "ApplyModifications": (valueName, curValue, ent) => { // Called in helpers/ValueModification.js::ApplyValueModificationsToEntity() @@ -111,6 +106,14 @@ } }); +// Init Player: +AddMock(10, IID_Player, { + "AddResources": () => {}, // Called in components/Upgrade.js::CancelUpgrade(). + "GetPlayerID": () => playerID, // Called in helpers/Player.js::QueryOwnerInterface() (and several times below). + "GetTimeMultiplier": () => 1.0, // Called in components/Upgrade.js::GetUpgradeTime(). + "TrySubtractResources": () => true // Called in components/Upgrade.js::Upgrade(). +}); + // Create an entity with an Upgrade component: AddMock(20, IID_Ownership, { "GetOwner": () => playerID // Called in helpers/Player.js::QueryOwnerInterface(). Index: binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js +++ binaries/data/mods/public/simulation/components/tests/test_ValueModificationHelper.js @@ -1,15 +1,15 @@ Engine.LoadHelperScript("Player.js"); Engine.LoadHelperScript("ValueModification.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); Engine.LoadComponentScript("interfaces/Player.js"); -Engine.LoadComponentScript("interfaces/TechnologyManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); let player = 1; let playerEnt = 10; let ownedEnt = 60; let techKey = "Attack/BigAttack"; +let otherKey = "Other/Key"; -AddMock(playerEnt, IID_TechnologyManager, { +AddMock(SYSTEM_ENTITY, IID_ModificationsManager, { "ApplyModifications": (key, val, ent) => { if (key != techKey) return val; @@ -21,18 +21,6 @@ } }); -AddMock(SYSTEM_ENTITY, IID_AuraManager, { - "ApplyModifications": (key, val, ent) => { - if (key != techKey) - return val; - if (ent == playerEnt) - return val * 10; - if (ent == ownedEnt) - return val * 100; - return val; - } -}); - AddMock(SYSTEM_ENTITY, IID_PlayerManager, { "GetPlayerByID": () => 10 }); @@ -45,6 +33,8 @@ "GetOwner": () => 1 }); -TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, playerEnt), 50.0); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(otherKey, 2.0, playerEnt), 2.0); -TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 900.0); +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, playerEnt), 5.0); + +TS_ASSERT_EQUALS(ApplyValueModificationsToEntity(techKey, 2.0, ownedEnt), 9.0); Index: binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js +++ binaries/data/mods/public/simulation/components/tests/test_VisionSharing.js @@ -3,7 +3,7 @@ Engine.LoadHelperScript("Commands.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js"); Engine.LoadComponentScript("interfaces/TechnologyManager.js"); -Engine.LoadComponentScript("interfaces/AuraManager.js"); +Engine.LoadComponentScript("interfaces/ModificationsManager.js"); Engine.LoadComponentScript("interfaces/Timer.js"); Engine.LoadComponentScript("interfaces/VisionSharing.js"); Engine.LoadComponentScript("interfaces/StatisticsTracker.js"); @@ -118,6 +118,9 @@ AddMock(14, IID_TechnologyManager, { "CanProduce": entity => false, +}); + +AddMock(14, IID_ModificationsManager, { "ApplyModificationsTemplate": (valueName, curValue, template) => curValue }); @@ -127,8 +130,12 @@ AddMock(14, IID_TechnologyManager, { "CanProduce": entity => entity == "special/spy", +}); + +AddMock(14, IID_ModificationsManager, { "ApplyModificationsTemplate": (valueName, curValue, template) => curValue }); + AddMock(14, IID_Player, { "GetSpyCostMultiplier": () => 1, "TrySubtractResources": costs => false Index: binaries/data/mods/public/simulation/helpers/MultiKeyMap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/MultiKeyMap.js @@ -0,0 +1,241 @@ +// Convenient container abstraction for storing items referenced by a 3-tuple. +// Used by the itemsManager to store items by (property Name, entity, item ID). +// Methods starting with an underscore are private to the storage. +// This supports stackable items as it stores count for each 3-tuple. +function MultiKeyMap() +{ + this.items = new Map(); + // Keys are referred to as 'primaryKey', 'secondaryKey', 'itemID'. +} + +MultiKeyMap.prototype.Serialize = function() +{ + let ret = []; + for (let primary of this.items.keys()) + { + // Keys of a Map can be arbitrary types whereas objects only support string, so use a list. + let vals = [primary, []]; + ret.push(vals); + for (let secondary of this.items.get(primary).keys()) + vals[1].push([secondary, this.items.get(primary).get(secondary)]); + } + return ret; +}; + +MultiKeyMap.prototype.Deserialize = function(data) +{ + for (let primary in data) + { + this.items.set(data[primary][0], new Map()); + for (let secondary in data[primary][1]) + this.items.get(data[primary][0]).set(data[primary][1][secondary][0], data[primary][1][secondary][1]); + } +}; + +/** + * Add a single item. + * NB: if you add an item with a different value but the same itemID, the original value remains. + * @param itemID - internal ID of this item, for later removal and/or updating + * @param stackable - if stackable, changing the count of items invalides, otherwise not. + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.AddItem = function(primaryKey, itemID, item, secondaryKey, stackable = false) +{ + if (!this._AddItem(primaryKey, itemID, item, secondaryKey, stackable)) + return false; + + this._OnItemModified(primaryKey, secondaryKey, itemID); + return true; +}; + +/** + * Add items to multiple properties at once (only one item per property) + * @param items - Dictionnary of { primaryKey: item } + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.AddItems = function(itemID, items, secondaryKey, stackable = false) +{ + let modified = false; + for (let primaryKey in items) + modified = this.AddItem(primaryKey, itemID, items[primaryKey], secondaryKey, stackable) || modified; + return modified; +}; + +/** + * Removes a item on a property. + * @param primaryKey - property to change (e.g. "Health/Max") + * @param itemID - internal ID of the item to remove + * @param secondaryKey - secondaryKey ID + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.RemoveItem = function(primaryKey, itemID, secondaryKey, stackable = false) +{ + if (!this._RemoveItem(primaryKey, itemID, secondaryKey, stackable)) + return false; + + this._OnItemModified(primaryKey, secondaryKey, itemID); + return true; +}; + +/** + * Removes items with this ID for any property name. + * Naively iterates all property names. + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype.RemoveAllItems = function(itemID, secondaryKey, stackable = false) +{ + let modified = false; + // Map doesn't implement some so use a for-loop here. + for (let primaryKey of this.items.keys()) + modified = this.RemoveItem(primaryKey, itemID, secondaryKey, stackable) || modified; + return modified; +}; + +/** + * @param itemID - internal ID of the item to try and find. + * @returns true if there is at least one item with that itemID + */ +MultiKeyMap.prototype.HasItem = function(primaryKey, itemID, secondaryKey) +{ + // some() returns false for an empty list which is wanted here. + return this._getItems(primaryKey, secondaryKey).some(item => item.ID === itemID); +}; + +/** + * Check if we have a item for any property name. + * Naively iterates all property names. + * @returns true if there is at least one item with that itemID + */ +MultiKeyMap.prototype.HasAnyItem = function(itemID, secondaryKey) +{ + // Map doesn't implement some so use for loops instead. + for (let primaryKey of this.items.keys()) + if (this.HasItem(primaryKey, itemID, secondaryKey)) + return true; + return false; +}; + +/** + * @returns A list of items with storage metadata removed. + */ +MultiKeyMap.prototype.GetItems = function(primaryKey, secondaryKey, stackable = false) +{ + let items = []; + + if (stackable) + this._getItems(primaryKey, secondaryKey).forEach(item => (items = items.concat(Array(item.count).fill(item.value)))); + else + this._getItems(primaryKey, secondaryKey).forEach(item => items.push(item.value)); + + return items; +}; + +/** + * @returns A dictionary of { Property Name: items } for the secondary Key. + * Naively iterates all property names. + */ +MultiKeyMap.prototype.GetAllItems = function(secondaryKey, stackable = false) +{ + let items = {}; + + // Map doesn't implement filter so use a for loop. + for (let primaryKey of this.items.keys()) + { + if (!this.items.get(primaryKey).has(secondaryKey)) + continue; + items[primaryKey] = this.GetItems(primaryKey, secondaryKey, stackable); + } + return items; +}; + +/** + * @returns a list of items. + * This does not necessarily return a reference to items' list, use _getItemsOrInit for that. + */ +MultiKeyMap.prototype._getItems = function(primaryKey, secondaryKey) +{ + if (!this._exists(primaryKey, secondaryKey)) + return []; + return this.items.get(primaryKey).get(secondaryKey); +}; + +/** + * @returns a reference to the list of items for that property name and secondaryKey. + */ +MultiKeyMap.prototype._getItemsOrInit = function(primaryKey, secondaryKey) +{ + if (!this._exists(primaryKey, secondaryKey)) + this._initItemsIfNeeded(primaryKey, secondaryKey); + return this.items.get(primaryKey).get(secondaryKey); +}; + +MultiKeyMap.prototype._exists = function(primaryKey, secondaryKey) +{ + if (!this.items.has(primaryKey)) + return false; + if (!this.items.get(primaryKey).has(secondaryKey)) + return false; + return true; +}; + +MultiKeyMap.prototype._initItemsIfNeeded = function(primaryKey, secondaryKey) +{ + if (!this.items.get(primaryKey)) + this.items.set(primaryKey, new Map()); + if (!this.items.get(primaryKey).get(secondaryKey)) + this.items.get(primaryKey).set(secondaryKey, []); +}; + +/** + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype._AddItem = function(primaryKey, itemID, item, secondaryKey, stackable) +{ + let items = this._getItemsOrInit(primaryKey, secondaryKey); + for (let it of items) + if (it.ID == itemID) + { + it.count++; + return stackable; + } + + items.push({ "ID": itemID, "count": 1, "value": item }); + return true; +}; + +/** + * @returns true if the items list changed in such a way that cached values are possibly invalidated. + */ +MultiKeyMap.prototype._RemoveItem = function(primaryKey, itemID, secondaryKey, stackable) +{ + let items = this._getItems(primaryKey, secondaryKey); + + let existingItem = items.filter(item => { return item.ID == itemID; }); + if (!existingItem.length) + return false; + + if (--existingItem[0].count > 0) + return stackable; + + let stilValidItems = items.filter(item => item.count > 0); + + // Delete entries from the map if necessary to clean up. + if (!stilValidItems.length) + { + this.items.get(primaryKey).delete(secondaryKey); + if (!this.items.get(primaryKey).size) + this.items.delete(primaryKey); + return true; + } + + this.items.get(primaryKey).set(secondaryKey, stilValidItems); + + return true; +}; + +/** + * Stub method, to overload. + */ +MultiKeyMap.prototype._OnItemModified = function(primaryKey, secondaryKey, itemID) {}; + +Engine.RegisterGlobal("MultiKeyMap", MultiKeyMap); Index: binaries/data/mods/public/simulation/helpers/Player.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Player.js +++ binaries/data/mods/public/simulation/helpers/Player.js @@ -184,6 +184,31 @@ return path; } +/** + * @param id An entity's ID + * @returns The entity ID of the owner player (not his player ID) or ent if ent is a player entity. + */ +function QueryOwnerEntityID(ent) +{ + let cmpPlayer = Engine.QueryInterface(ent, IID_Player); + if (cmpPlayer) + return ent; + + let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); + if (!cmpOwnership) + return null; + + let owner = cmpOwnership.GetOwner(); + if (owner == INVALID_PLAYER) + return null; + + let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); + if (!cmpPlayerManager) + return null; + + return cmpPlayerManager.GetPlayerByID(owner); +} + /** * Similar to Engine.QueryInterface but applies to the player entity * that owns the given entity. @@ -326,6 +351,7 @@ } Engine.RegisterGlobal("LoadPlayerSettings", LoadPlayerSettings); +Engine.RegisterGlobal("QueryOwnerEntityID", QueryOwnerEntityID); Engine.RegisterGlobal("QueryOwnerInterface", QueryOwnerInterface); Engine.RegisterGlobal("QueryPlayerIDInterface", QueryPlayerIDInterface); Engine.RegisterGlobal("QueryMiragedInterface", QueryMiragedInterface); Index: binaries/data/mods/public/simulation/helpers/ValueModification.js =================================================================== --- binaries/data/mods/public/simulation/helpers/ValueModification.js +++ binaries/data/mods/public/simulation/helpers/ValueModification.js @@ -3,29 +3,21 @@ function ApplyValueModificationsToEntity(tech_type, current_value, entity) { let value = current_value; + // entity can be an owned entity or a player entity. - let cmpTechnologyManager = Engine.QueryInterface(entity, IID_Player) ? - Engine.QueryInterface(entity, IID_TechnologyManager) : QueryOwnerInterface(entity, IID_TechnologyManager); - if (cmpTechnologyManager) - value = cmpTechnologyManager.ApplyModifications(tech_type, current_value, entity); - - let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - if (!cmpAuraManager) - return value; - return cmpAuraManager.ApplyModifications(tech_type, value, entity); + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + if (cmpModificationsManager) + value = cmpModificationsManager.ApplyModifications(tech_type, current_value, entity); + return value; } function ApplyValueModificationsToTemplate(tech_type, current_value, playerID, template) { let value = current_value; - let cmpTechnologyManager = QueryPlayerIDInterface(playerID, IID_TechnologyManager); - if (cmpTechnologyManager) - value = cmpTechnologyManager.ApplyModificationsTemplate(tech_type, current_value, template); - - let cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - if (!cmpAuraManager) - return value; - return cmpAuraManager.ApplyTemplateModifications(tech_type, value, playerID, template); + let cmpModificationsManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ModificationsManager); + if (cmpModificationsManager) + value = cmpModificationsManager.ApplyModificationsTemplate(tech_type, current_value, template, playerID); + return value; } Engine.RegisterGlobal("ApplyValueModificationsToEntity", ApplyValueModificationsToEntity); Index: binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/helpers/tests/test_MultiKeyMap.js @@ -0,0 +1,132 @@ +Engine.LoadHelperScript("MultiKeyMap.js"); + +function setup_keys(map) +{ + map.AddItem("prim_a", "item_a", 0, "sec_a"); + map.AddItem("prim_a", "item_b", 0, "sec_a"); + map.AddItem("prim_a", "item_c", 0, "sec_a"); + map.AddItem("prim_a", "item_a", 0, "sec_b"); + map.AddItem("prim_b", "item_a", 0, "sec_a"); + map.AddItem("prim_c", "item_a", 0, "sec_a"); + map.AddItem("prim_c", "item_a", 0, 5); +} + +// Check that key-related operations are correct. +function test_keys(map) +{ + TS_ASSERT(map.items.has("prim_a")); + TS_ASSERT(map.items.has("prim_b")); + TS_ASSERT(map.items.has("prim_c")); + + TS_ASSERT(map.items.get("prim_a").has("sec_a")); + TS_ASSERT(map.items.get("prim_a").has("sec_b")); + TS_ASSERT(!map.items.get("prim_a").has("sec_c")); + TS_ASSERT(map.items.get("prim_b").has("sec_a")); + TS_ASSERT(map.items.get("prim_c").has("sec_a")); + TS_ASSERT(map.items.get("prim_c").has(5)); + + TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3); + TS_ASSERT(map.items.get("prim_a").get("sec_b").length == 1); + TS_ASSERT(map.items.get("prim_b").get("sec_a").length == 1); + TS_ASSERT(map.items.get("prim_c").get("sec_a").length == 1); + TS_ASSERT(map.items.get("prim_c").get(5).length == 1); + + TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3); + TS_ASSERT(map.GetItems("prim_a", "sec_b").length == 1); + TS_ASSERT(map.GetItems("prim_b", "sec_a").length == 1); + TS_ASSERT(map.GetItems("prim_c", "sec_a").length == 1); + TS_ASSERT(map.GetItems("prim_c", 5).length == 1); + + TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_c", "sec_a")); + TS_ASSERT(!map.HasItem("prim_a", "item_d", "sec_a")); + TS_ASSERT(map.HasItem("prim_a", "item_a", "sec_b")); + TS_ASSERT(!map.HasItem("prim_a", "item_b", "sec_b")); + TS_ASSERT(!map.HasItem("prim_a", "item_c", "sec_b")); + TS_ASSERT(map.HasItem("prim_b", "item_a", "sec_a")); + TS_ASSERT(map.HasItem("prim_c", "item_a", "sec_a")); + TS_ASSERT(map.HasAnyItem("item_a", "sec_b")); + TS_ASSERT(map.HasAnyItem("item_b", "sec_a")); + TS_ASSERT(!map.HasAnyItem("item_d", "sec_a")); + TS_ASSERT(!map.HasAnyItem("item_b", "sec_b")); + + // Adding the same item increases its count. + map.AddItem("prim_a", "item_b", 0, "sec_a"); + TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 3); + TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item.ID == "item_b")[0].count == 2); + TS_ASSERT(map.GetItems("prim_a", "sec_a").length == 3); + TS_ASSERT(map.GetItems("prim_a", "sec_a", true).length == 4); + + // Adding without stackable doesn't invalidate caches, adding with does. + TS_ASSERT(!map.AddItem("prim_a", "item_b", 0, "sec_a")); + TS_ASSERT(map.AddItem("prim_a", "item_b", 0, "sec_a", true)); + + TS_ASSERT(map.items.get("prim_a").get("sec_a").filter(item => item.ID == "item_b")[0].count == 4); + + // Likewise removing, unless we now reach 0 + TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a", true)); + TS_ASSERT(!map.RemoveItem("prim_a", "item_b", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_b", "sec_a")); + + // Check that cleanup is done + TS_ASSERT(map.items.get("prim_a").get("sec_a").length == 2); + TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_a")); + TS_ASSERT(map.RemoveItem("prim_a", "item_c", "sec_a")); + TS_ASSERT(!map.items.get("prim_a").has("sec_a")); + TS_ASSERT(map.items.get("prim_a").has("sec_b")); + TS_ASSERT(map.RemoveItem("prim_a", "item_a", "sec_b")); + TS_ASSERT(!map.items.has("prim_a")); +} + +function setup_items(map) +{ + map.AddItem("prim_a", "item_a", 1, "sec_a"); + map.AddItem("prim_a", "item_b", 2, "sec_a"); + map.AddItem("prim_a", "item_c", 3, "sec_a"); + map.AddItem("prim_a", "item_c", 1000, "sec_a"); + map.AddItem("prim_a", "item_a", 5, "sec_b"); + map.AddItem("prim_b", "item_a", 6, "sec_a"); + map.AddItem("prim_c", "item_a", 7, "sec_a"); +} + +// Check that items returned are correct. +function test_items(map) +{ + let items = map.GetAllItems("sec_a"); + TS_ASSERT("prim_a" in items); + TS_ASSERT("prim_b" in items); + TS_ASSERT("prim_c" in items); + let sum = 0; + for (let key in items) + items[key].forEach(item => (sum += item)); + TS_ASSERT(sum == 19); + + items = map.GetAllItems("sec_a", true); + sum = 0; + for (let key in items) + items[key].forEach(item => (sum += item)); + // We're adding more of the first item_c, the value wasn't replaced. + TS_ASSERT(sum == 22); +} + +// Test items, and test that deserialised versions still pass test (i.e. test serialisation). +let map = new MultiKeyMap(); +setup_keys(map); +test_keys(map); + +map = new MultiKeyMap(); +let map2 = new MultiKeyMap(); +setup_keys(map); +map2.Deserialize(map.Serialize()); +test_keys(map2); + +map = new MultiKeyMap(); +setup_items(map); +test_items(map); +map = new MultiKeyMap(); +map2 = new MultiKeyMap(); +setup_items(map); +map2.Deserialize(map.Serialize()); +test_items(map2); Index: source/simulation2/components/tests/test_scripts.h =================================================================== --- source/simulation2/components/tests/test_scripts.h +++ source/simulation2/components/tests/test_scripts.h @@ -66,6 +66,7 @@ VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); + TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) {