0ad/binaries/data/mods/public/simulation/components/ModifiersManager.js
wraitii 7bf933d5f6 Fix VisualActor tech changes for mirages
Correctly recompute the actor when something changes that could modify
it (ownership change, ...).
Make sure mirages are updated when they reappear after being hidden.
Make sure foundations have proper identity classes.
Make sure mirages don't respond to value modifications in the visual
component.
Clarify a few comments.

Earlier work by: Sandarac
Fixes #2907

Differential Revision: https://code.wildfiregames.com/D576
This was SVN commit r24279.
2020-11-28 08:57:15 +00:00

292 lines
11 KiB
JavaScript
Executable file

function ModifiersManager() {}
ModifiersManager.prototype.Schema =
"<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.
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.
let 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)
{
let playerCache = this.playerEntitiesCached.get(entity);
this.InvalidateCache(propertyName, entity, playerCache);
if (playerCache)
{
let 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
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] });
};
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)
{
let 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)
{
let modifs = this.modifiersStorage.GetItems(propertyName, target);
if (!modifs.length)
return originalValue;
// Flatten the list of modifications
let modifications = [];
modifs.forEach(item => { modifications = modifications.concat(item.value); });
return GetTechModifiedProperty(modifications, classesList, originalValue);
};
/**
* @returns originalValue after modifiers
*/
ModifiersManager.prototype.Cache = function(classesList, propertyName, originalValue, 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);
let value = this.FetchModifiedProperty(classesList, propertyName, originalValue, entity);
cache2.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 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)
{
newValue = newValue.get(entity);
if (newValue)
{
newValue = newValue.get(originalValue);
if (newValue)
return newValue;
}
}
// 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).
let ownerEntity = QueryOwnerEntityID(entity);
if (ownerEntity == entity)
ownerEntity = null;
newValue = originalValue;
let cmpIdentity = QueryMiragedInterface(entity, IID_Identity);
if (!cmpIdentity)
return originalValue;
let classesList = cmpIdentity.GetClassesList();
// Apply player-wide modifiers before entity-local modifiers.
if (ownerEntity)
{
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.Cache(classesList, propertyName, 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;
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.
*/
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.from == INVALID_PLAYER || 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 modifiers will be added by the relevant components, so no need to check for them here.
let modifiedComponents = {};
let playerModifs = this.modifiersStorage.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 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] = [];
modifiedComponents[component].push(propertyName);
}));
}
for (let component in modifiedComponents)
Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
};
/**
* The following functions simply proxy MultiKeyMap's interface.
*/
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);