0ad/binaries/data/mods/public/simulation/components/tests/test_ModifiersManager.js
Lancelot de Ferrière ff754b8bb1 Reset modifiers properly when changing ownership
The ModifiersManagers needs to reset caches and recompute modifiers when changing ownership. However, the code only did this for global modifiers for the new player entity, not the old player entity. This meant we would not reset all modifiers, and could OOS (#7996) or lead to incorrect values.

Fixes #7996

Reported by: Itms
2025-06-15 21:37:00 +02:00

249 lines
10 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;
})();