UnitAI: properly follow queued return resources orders

GATHER.FINDINGNEWTARGET inserts a ReturnResource order ahead of the
next queued order when the unit is still carrying resources, but
skips doing so if the next order already returns those resources.
Unfortunately, the check was incorrectly written, missing a `.type`.
This meant we always added a fresh order.

Units could then sometimes return resources to the closest dropsites,
then move to the actually queued dropsite, which could lead to
unexpected pathing.

Introduced in 0dda3b579c where the feature was added.
This commit is contained in:
Lancelot de Ferrière
2026-08-17 09:08:28 +02:00
parent 2add365a4f
commit 4e04ccca4d
2 changed files with 70 additions and 1 deletions
@@ -2918,7 +2918,7 @@ UnitAI.prototype.UnitFsmSpec = {
// but first check what is our next order and, if needed, insert a returnResource order
const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer);
if (cmpResourceGatherer.IsCarrying(resourceType.generic) &&
this.orderQueue.length > 1 && this.orderQueue[1] !== "ReturnResource" &&
this.orderQueue.length > 1 && this.orderQueue[1].type !== "ReturnResource" &&
(this.orderQueue[1].type !== "Gather" || this.orderQueue[1].data.type.generic !== resourceType.generic))
{
const nearestDropsite = this.FindNearestDropsite(resourceType.generic);
@@ -1259,4 +1259,73 @@ function TestAttackGroupBehavior()
})();
}
function TestGatherFindingNewTargetRespectsQueuedReturnResource()
{
ResetState();
const ent = 10;
const gatherTarget = 20;
const queuedDropsite = 30;
const resourceType = { "generic": "food", "specific": "grain" };
AddMock(SYSTEM_ENTITY, IID_Timer, {
"SetInterval": () => {},
"SetTimeout": () => {}
});
AddMock(ent, IID_Position, {
"IsInWorld": () => true
});
AddMock(ent, IID_ResourceGatherer, {
"IsCarrying": (type) => type == resourceType.generic,
"AddToPlayerCounter": () => {},
"RemoveFromPlayerCounter": () => {},
"GetLastCarriedType": () => resourceType,
"StartGathering": () => true,
"StopGathering": () => {}
});
const unitAI = ConstructComponent(ent, "UnitAI", {
"FormationController": "false",
"DefaultStance": "aggressive",
"FleeDistance": 10
});
unitAI.OnCreate();
unitAI.order = {
"type": "Gather",
"data": {
"force": false,
"target": gatherTarget,
"type": resourceType,
"template": "gaia/fruit/grain"
}
};
unitAI.orderQueue = [
unitAI.order,
{ "type": "ReturnResource", "data": { "target": queuedDropsite, "force": false } }
];
let dropsiteLookups = 0;
unitAI.FindNearestDropsite = () => { ++dropsiteLookups; return 999; };
unitAI.CheckTargetRange = () => true;
unitAI.UnitFsm.Init(unitAI, "INDIVIDUAL.GATHER.GATHERING");
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), "INDIVIDUAL.GATHER.GATHERING");
// Simulates the target running out: matches ResourceGatherer.PerformGather,
// which sends this same message on the same condition.
unitAI.CheckTargetRange = () => false;
unitAI.AbleToMove = () => true;
unitAI.MoveTo = () => true;
unitAI.ProcessMessage("TargetInvalidated", null);
TS_ASSERT_EQUALS(dropsiteLookups, 0);
TS_ASSERT_EQUALS(unitAI.orderQueue.length, 1);
TS_ASSERT_EQUALS(unitAI.order.type, "ReturnResource");
TS_ASSERT_EQUALS(unitAI.order.data.target, queuedDropsite);
TS_ASSERT_EQUALS(unitAI.GetCurrentState(), "INDIVIDUAL.RETURNRESOURCE.APPROACHING");
}
TestGatherFindingNewTargetRespectsQueuedReturnResource();
TestAttackGroupBehavior();