From a6f66492cbc5a3470d74d53e2aaf8d231056b3cf Mon Sep 17 00:00:00 2001 From: Frankie-hz <105882754+Frankie-hz@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:31:25 -0400 Subject: [PATCH] Fix mob chase reading a cleared path after checking in-range --- .../systems/pathing/chase_target_on_top.lua | 48 +++++++++++++++++++ src/map/ai/controllers/mob_controller.cpp | 4 +- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 scripts/tests/systems/pathing/chase_target_on_top.lua diff --git a/scripts/tests/systems/pathing/chase_target_on_top.lua b/scripts/tests/systems/pathing/chase_target_on_top.lua new file mode 100644 index 00000000000..e52bd33ba63 --- /dev/null +++ b/scripts/tests/systems/pathing/chase_target_on_top.lua @@ -0,0 +1,48 @@ +----------------------------------- +-- A chasing mob whose target lands within 1 yalm of it +-- +-- The in-range directness probe clears the chase path when its navmesh query fails, which it always does under 1 yalm. +-- Move() then read the destination of the cleared path, aborting builds with STL assertions (Linux ASAN, _MSVC_STL_HARDENING). +----------------------------------- + +describe('Mob chase when the target lands on top of it', function() + ---@type CClientEntityPair + local player + + ---@type CTestEntity + local mob + + before_each(function() + player = xi.test.world:spawnPlayer({ zone = xi.zone.WEST_RONFAURE, level = 99 }) + mob = player.entities:moveTo('Wild_Rabbit') + mob:respawn() + mob:clearPath() + mob:setUnkillable(true) + mob:setBaseSpeed(40) -- the mob default, other tests leave this rabbit immobile + end) + + it('settles on the target after the in-range probe clears its chase path', function() + local start = mob:getPos() + player:setPos(start.x - 10, start.y, start.z, 0) + + mob:addEnmity(player, 100, 100) + mob:engage(player:getTargID()) + + for _ = 1, 5 do + xi.test.world:tickEntity(mob) + if mob:isFollowingPath() then + break + end + end + + assert(mob:isFollowingPath(), 'mob should be chasing along a path') + assert(mob:checkDistance(player) > 3, 'mob should still be short of melee range') + + local chasePos = mob:getPos() + player:setPos(chasePos.x + 0.5, chasePos.y, chasePos.z, 0) + xi.test.world:tickEntity(mob) + + assert(mob:isEngaged(), 'mob should still be engaged on the player') + assert(not mob:isFollowingPath(), 'mob should settle on the player instead of walking a stale path') + end) +end) diff --git a/src/map/ai/controllers/mob_controller.cpp b/src/map/ai/controllers/mob_controller.cpp index 303f5838d72..0f2c38b96eb 100644 --- a/src/map/ai/controllers/mob_controller.cpp +++ b/src/map/ai/controllers/mob_controller.cpp @@ -1251,7 +1251,6 @@ void CMobController::Move() } const float currentDistance = distance(PMob->loc.p, PTarget->loc.p); - const bool isFollowingPath = PMob->PAI->PathFind->IsFollowingPath(); // Teleport type 1: jump in if out of melee range but within 30y and off cooldown. if (PMob->getMobMod(xi::MobMod::TeleportType) == 1 && @@ -1341,7 +1340,8 @@ void CMobController::Move() bool needNewPath = false; bool isStuckRepath = false; bool targetMoved = false; - if (isFollowingPath) + // Read after the in-range probe, which clears the path when its query fails. + if (PMob->PAI->PathFind->IsFollowingPath()) { needNewPath = !isWithinDistance(PMob->PAI->PathFind->GetDestination(), PTarget->loc.p, kChaseRepathDrift); }