From 4e04ccca4dcc21724bafb94e7f0709eb6df3f69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lancelot=20de=20Ferri=C3=A8re?= Date: Mon, 17 Aug 2026 09:08:28 +0200 Subject: [PATCH] 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. --- .../public/simulation/components/UnitAI.js | 2 +- .../components/tests/test_UnitAI.js | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 54548f76c0..25449d0c1e 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -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); diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js index d381dc7850..2ae6eef78e 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -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();