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.
311 lines
13 KiB
JavaScript
311 lines
13 KiB
JavaScript
Engine.LoadComponentScript("interfaces/ModifiersManager.js");
|
|
Engine.LoadComponentScript("ModifiersManager.js");
|
|
Engine.LoadHelperScript("Player.js");
|
|
Engine.LoadHelperScript("ValueModification.js");
|
|
Engine.LoadComponentScript("interfaces/Health.js");
|
|
|
|
let cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
|
cmpModifiersManager.Init();
|
|
|
|
// These should be different as that is the general case.
|
|
const PLAYER_ID_FOR_TEST = 2;
|
|
const PLAYER_ENTITY_ID = 3;
|
|
const STRUCTURE_ENTITY_ID = 5;
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_RangeManager, {
|
|
"GetEntitiesByPlayer": () => [],
|
|
});
|
|
|
|
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|
"GetPlayerByID": (a) => PLAYER_ENTITY_ID
|
|
});
|
|
|
|
AddMock(PLAYER_ENTITY_ID, IID_Player, {
|
|
"GetPlayerID": () => PLAYER_ID_FOR_TEST
|
|
});
|
|
|
|
const entitiesToTest = [STRUCTURE_ENTITY_ID, 6, 7, 8];
|
|
for (const ent of entitiesToTest)
|
|
AddMock(ent, IID_Ownership, {
|
|
"GetOwner": () => PLAYER_ID_FOR_TEST
|
|
});
|
|
|
|
AddMock(PLAYER_ENTITY_ID, IID_Identity, {
|
|
"GetClassesList": () => "Player",
|
|
});
|
|
AddMock(STRUCTURE_ENTITY_ID, IID_Identity, {
|
|
"GetClassesList": () => "Structure",
|
|
});
|
|
AddMock(6, IID_Identity, {
|
|
"GetClassesList": () => "Infantry",
|
|
});
|
|
AddMock(7, IID_Identity, {
|
|
"GetClassesList": () => "Unit",
|
|
});
|
|
AddMock(8, IID_Identity, {
|
|
"GetClassesList": () => "Structure Unit",
|
|
});
|
|
|
|
// Sprinkle random serialisation cycles.
|
|
function SerializationCycle()
|
|
{
|
|
const data = cmpModifiersManager.Serialize();
|
|
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
|
cmpModifiersManager.Deserialize(data);
|
|
}
|
|
|
|
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST, "from": -1, "to": PLAYER_ENTITY_ID });
|
|
|
|
cmpModifiersManager.AddModifier("Test_A", "Test_A_0", [{ "affects": ["Structure"], "add": 10 }], 10, "testLol");
|
|
|
|
cmpModifiersManager.AddModifier("Test_A", "Test_A_0", [{ "affects": ["Structure"], "add": 10 }], PLAYER_ENTITY_ID);
|
|
cmpModifiersManager.AddModifier("Test_A", "Test_A_1", [{ "affects": ["Infantry"], "add": 5 }], PLAYER_ENTITY_ID);
|
|
cmpModifiersManager.AddModifier("Test_A", "Test_A_2", [{ "affects": ["Unit"], "add": 3 }], PLAYER_ENTITY_ID);
|
|
|
|
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, PLAYER_ENTITY_ID), 5);
|
|
cmpModifiersManager.AddModifier("Test_A", "Test_A_Player", [{ "affects": ["Player"], "add": 3 }], PLAYER_ENTITY_ID);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, PLAYER_ENTITY_ID), 8);
|
|
|
|
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);
|
|
|
|
cmpModifiersManager.RemoveAllModifiers("Test_A_0", PLAYER_ENTITY_ID);
|
|
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_A", 5, 5), 5);
|
|
|
|
cmpModifiersManager.AddModifiers("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.
|
|
cmpModifiersManager.AddModifier("Test_C", "Test_C_0", [{ "affects": ["Structure"], "add": 10 }], STRUCTURE_ENTITY_ID);
|
|
cmpModifiersManager.AddModifier("Test_C", "Test_C_invalid", [{ "affects": ["Unit"], "add": 5 }], STRUCTURE_ENTITY_ID);
|
|
|
|
SerializationCycle();
|
|
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, STRUCTURE_ENTITY_ID), 15);
|
|
|
|
// test that local modifications are indeed applied after global managers
|
|
cmpModifiersManager.AddModifier("Test_C", "Test_C_player", [{ "affects": ["Structure"], "replace": 2 }], PLAYER_ENTITY_ID);
|
|
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, STRUCTURE_ENTITY_ID), 12);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 2, STRUCTURE_ENTITY_ID), 12);
|
|
|
|
SerializationCycle();
|
|
|
|
// test removal
|
|
cmpModifiersManager.RemoveModifier("Test_C", "Test_C_player", PLAYER_ENTITY_ID);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, STRUCTURE_ENTITY_ID), 15);
|
|
|
|
// check that things still work properly if we change global modifications
|
|
cmpModifiersManager.AddModifier("Test_C", "Test_C_player", [{ "affects": ["Structure"], "add": 12 }], PLAYER_ENTITY_ID);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_C", 5, STRUCTURE_ENTITY_ID), 27);
|
|
|
|
TS_ASSERT(cmpModifiersManager.HasAnyModifier("Test_C_player", PLAYER_ENTITY_ID));
|
|
TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_player", PLAYER_ENTITY_ID));
|
|
|
|
SerializationCycle();
|
|
|
|
TS_ASSERT(cmpModifiersManager.HasModifier("Test_C", "Test_C_player", PLAYER_ENTITY_ID));
|
|
TS_ASSERT(!cmpModifiersManager.HasModifier("Test_C", "Test_C_player", STRUCTURE_ENTITY_ID));
|
|
|
|
|
|
// Regression test for a caching issue
|
|
cmpModifiersManager.AddModifier("Test_E", "Test_E_player", [{ "affects": ["Structure"], "add": 1 }], PLAYER_ENTITY_ID);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_E", 3, STRUCTURE_ENTITY_ID), 4);
|
|
cmpModifiersManager.AddModifier("Test_E", "Test_E_1", [{ "affects": ["Structure"], "add": 1 }], STRUCTURE_ENTITY_ID);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_E", 4, STRUCTURE_ENTITY_ID), 6);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_E", 5, STRUCTURE_ENTITY_ID), 7);
|
|
|
|
|
|
// 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
|
|
});
|
|
|
|
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
|
cmpModifiersManager.Init();
|
|
|
|
cmpModifiersManager.AddModifier("Test_D", "Test_D_0", [{ "affects": ["Structure"], "add": 10 }], PLAYER_ENTITY_ID);
|
|
cmpModifiersManager.AddModifier("Test_D", "Test_D_1", [{ "affects": ["Structure"], "add": 1 }], PLAYER_ENTITY_ID + 1);
|
|
cmpModifiersManager.AddModifier("Test_D", "Test_D_2", [{ "affects": ["Structure"], "add": 5 }], STRUCTURE_ENTITY_ID);
|
|
|
|
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST, "from": -1, "to": PLAYER_ENTITY_ID });
|
|
cmpModifiersManager.OnGlobalPlayerEntityChanged({ "player": PLAYER_ID_FOR_TEST + 1, "from": -1, "to": PLAYER_ENTITY_ID + 1 });
|
|
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Test_D", 10, 5), 25);
|
|
cmpModifiersManager.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);
|
|
|
|
// Test: Entity changes owner from player 2 (HP modifier) to player 3 (Vision modifier)
|
|
(function Test_OwnerChange_ModifierSwitch()
|
|
{
|
|
const PLAYER2_ID = 2;
|
|
const PLAYER3_ID = 3;
|
|
const PLAYER2_ENTITY = 20;
|
|
const PLAYER3_ENTITY = 21;
|
|
const TEST_ENTITY = 30;
|
|
|
|
const baseHp = 100;
|
|
const baseVision = 20;
|
|
|
|
// Set up mocks for both players
|
|
AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
|
|
"GetPlayerByID": (a) => a === PLAYER2_ID ? PLAYER2_ENTITY : PLAYER3_ENTITY
|
|
});
|
|
AddMock(PLAYER2_ENTITY, IID_Player, {
|
|
"GetPlayerID": () => PLAYER2_ID
|
|
});
|
|
AddMock(PLAYER3_ENTITY, IID_Player, {
|
|
"GetPlayerID": () => PLAYER3_ID
|
|
});
|
|
AddMock(TEST_ENTITY, IID_Ownership, {
|
|
"GetOwner": () => PLAYER2_ID
|
|
});
|
|
AddMock(TEST_ENTITY, IID_Identity, {
|
|
"GetClassesList": () => "Unit"
|
|
});
|
|
// These components cache the values, so we need to mock the message passing.
|
|
let cachedHp = baseHp;
|
|
AddMock(TEST_ENTITY, IID_Health, {
|
|
"GetHitPoints": () => cachedHp,
|
|
});
|
|
let cachedVision = baseVision;
|
|
AddMock(TEST_ENTITY, IID_Vision, {
|
|
"GetRange": () => cachedVision
|
|
});
|
|
const oldPostMessage = Engine.PostMessage;
|
|
const oldBroadcastMessage = Engine.BroadcastMessage;
|
|
Engine.PostMessage = function(ent, iid, message)
|
|
{
|
|
if (message.component === "HP")
|
|
cachedHp = ApplyValueModificationsToEntity("HP", baseHp, TEST_ENTITY);
|
|
else if (message.component === "Vision")
|
|
cachedVision = ApplyValueModificationsToEntity("Vision", baseVision, TEST_ENTITY);
|
|
else
|
|
throw new Error("Unexpected component: " + message.component);
|
|
};
|
|
Engine.BroadcastMessage = function(iid, message)
|
|
{
|
|
if (message.component === "HP")
|
|
cachedHp = ApplyValueModificationsToEntity("HP", baseHp, TEST_ENTITY);
|
|
else if (message.component === "Vision")
|
|
cachedVision = ApplyValueModificationsToEntity("Vision", baseVision, TEST_ENTITY);
|
|
else
|
|
throw new Error("Unexpected component: " + message.component);
|
|
};
|
|
// Initialize ModifiersManager
|
|
const cmp = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
|
cmp.Init();
|
|
|
|
cmp.OnGlobalPlayerEntityChanged({ "player": PLAYER2_ID, "from": INVALID_PLAYER, "to": PLAYER2_ENTITY });
|
|
cmp.OnGlobalPlayerEntityChanged({ "player": PLAYER3_ID, "from": INVALID_PLAYER, "to": PLAYER3_ENTITY });
|
|
|
|
// Player 2 gets HP modifier
|
|
cmp.AddModifier("HP", "HP_mod", [{ "affects": ["Unit"], "add": 50 }], PLAYER2_ENTITY);
|
|
// Player 3 gets Vision modifier
|
|
cmp.AddModifier("Vision", "Vision_mod", [{ "affects": ["Unit"], "add": 10 }], PLAYER3_ENTITY);
|
|
|
|
// Should have HP modified, not Vision
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("HP", baseHp, TEST_ENTITY), 150);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Vision", baseVision, TEST_ENTITY), 20);
|
|
TS_ASSERT_EQUALS(Engine.QueryInterface(TEST_ENTITY, IID_Health).GetHitPoints(), 150);
|
|
TS_ASSERT_EQUALS(Engine.QueryInterface(TEST_ENTITY, IID_Vision).GetRange(), 20);
|
|
|
|
// Change owner to player 3
|
|
AddMock(TEST_ENTITY, IID_Ownership, {
|
|
"GetOwner": () => PLAYER3_ID
|
|
});
|
|
cmp.OnGlobalOwnershipChanged({ "entity": TEST_ENTITY, "from": PLAYER2_ID, "to": PLAYER3_ID });
|
|
|
|
// Now should have Vision modified, not HP
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("HP", baseHp, TEST_ENTITY), 100);
|
|
TS_ASSERT_EQUALS(ApplyValueModificationsToEntity("Vision", baseVision, TEST_ENTITY), 30);
|
|
TS_ASSERT_EQUALS(Engine.QueryInterface(TEST_ENTITY, IID_Health).GetHitPoints(), 100);
|
|
TS_ASSERT_EQUALS(Engine.QueryInterface(TEST_ENTITY, IID_Vision).GetRange(), 30);
|
|
|
|
// Cleanup
|
|
Engine.PostMessage = oldPostMessage;
|
|
Engine.BroadcastMessage = oldBroadcastMessage;
|
|
})();
|
|
|
|
|
|
(function Test_ModifiersInfo()
|
|
{
|
|
const TEST_ENTITY_1 = 13;
|
|
const TEST_ENTITY_2 = 14;
|
|
|
|
cmpModifiersManager = ConstructComponent(SYSTEM_ENTITY, "ModifiersManager", {});
|
|
cmpModifiersManager.Init();
|
|
|
|
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
|
"modifiedComponents": new Set(),
|
|
"modificationsID": ""
|
|
});
|
|
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_2), {
|
|
"modifiedComponents": new Set(),
|
|
"modificationsID": ""
|
|
});
|
|
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_1, true);
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_1, true);
|
|
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
|
"modifiedComponents": new Set(["Component1"]),
|
|
"modificationsID": "Component1/Property1:Modifier1(2) "
|
|
});
|
|
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier2", [{ "affects": ["Unit"], "add": 2 }], TEST_ENTITY_1);
|
|
cmpModifiersManager.AddModifier("Component1/Property2", "Modifier3", [{ "affects": ["Unit"], "add": 3 }], TEST_ENTITY_1);
|
|
cmpModifiersManager.AddModifier("Component2/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 10 }], TEST_ENTITY_1);
|
|
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
|
"modifiedComponents": new Set(["Component1", "Component2"]),
|
|
"modificationsID": "Component1/Property1:Modifier1(2)Modifier2(1) Component1/Property2:Modifier3(1) Component2/Property1:Modifier1(1) "
|
|
});
|
|
cmpModifiersManager.RemoveModifier("Component1/Property2", "Modifier3", TEST_ENTITY_1);
|
|
TS_ASSERT_UNEVAL_EQUALS(cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1), {
|
|
"modifiedComponents": new Set(["Component1", "Component2"]),
|
|
"modificationsID": "Component1/Property1:Modifier1(2)Modifier2(1) Component2/Property1:Modifier1(1) "
|
|
});
|
|
|
|
const assertModificationsIDsEqual = equal =>
|
|
{
|
|
let expected = cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_1).modificationsID ==
|
|
cmpModifiersManager.GetModifiersInfo(TEST_ENTITY_2).modificationsID;
|
|
if (!equal)
|
|
expected = !expected;
|
|
|
|
TS_ASSERT(expected);
|
|
};
|
|
|
|
// Even if modifiers were added in different orders, two entities with the same one should have the same modifications ID.
|
|
cmpModifiersManager.AddModifier("Component2/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 10 }], TEST_ENTITY_2);
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_2, true);
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier2", [{ "affects": ["Unit"], "add": 2 }], TEST_ENTITY_2);
|
|
assertModificationsIDsEqual(false);
|
|
cmpModifiersManager.AddModifier("Component1/Property1", "Modifier1", [{ "affects": ["Unit"], "add": 1 }], TEST_ENTITY_2, true);
|
|
assertModificationsIDsEqual(true);
|
|
cmpModifiersManager.RemoveModifier("Component1/Property1", "Modifier3", TEST_ENTITY_1);
|
|
assertModificationsIDsEqual(true);
|
|
cmpModifiersManager.RemoveModifier("Component2/Property1", "Modifier1", TEST_ENTITY_1);
|
|
assertModificationsIDsEqual(false);
|
|
})();
|