mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
348 lines
12 KiB
JavaScript
348 lines
12 KiB
JavaScript
function ModifiersManager() {}
|
|
|
|
ModifiersManager.prototype.Schema =
|
|
"<a:component type='system'/><empty/>";
|
|
|
|
ModifiersManager.prototype.Init = function()
|
|
{
|
|
// TODO:
|
|
// - add a way to show an icon for a given modifier 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 modifiers, 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.
|
|
|
|
// Contains the modifier infos for each entity, since piecing them together from scratch it is a bit slow
|
|
// and they're very frequently retrieved.
|
|
this.cachedInfos = new Map();
|
|
|
|
this.modifiersStorage = new MultiKeyMap(); // Keyed by property name, entity.
|
|
|
|
this.modifiersStorage._OnItemModified = (prim, sec, itemID) => this.ModifiersChanged.apply(this, [prim, sec, itemID]);
|
|
};
|
|
|
|
ModifiersManager.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.
|
|
const players = [];
|
|
this.playerEntitiesCached.forEach((_, player) => players.push(player));
|
|
return {
|
|
"modifiersStorage": this.modifiersStorage.Serialize(),
|
|
"players": players
|
|
};
|
|
};
|
|
|
|
ModifiersManager.prototype.Deserialize = function(data)
|
|
{
|
|
this.Init();
|
|
this.modifiersStorage.Deserialize(data.modifiersStorage);
|
|
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.
|
|
*/
|
|
ModifiersManager.prototype.ModifiersChanged = function(propertyName, entity)
|
|
{
|
|
const playerCache = this.playerEntitiesCached.get(entity);
|
|
this.InvalidateCache(propertyName, entity, playerCache);
|
|
|
|
if (playerCache)
|
|
{
|
|
const cmpPlayer = Engine.QueryInterface(entity, IID_Player);
|
|
if (cmpPlayer)
|
|
this.SendPlayerModifierMessages(propertyName, cmpPlayer.GetPlayerID());
|
|
}
|
|
else
|
|
Engine.PostMessage(entity, MT_ValueModification, { "entities": [entity], "component": propertyName.split("/")[0], "valueNames": [propertyName] });
|
|
};
|
|
|
|
ModifiersManager.prototype.SendPlayerModifierMessages = 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
|
|
const cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
|
|
const ents = cmpRangeManager.GetEntitiesByPlayer(player);
|
|
Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": propertyName.split("/")[0], "valueNames": [propertyName] });
|
|
};
|
|
|
|
ModifiersManager.prototype.InvalidatePlayerEntCache = function(valueCache, propertyName, entsMap)
|
|
{
|
|
entsMap = entsMap.get(propertyName);
|
|
if (entsMap)
|
|
{
|
|
// Invalidate all local caches directly (for simplicity in ApplyModifiers).
|
|
entsMap.forEach(ent => valueCache.set(ent, new Map()));
|
|
entsMap.clear();
|
|
}
|
|
};
|
|
|
|
ModifiersManager.prototype.InvalidateCache = function(propertyName, entity, playerCache)
|
|
{
|
|
this.cachedInfos.delete(entity);
|
|
const valueCache = this.cachedValues.get(propertyName);
|
|
if (!valueCache)
|
|
return;
|
|
|
|
if (playerCache)
|
|
this.InvalidatePlayerEntCache(valueCache, propertyName, playerCache);
|
|
valueCache.set(entity, new Map());
|
|
};
|
|
|
|
/**
|
|
* @returns originalValue after modifiers.
|
|
*/
|
|
ModifiersManager.prototype.FetchModifiedProperty = function(classesList, propertyName, originalValue, target)
|
|
{
|
|
const modifs = this.modifiersStorage.GetItems(propertyName, target);
|
|
if (!modifs.length)
|
|
return originalValue;
|
|
// Flatten the list of modifications
|
|
const modifications = [];
|
|
modifs.forEach(item => { modifications.push(item.value); });
|
|
return GetTechModifiedProperty(modifications.flat(), classesList, originalValue);
|
|
};
|
|
|
|
ModifiersManager.prototype.Cache = function(classesList, propertyName, originalValue, newValue, entity)
|
|
{
|
|
let cache = this.cachedValues.get(propertyName);
|
|
if (!cache)
|
|
cache = this.cachedValues.set(propertyName, new Map()).get(propertyName);
|
|
|
|
let cache2 = cache.get(entity);
|
|
if (!cache2)
|
|
cache2 = cache.set(entity, new Map()).get(entity);
|
|
|
|
cache2.set(originalValue, newValue);
|
|
};
|
|
|
|
/**
|
|
* Get information about an entity's local modifications.
|
|
* This includes a list of modified components and a "modifications ID", which is consistent
|
|
* and uniquely identifies the entire set of modifiers registered specifically to the entity.
|
|
*/
|
|
ModifiersManager.prototype.GetModifiersInfo = function(entity)
|
|
{
|
|
let info = this.cachedInfos.get(entity);
|
|
if (!info)
|
|
{
|
|
const modifications = this.modifiersStorage.GetAllItems(entity);
|
|
const components = new Set();
|
|
let id = "";
|
|
for (const value in modifications)
|
|
{
|
|
id += `${value}:`;
|
|
|
|
const idx = value.indexOf("/");
|
|
const component = idx > 0 ? value.slice(0, idx) : value;
|
|
components.add(component);
|
|
|
|
// Sort the modifiers to ignore insertion order.
|
|
const modifiers = modifications[value].toSorted((a, b) =>
|
|
a._ID > b._ID ? 1 : -1 // the IDs can't be the same
|
|
);
|
|
for (const modifier of modifiers)
|
|
id += `${modifier._ID}(${modifier._count})`;
|
|
|
|
id += ' ';
|
|
}
|
|
info = {
|
|
"modifiedComponents": components,
|
|
"modificationsID": id
|
|
};
|
|
this.cachedInfos.set(entity, info);
|
|
}
|
|
return info;
|
|
};
|
|
|
|
/**
|
|
* Caching system in front of FetchModifiedProperty(), as calling that every time is quite slow.
|
|
* This recomputes lazily.
|
|
* Applies per-player modifiers before per-entity modifiers, so the latter take priority;
|
|
* @param propertyName - Handle of a technology property (eg Attack/Ranged/Pierce) that was changed.
|
|
* @param originalValue - template/raw/before-modifiers 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 modifiers
|
|
*/
|
|
ModifiersManager.prototype.ApplyModifiers = function(propertyName, originalValue, entity)
|
|
{
|
|
let newValue = this.cachedValues.get(propertyName);
|
|
if (newValue !== undefined)
|
|
{
|
|
newValue = newValue.get(entity);
|
|
if (newValue !== undefined)
|
|
{
|
|
newValue = newValue.get(originalValue);
|
|
if (newValue !== undefined)
|
|
return newValue;
|
|
}
|
|
}
|
|
|
|
newValue = originalValue;
|
|
|
|
const cmpIdentity = QueryMiragedInterface(entity, IID_Identity);
|
|
if (!cmpIdentity)
|
|
return originalValue;
|
|
const classesList = cmpIdentity.GetClassesList();
|
|
|
|
// Get the entity ID of the player / owner of the entity, since we use that to store per-player modifiers
|
|
// (this prevents conflicts between player ID and entity ID).
|
|
const ownerPlayer = Engine.QueryInterface(entity, IID_Ownership)?.GetOwner();
|
|
|
|
// Apply player-wide modifiers before entity-local modifiers.
|
|
if (ownerPlayer !== undefined && ownerPlayer !== INVALID_PLAYER)
|
|
{
|
|
const ownerEntity = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetPlayerByID(ownerPlayer);
|
|
let pc = this.playerEntitiesCached.get(ownerEntity).get(propertyName);
|
|
if (!pc)
|
|
pc = this.playerEntitiesCached.get(ownerEntity).set(propertyName, new Set()).get(propertyName);
|
|
pc.add(entity);
|
|
newValue = this.FetchModifiedProperty(classesList, propertyName, newValue, ownerEntity);
|
|
}
|
|
newValue = this.FetchModifiedProperty(classesList, propertyName, newValue, entity);
|
|
this.Cache(classesList, propertyName, originalValue, newValue, entity);
|
|
|
|
return newValue;
|
|
};
|
|
|
|
/**
|
|
* Alternative version of ApplyModifiers, applies to templates instead of entities.
|
|
* Only needs to handle global modifiers.
|
|
*/
|
|
ModifiersManager.prototype.ApplyTemplateModifiers = function(propertyName, originalValue, template, player)
|
|
{
|
|
if (!template || !template.Identity)
|
|
return originalValue;
|
|
|
|
const 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.
|
|
*/
|
|
ModifiersManager.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 modifiers when an entity changes owner.
|
|
* We do not retain the original modifiers for now.
|
|
*/
|
|
ModifiersManager.prototype.OnGlobalOwnershipChanged = function(msg)
|
|
{
|
|
if (msg.to == INVALID_PLAYER)
|
|
return;
|
|
|
|
// Invalidate all caches.
|
|
for (const propName of this.cachedValues.keys())
|
|
this.InvalidateCache(propName, msg.entity);
|
|
|
|
const cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
|
|
if (!cmpIdentity)
|
|
return;
|
|
const classes = cmpIdentity.GetClassesList();
|
|
|
|
const cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
|
|
const oldOwner = cmpPlayerManager.GetPlayerByID(msg.from);
|
|
const newOwner = cmpPlayerManager.GetPlayerByID(msg.to);
|
|
|
|
// Warn entities that our values have changed.
|
|
// Local modifiers will be added by the relevant components, so no need to check for them here.
|
|
const modifiedComponents = {};
|
|
const fetchPlayerModifiedValueNames = (owner) =>
|
|
{
|
|
if (!owner)
|
|
return;
|
|
const playerModifs = this.modifiersStorage.GetAllItems(owner);
|
|
for (const propertyName in playerModifs)
|
|
{
|
|
// We only need to find one one tech per component for a match.
|
|
const component = propertyName.split("/")[0];
|
|
// Only inform if the modifier actually applies to the entity as an optimisation.
|
|
// TODO: would it be better to call FetchModifiedProperty here and compare values?
|
|
playerModifs[propertyName].forEach(item => item.value.forEach(modif =>
|
|
{
|
|
if (!DoesModificationApply(modif, classes))
|
|
return;
|
|
if (!modifiedComponents[component])
|
|
modifiedComponents[component] = new Set();
|
|
modifiedComponents[component].add(propertyName);
|
|
}));
|
|
}
|
|
};
|
|
|
|
// We'll assume these are always different.
|
|
fetchPlayerModifiedValueNames(oldOwner);
|
|
fetchPlayerModifiedValueNames(newOwner);
|
|
|
|
for (const component in modifiedComponents)
|
|
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": Array.from(modifiedComponents[component]) });
|
|
};
|
|
|
|
/**
|
|
* The following functions simply proxy MultiKeyMap's interface.
|
|
*/
|
|
ModifiersManager.prototype.AddModifier = function(propName, ModifID, Modif, entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.AddItem(propName, ModifID, Modif, entity, stackable);
|
|
};
|
|
|
|
ModifiersManager.prototype.AddModifiers = function(ModifID, Modifs, entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.AddItems(ModifID, Modifs, entity, stackable);
|
|
};
|
|
|
|
ModifiersManager.prototype.RemoveModifier = function(propName, ModifID, entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.RemoveItem(propName, ModifID, entity, stackable);
|
|
};
|
|
|
|
ModifiersManager.prototype.RemoveAllModifiers = function(ModifID, entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.RemoveAllItems(ModifID, entity, stackable);
|
|
};
|
|
|
|
ModifiersManager.prototype.HasModifier = function(propName, ModifID, entity)
|
|
{
|
|
return this.modifiersStorage.HasItem(propName, ModifID, entity);
|
|
};
|
|
|
|
ModifiersManager.prototype.HasAnyModifier = function(ModifID, entity)
|
|
{
|
|
return this.modifiersStorage.HasAnyItem(ModifID, entity);
|
|
};
|
|
|
|
ModifiersManager.prototype.GetModifiers = function(propName, entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.GetItems(propName, entity, stackable);
|
|
};
|
|
|
|
ModifiersManager.prototype.GetAllModifiers = function(entity, stackable = false)
|
|
{
|
|
return this.modifiersStorage.GetAllItems(entity, stackable);
|
|
};
|
|
|
|
Engine.RegisterSystemComponentType(IID_ModifiersManager, "ModifiersManager", ModifiersManager);
|