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);