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() @@ -243,8 +243,10 @@ if (this.IsGlobalAura(name)) { this.ApplyTemplateBonus(name, affectedPlayers); - for (let player of affectedPlayers) - this.ApplyBonus(name, cmpRangeManager.GetEntitiesByPlayer(player)); + // This only applies icons. + if (this.GetOverlayIcon(name)) + for (let player of affectedPlayers) + this.ApplyBonus(name, cmpRangeManager.GetEntitiesByPlayer(player)); continue; } @@ -263,15 +265,15 @@ 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); } @@ -315,20 +317,6 @@ } }; -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) { for (let name of this.GetAuraNames().filter(n => this.IsFormationAura(n))) @@ -348,15 +336,22 @@ 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) @@ -378,16 +373,21 @@ if (!this.IsGlobalAura(name)) return; - var cmpAuraManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AuraManager); - cmpAuraManager.UnregisterGlobalAuraSource(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); - var modifications = this.GetModifications(name); - var classes = this.GetClasses(name); - var players = this.GetAffectedPlayers(name); - - 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) @@ -401,21 +401,33 @@ 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 (this.IsGlobalAura(name)) 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) @@ -429,22 +441,32 @@ 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)) - return; - - for (let ent of validEnts) - { - var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); - if (cmpStatusBars) - cmpStatusBars.RemoveAuraSource(this.entity, 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 (this.IsGlobalAura(name)) + 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 ents) + for (let modifierPath in derivedModifiers) + for (let modifier of derivedModifiers[modifierPath]) + cmpModificationsManager.RemoveModif(modifierPath, modifName, ent); }; Auras.prototype.OnOwnershipChanged = function(msg) @@ -484,7 +506,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/ModificationsManager.js =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/components/ModificationsManager.js @@ -0,0 +1,287 @@ +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. + + // Proxy with the correct 'this'. + let proxy = (func) => ((self) => (...args) => func.apply(self, args))(this); + this.modifsStorage._OnItemModified = proxy(this.ModificationsChanged); + + // Proxy the public methods of the modifications storage, unless we have already defined such methods. + // (more convenient than writing several wrapper functions). + for (let propName in this.modifsStorage) + { + if (typeof this.modifsStorage[propName] !== "function" || propName[0] === '_') + continue; + if (propName in this) + continue; + this[propName.replace('Item', 'Modif')] = ((pn) => (...args) => this.modifsStorage[pn].apply(this.modifsStorage, args))(propName); + } +}; + +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) +{ + // GetTechModifiedProperty expects a dictionary (TODO: this seems weird and should be changed). + let modifications = { [propertyName]: this.modifsStorage.GetItems(propertyName, target) }; + + // Small optimisation. + if (!modifications[propertyName].length) + return originalValue; + + return GetTechModifiedProperty(modifications, classesList, propertyName, 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].filter(modif => DoesModificationApply(modif, classes)).forEach(modif => { + 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] }); +}; + +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); } 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,45 @@ +// TODO: Move this to a folder of tests for GlobalScripts (once one is created) + +// This tests the GetTechModifiedProperty function. + +let add = { + "Test_A": [{ "add": 10, "affects": "Unit" }] +}; + +let add_add = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "add": 5, "affects": "Unit" }] +}; + +let add_mul_add = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "multiply": 2, "affects": "Unit" }, { "add": 5, "affects": "Unit" }] +}; + +let add_replace = { + "Test_A": [{ "add": 10, "affects": "Unit" }, { "replace": 10, "affects": "Unit" }] +}; + +let replace_add = { + "Test_A": [{ "replace": 10, "affects": "Unit" }, { "add": 10, "affects": "Unit" }] +}; + +let replace_replace = { + "Test_A": [{ "replace": 10, "affects": "Unit" }, { "replace": 30, "affects": "Unit" }] +}; + +let replace_nonnum = { + "Test_A": [{ "replace": "alpha", "affects": "Unit" }] +}; + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add, "Unit", "Test_A", 5), 15); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Unit", "Test_A", 5), 20); +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_add, "Other", "Test_A", 5), 5); + +// Technologies work by multiplying then adding all. +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_mul_add, "Unit", "Test_A", 5), 25); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(add_replace, "Unit", "Test_A", 5), 10); + +// Only the first replace is taken into account +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_replace, "Unit", "Test_A", 5), 10); + +TS_ASSERT_EQUALS(GetTechModifiedProperty(replace_nonnum, "Unit", "Test_A", "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,15 +88,8 @@ "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; @@ -111,6 +103,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 =================================================================== --- binaries/data/mods/public/simulation/helpers/MultiKeyMap.js +++ binaries/data/mods/public/simulation/helpers/MultiKeyMap.js @@ -193,12 +193,12 @@ { let items = this._getItemsOrInit(primaryKey, secondaryKey); - let existingItems = items.filter(item => { return item.ID == itemID; }); - if (existingItems.length) - { - existingItems[0].count++; - return stackable; - } + for (let it of items) + if (it.ID == itemID) + { + it.count++; + return stackable; + } items.push({ "ID": itemID, "count": 1, "value": item }); return true; 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);