Optimise GetEntityState
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.
2026-02-12 06:52:21 -08:00
/ * *
* Helper class for the GuiInterface for retrieving an information about an entity or a template .
* It assumes a certain level of caching on the receiving side ( the GUI ) and leverages it by only computing
* the absolutely necessary ( "unpredictable" ) parts of the desired data and leaves it up to the receiver to
* put the whole data together by combining it with what has been returned previously already .
*
* It works like this :
* Certain parts ( the template data ) of an entity state are "predictable" and can therefore 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 - global modifications , which apply to all entities owned by that player . This includes stuff
* like bonuses from researched techs , civs bonuses , team bonuses .
* - Secondly , entity - local modifications , which apply to individual entities . This includes buffs or debuffs
* from status effects or auras ( of other entities ) .
*
* To take advantage of this , we separate the whole entity state into three parts :
* 1. The template data of the entity ' s template , which only has the player - global modifications applied . It is
* the same for all entities with the same template and owner .
* 2. The modified template data of the entity , which can contain a subset of the template data ' s values ,
* overwriting them . It has both the player - global and entity - local modifications applied , but only contains the
* values that actually differ from its template data . It is therefore the same for all entities with the same
* owner , template , and modifications ID .
* 3. the dynamic state , which differs from entity to entity and has to always be computed from scratch . It should be kept
* as small as possible .
* /
class EntityStateRetriever
{
_computedTemplateData = { } ;
/ * *
* Compute basic information about a given template accounting for all modifications registered to a given player .
* The returned data is ( of course ) consistent . So to avoid unnecessary work , this should never be called twice for the
* same player and template , unless the player 's modifications changed in the meantime. It is the caller' s responsibility
* to cache and reuse the results .
* /
getTemplateData ( player , templateName )
{
if ( this . _hasComputedTemplateData ( player , templateName ) )
warn ( "GetTemplateData called multiple times for the template '" + templateName + "'and player " + player + ". The return value should have been cached in the GUI." ) ;
this . _addComputedTemplateData ( player , templateName ) ;
const template = Engine . QueryInterface ( SYSTEM _ENTITY , IID _TemplateManager ) ? . GetTemplate ( templateName ) ;
const civ = QueryPlayerIDInterface ( player , IID _Identity ) . GetCiv ( ) ;
return template && g _TemplateHelper . computeDataFromPlayer ( template , AuraTemplates . GetAll ( ) , Resources , player , civ ) ;
}
/ * *
* Compute basic information about a given entity . This includes the template data , the modified template data , and
* the dynamic state , but only what hasn ' t been computed since the last time that the player modifications changed .
* The caller has to cache and reuse the data where possible . See the class description .
* /
getEntityState ( player , ent )
{
if ( ! ent || ent == INVALID _ENTITY )
return null ;
const cmpTemplateManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _TemplateManager ) ;
if ( ! cmpTemplateManager )
return null ;
const templateName = cmpTemplateManager . GetCurrentTemplateName ( ent ) ;
const template = templateName && cmpTemplateManager . GetTemplate ( templateName ) ;
const owner = Engine . QueryInterface ( ent , IID _Ownership ) ? . GetOwner ( ) ;
// All entities must have a template and an owner; if not then it's a nonexistent entity id.
if ( ! template || owner === undefined )
return null ;
const ret = {
"dynamicState" : this . _computeDynamicState ( owner , player , ent , templateName )
} ;
const civ = QueryPlayerIDInterface ( owner , IID _Identity ) . GetCiv ( ) ;
if ( ! this . _hasComputedTemplateData ( owner , templateName ) )
{
ret . templateData = g _TemplateHelper . computeDataFromPlayer ( template , AuraTemplates . GetAll ( ) , Resources , owner , civ ) ;
this . _addComputedTemplateData ( owner , templateName ) ;
}
const info = Engine . QueryInterface ( SYSTEM _ENTITY , IID _ModifiersManager ) ? . GetModifiersInfo ( ent ) ;
// A falsy modifications ID means the entity doesn't have any modifications applied locally, just the ones from its owner.
if ( info . modificationsID )
{
ret . modificationsID = info . modificationsID ;
if ( ! this . _hasComputedTemplateData ( owner , templateName , info . modificationsID ) )
{
ret . modifiedTemplateData = g _TemplateHelper . computeDataFromEntity ( template , AuraTemplates . GetAll ( ) , Resources , ent , civ , info . modifiedComponents ) ;
this . _addComputedTemplateData ( owner , templateName , info . modificationsID ) ;
}
}
return ret ;
}
onPlayerModificationsChanged ( player )
{
this . _computedTemplateData [ player ] ? . clear ( ) ;
}
reset ( )
{
for ( const player in this . _computedTemplateData )
this . _computedTemplateData [ player ] . clear ( ) ;
}
_addComputedTemplateData ( player , templateName , modificationsID = "" )
{
let playerTable = this . _computedTemplateData [ player ] ;
if ( ! playerTable )
{
playerTable = new Set ( ) ;
this . _computedTemplateData [ player ] = playerTable ;
}
playerTable . add ( ` ${ templateName } ${ modificationsID } ` ) ;
}
_hasComputedTemplateData ( player , templateName , modificationsID = "" )
{
return this . _computedTemplateData [ player ] ? . has ( ` ${ templateName } ${ modificationsID } ` ) ;
}
/ * *
* Get an entity ' s dynamic state , which is unpredictable and differs from entity to entity .
* The information is pulled from its components .
* The structure of each component ' s data has to be coordinated with the one in TemplateHelper , so that they
* can later be merged into one .
* @ param { number } owner - The owner of the entity .
* @ param { number } player - The player , for whom to calculate the state for .
* @ param { number } ent - ID of the entity .
* @ param { string } templateName - The entity ' s template .
* /
_computeDynamicState ( owner , player , ent , templateName )
{
const ret = {
"id" : ent ,
// TODO: Should maybe be renamed to owner for clarity.
"player" : owner ,
"templateName" : templateName
} ;
const cmpAttack = Engine . QueryInterface ( ent , IID _Attack ) ;
const cmpPosition = Engine . QueryInterface ( ent , IID _Position ) ;
const cmpRangeManager = Engine . QueryInterface ( SYSTEM _ENTITY , IID _RangeManager ) ;
const cmpUnitAI = Engine . QueryInterface ( ent , IID _UnitAI ) ;
if ( cmpPosition ? . IsInWorld ( ) && cmpAttack ? . GetAttackTypes ( ) . includes ( "Ranged" ) )
{
ret . attack = {
"Ranged" : {
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
"elevationAdaptedRange" : cmpRangeManager . GetElevationAdaptedRange (
cmpPosition . GetPosition ( ) , cmpPosition . GetRotation ( ) , cmpAttack . GetRange ( "Ranged" ) . max ,
cmpAttack . GetAttackYOrigin ( "Ranged" ) , cmpUnitAI ? 0 : 2 * Math . PI
)
}
} ;
}
const cmpBuildingAI = Engine . QueryInterface ( ent , IID _BuildingAI ) ;
if ( cmpBuildingAI )
ret . buildingAI = {
"arrowCount" : cmpBuildingAI . GetArrowCount ( )
} ;
const cmpCapturable = QueryMiragedInterface ( ent , IID _Capturable ) ;
if ( cmpCapturable )
ret . capturePoints = cmpCapturable . GetCapturePoints ( ) ;
const cmpIdentity = Engine . QueryInterface ( ent , IID _Identity ) ;
ret . controllable = ! cmpIdentity || cmpIdentity . IsControllable ( ) ;
const cmpFormation = Engine . QueryInterface ( ent , IID _Formation ) ;
if ( cmpFormation )
ret . formation = {
"members" : cmpFormation . GetMembers ( )
} ;
const cmpFoundation = QueryMiragedInterface ( ent , IID _Foundation ) ;
if ( cmpFoundation )
ret . foundation = {
"numBuilders" : cmpFoundation . GetNumBuilders ( ) ,
"buildTime" : cmpFoundation . GetBuildTime ( )
} ;
const cmpGarrisonable = Engine . QueryInterface ( ent , IID _Garrisonable ) ;
if ( cmpGarrisonable )
ret . garrisonable = {
"holder" : cmpGarrisonable . HolderID ( )
} ;
const cmpGarrisonHolder = Engine . QueryInterface ( ent , IID _GarrisonHolder ) ;
if ( cmpGarrisonHolder )
ret . garrisonHolder = {
"entities" : cmpGarrisonHolder . GetEntities ( ) ,
"occupiedSlots" : cmpGarrisonHolder . OccupiedSlots ( )
} ;
const cmpGate = Engine . QueryInterface ( ent , IID _Gate ) ;
if ( cmpGate )
ret . gate = {
"locked" : cmpGate . IsLocked ( )
} ;
const cmpGuard = Engine . QueryInterface ( ent , IID _Guard ) ;
if ( cmpGuard )
ret . guard = {
"entities" : cmpGuard . GetEntities ( )
} ;
const cmpHealth = QueryMiragedInterface ( ent , IID _Health ) ;
if ( cmpHealth )
{
ret . hitpoints = cmpHealth . GetHitpoints ( ) ;
ret . needsRepair = cmpHealth . IsRepairable ( ) && cmpHealth . IsInjured ( ) ;
ret . needsHeal = ! cmpHealth . IsUnhealable ( ) ;
}
if ( cmpPosition )
{
if ( cmpPosition . IsInWorld ( ) )
ret . position = cmpPosition . GetPosition ( ) ;
if ( cmpPosition . GetTurretParent ( ) != INVALID _ENTITY )
ret . turretParent = cmpPosition . GetTurretParent ( ) ;
}
const cmpPack = Engine . QueryInterface ( ent , IID _Pack ) ;
if ( cmpPack )
ret . pack = {
"packed" : cmpPack . IsPacked ( ) ,
"progress" : cmpPack . GetProgress ( )
} ;
const cmpProductionQueue = Engine . QueryInterface ( ent , IID _ProductionQueue ) ;
if ( cmpProductionQueue )
ret . production = {
"queue" : cmpProductionQueue . GetQueue ( ) ,
"autoqueue" : cmpProductionQueue . IsAutoQueueing ( )
} ;
const cmpPromotion = Engine . QueryInterface ( ent , IID _Promotion ) ;
if ( cmpPromotion )
ret . promotion = {
"curr" : cmpPromotion . GetCurrentXp ( ) ,
} ;
const cmpRallyPoint = Engine . QueryInterface ( ent , IID _RallyPoint ) ;
if ( cmpRallyPoint )
ret . rallyPoint = { "position" : cmpRallyPoint . GetPositions ( ) [ 0 ] } ; // undefined or {x,z} object
const cmpRepairable = QueryMiragedInterface ( ent , IID _Repairable ) ;
if ( cmpRepairable )
ret . repairable = {
"numBuilders" : cmpRepairable . GetNumBuilders ( ) ,
"buildTime" : cmpRepairable . GetBuildTime ( )
} ;
const cmpResourceDropsite = Engine . QueryInterface ( ent , IID _ResourceDropsite ) ;
if ( cmpResourceDropsite )
ret . resourceDropsite = {
"shared" : cmpResourceDropsite . IsShared ( )
} ;
const cmpResearcher = Engine . QueryInterface ( ent , IID _Researcher ) ;
if ( cmpResearcher )
ret . researcher = {
"technologies" : cmpResearcher . GetTechnologiesList ( )
} ;
const cmpResourceGatherer = Engine . QueryInterface ( ent , IID _ResourceGatherer ) ;
if ( cmpResourceGatherer )
ret . resourceCarrying = cmpResourceGatherer . GetCarryingStatus ( ) ;
const cmpResourceSupply = QueryMiragedInterface ( ent , IID _ResourceSupply ) ;
if ( cmpResourceSupply )
ret . resourceSupply = {
"amount" : cmpResourceSupply . GetCurrentAmount ( ) ,
"numGatherers" : cmpResourceSupply . GetNumGatherers ( )
} ;
const cmpStatusEffects = Engine . QueryInterface ( ent , IID _StatusEffectsReceiver ) ;
if ( cmpStatusEffects )
ret . statusEffects = cmpStatusEffects . GetActiveStatuses ( ) ;
const cmpTrader = Engine . QueryInterface ( ent , IID _Trader ) ;
if ( cmpTrader )
ret . trader = {
"goods" : cmpTrader . GetGoods ( )
} ;
const cmpTrainer = Engine . QueryInterface ( ent , IID _Trainer ) ;
if ( cmpTrainer )
ret . trainer = {
// TODO: This is technically not "dynamic", since it only depends on the template and modifications,
// so it should be made part of the template data instead in some way without causing too much code
// duplication.
"entities" : cmpTrainer . GetEntitiesList ( )
} ;
const cmpTurretable = Engine . QueryInterface ( ent , IID _Turretable ) ;
if ( cmpTurretable )
ret . turretable = {
"ejectable" : cmpTurretable . IsEjectable ( ) ,
"holder" : cmpTurretable . HolderID ( )
} ;
const cmpTurretHolder = Engine . QueryInterface ( ent , IID _TurretHolder ) ;
if ( cmpTurretHolder )
ret . turretHolder = {
"turretPoints" : cmpTurretHolder . GetTurretPoints ( )
} ;
if ( cmpUnitAI )
ret . unitAI = {
"state" : cmpUnitAI . GetCurrentState ( ) ,
"orders" : cmpUnitAI . GetOrders ( ) ,
"hasWorkOrders" : cmpUnitAI . HasWorkOrders ( ) ,
"isGuarding" : cmpUnitAI . IsGuardOf ( ) ,
"isIdle" : cmpUnitAI . IsIdle ( ) ,
2026-08-06 10:37:28 -07:00
"formationController" : cmpUnitAI . GetFormationController ( )
Optimise GetEntityState
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.
2026-02-12 06:52:21 -08:00
} ;
const cmpUpgrade = Engine . QueryInterface ( ent , IID _Upgrade ) ;
if ( cmpUpgrade )
ret . upgrade = {
"progress" : cmpUpgrade . GetProgress ( ) ,
"template" : cmpUpgrade . GetUpgradingTo ( ) ,
"isUpgrading" : cmpUpgrade . IsUpgrading ( )
} ;
if ( cmpRangeManager )
ret . visibility = cmpRangeManager . GetLosVisibility ( ent , player ) ;
// Because mirage entities mirage other entities' components, some values can't be statically read from their
// template. They therefore are "missing" from their template data, so we retrieve them from the components
// directly here and add them to the dynamic state instead, so that they're still present in the entire final
// entity state (the GUI expects them).
// This is somewhat hacky and it would be good to achieve this in another way.
const cmpMirage = Engine . QueryInterface ( ent , IID _Mirage ) ;
if ( cmpMirage )
{
if ( cmpCapturable )
ret . maxCapturePoints = cmpCapturable . GetMaxCapturePoints ( ) ;
if ( cmpHealth )
ret . maxHitpoints = cmpHealth . GetMaxHitpoints ( ) ;
if ( cmpResourceSupply )
{
ret . resourceSupply . isInfinite = cmpResourceSupply . IsInfinite ( ) ;
ret . resourceSupply . max = cmpResourceSupply . GetMaxAmount ( ) ;
ret . resourceSupply . type = cmpResourceSupply . GetType ( ) ;
ret . resourceSupply . killBeforeGather = cmpResourceSupply . GetKillBeforeGather ( ) ;
ret . resourceSupply . maxGatherers = cmpResourceSupply . GetMaxGatherers ( ) ;
}
}
return ret ;
}
}
Engine . RegisterGlobal ( "EntityStateRetriever" , EntityStateRetriever ) ;