Trigger an exit-reentry when the target entity of an order is renamed

This lets sunit AI FSM states correctly handle target entity renaming by
processing a message when that happens. The default behaviour is to
leave-reenter the state, which re-runs sanity checks and optionally
picks a better behaviour.

UnitMotion is still not made aware of entity renaming, as the
leave-enter makes it irrelevant in practice. It still may be a good idea
to implement that someday.

Fixes the concern raised at de1bb8a766.
Fixes #5584

Comments by: bb, Freagarach
Reported by: minohaka, bb, Freagarach
Differential Revision: https://code.wildfiregames.com/D2735
This was SVN commit r23708.
This commit is contained in:
wraitii 2020-05-29 17:00:50 +00:00
parent 131590927f
commit 0363202a20
2 changed files with 110 additions and 7 deletions

View file

@ -193,6 +193,13 @@ UnitAI.prototype.UnitFsmSpec = {
// ignore
},
"OrderTargetRenamed": function() {
// By default, trigger an exit-reenter
// so that state preconditions are checked against the new entity
// (there is no reason to assume the target is still valid).
this.SetNextState(this.GetCurrentState());
},
// Formation handlers:
"FormationLeave": function(msg) {
@ -1978,6 +1985,7 @@ UnitAI.prototype.UnitFsmSpec = {
"Timer": function(msg) {
let target = this.order.data.target;
let attackType = this.order.data.attackType;
// Check the target is still alive and attackable
if (!this.CanAttack(target))
@ -2000,11 +2008,16 @@ UnitAI.prototype.UnitFsmSpec = {
if (!cmpBuildingAI)
{
let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
cmpAttack.PerformAttack(this.order.data.attackType, target);
cmpAttack.PerformAttack(attackType, target);
}
// PerformAttack might have triggered messages that moved us to another state.
if (this.GetCurrentState() != "INDIVIDUAL.COMBAT.ATTACKING")
return;
// Check we can still reach the target for the next attack
if (this.CheckTargetAttackRange(target, this.order.data.attackType))
if (this.CheckTargetAttackRange(target, attackType))
{
if (this.resyncAnimation)
{
@ -4144,24 +4157,32 @@ UnitAI.prototype.OnGlobalConstructionFinished = function(msg)
UnitAI.prototype.OnGlobalEntityRenamed = function(msg)
{
let changed = false;
for (let order of this.orderQueue)
let currentOrderChanged = false;
for (let i = 0; i < this.orderQueue.length; ++i)
{
let order = this.orderQueue[i];
if (order.data && order.data.target && order.data.target == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.target = msg.newentity;
}
if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity)
{
changed = true;
if (i == 0)
currentOrderChanged = true;
order.data.formationTarget = msg.newentity;
}
}
if (this.repairTarget && this.repairTarget == msg.entity)
this.repairTarget = msg.newentity;
if (!changed)
return;
if (changed)
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
if (currentOrderChanged)
this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg });
Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() });
};
UnitAI.prototype.OnAttacked = function(msg)

View file

@ -1,8 +1,10 @@
Engine.LoadHelperScript("FSM.js");
Engine.LoadHelperScript("Entity.js");
Engine.LoadHelperScript("Player.js");
Engine.LoadHelperScript("Sound.js");
Engine.LoadComponentScript("interfaces/Attack.js");
Engine.LoadComponentScript("interfaces/Auras.js");
Engine.LoadComponentScript("interfaces/Builder.js");
Engine.LoadComponentScript("interfaces/BuildingAI.js");
Engine.LoadComponentScript("interfaces/Capturable.js");
Engine.LoadComponentScript("interfaces/Resistance.js");
@ -17,6 +19,86 @@ Engine.LoadComponentScript("interfaces/UnitAI.js");
Engine.LoadComponentScript("Formation.js");
Engine.LoadComponentScript("UnitAI.js");
/**
* Fairly straightforward test that entity renaming is handled
* by unitAI states. These ought to be augmented with integration tests, ideally.
*/
function TestTargetEntityRenaming(init_state, post_state, setup)
{
ResetState();
const player_ent = 5;
const target_ent = 6;
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(SYSTEM_ENTITY, IID_ObstructionManager, {
"IsInTargetRange": () => false
});
let unitAI = ConstructComponent(player_ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
setup(unitAI, player_ent, target_ent);
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state);
unitAI.OnGlobalEntityRenamed({
"entity": target_ent,
"newentity": target_ent + 1
});
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state);
}
TestTargetEntityRenaming(
"INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE",
(unitAI, player_ent, target_ent) => {
unitAI.CanGarrison = (target) => target == target_ent;
unitAI.MoveToGarrisonRange = (target) => target == target_ent;
AddMock(target_ent, IID_GarrisonHolder, {
"CanPickup": () => false
});
unitAI.Garrison(target_ent, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.IDLE",
(unitAI, player_ent, target_ent) => {
QueryBuilderListInterface = () => {};
unitAI.CheckTargetRange = () => true;
unitAI.CanRepair = (target) => target == target_ent;
unitAI.Repair(target_ent, false, false);
}
);
TestTargetEntityRenaming(
"INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING",
(unitAI, player_ent, target_ent) => {
DistanceBetweenEntities = () => 10;
unitAI.CheckTargetRangeExplicit = () => false;
AddMock(player_ent, IID_UnitMotion, {
"MoveToTargetRange": () => true,
"GetRunMultiplier": () => 1,
"SetSpeedMultiplier": () => {},
"StopMoving": () => {}
});
unitAI.Flee(target_ent, false);
}
);
/* Regression test.
* Tests the FSM behaviour of a unit when walking as part of a formation,
* then exiting the formation.