From 8dcd4446809c87a7caea9585343a383ed47b4a13 Mon Sep 17 00:00:00 2001 From: sunzenshen Date: Wed, 23 Sep 2026 19:41:54 -0600 Subject: [PATCH] Look along the ledge for a drop-down's clear column Path::PostProcess turns a walk link onto a lower area into a DROP_DOWN by pushing the crossing point forward, 10 u at a time up to two hull widths, until a hull traced straight down is clear. It only looks straight ahead of where the path crosses the edge. When the opening below is off to one side of that line - a hatch in a floor, a gap in a railing - no column is clear, and the loop still sets the drop point at the last, unchecked position: two hull widths out, on solid floor. The bot walks there, stands on the floor it was meant to fall through, and presses into whatever is beyond until the stuck monitor fires; the next path puts it back. When the straight search finds nothing, try the same search at offsets along the portal (either side of the crossing, nearest first, staying inside the portal) and cross at the first clear column. Paths whose straight search succeeds are unchanged, and if no column is clear anywhere along the portal the old behavior stands. Found on ntre_sentinel_jgr: each freight elevator's roof has a 51 x 55 u hatch, and the roof's drop link into the shaft runs the whole 100 u edge. Measured there (the shipped mesh at the time, bot matches, paired per spawn pin, 36 pairs): stuck events at the hatches 44.9 -> 2.1 a match, OnStuck x0.55 [0.42, 0.71], 26 down / 10 up. Across ten maps on the current meshes (apparatus, dusk, ghost, oilstain, sentinel_ctg, skyline, terminal, transit, yard, and sentinel_jgr with its hatches since fixed in the mesh; 258 pairs over two batches) the search fires where drops cross off-axis openings and nothing gets worse: OnStuck x0.87 [0.78, 0.98], kills x1.01 [0.99, 1.04], every match clean. Stuck events at the drops it changed stay near zero in both arms, so that cross-map figure is mostly noise from unrelated heavy tails; the fix's measured effect is the hatch case above. --- src/game/server/NextBot/Path/NextBotPath.cpp | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/game/server/NextBot/Path/NextBotPath.cpp b/src/game/server/NextBot/Path/NextBotPath.cpp index 02ced4b052..f8732cf61e 100644 --- a/src/game/server/NextBot/Path/NextBotPath.cpp +++ b/src/game/server/NextBot/Path/NextBotPath.cpp @@ -138,6 +138,49 @@ bool Path::ComputePathDetails( INextBot *bot, const Vector &start ) } } +#ifdef NEO + // The search above only looks straight ahead of the crossing point. + // When the opening below is off to one side of it (a hatch in a floor), + // it finds no clear column and the drop point ends up on solid floor 2 hull widths out, + // where the bot stands and never falls. + // Look along the portal for the nearest clear column and cross there instead. + if ( pushDist > maxPushDist ) + { + const Vector2D side( -dir.y, dir.x ); + const float crossAlong = ( to->pos.x - to->m_portalCenter.x ) * side.x + ( to->pos.y - to->m_portalCenter.y ) * side.y; + bool found = false; + for ( float lat = inc; lat <= 2.0f * to->m_portalHalfWidth && !found; lat += inc ) + { + for ( int sgn = -1; sgn <= 1 && !found; sgn += 2 ) + { + if ( fabsf( crossAlong + sgn * lat ) > to->m_portalHalfWidth ) + continue; + + for ( float push = 0.0f; push <= maxPushDist; push += inc ) + { + Vector pos = to->pos + Vector( push * dir.x + sgn * lat * side.x, push * dir.y + sgn * lat * side.y, 0.0f ); + Vector lowerPos = Vector( pos.x, pos.y, toPos.z ); + + trace_t result; + NextBotTraceFilterIgnoreActors filter( bot->GetEntity(), COLLISION_GROUP_NONE ); + UTIL_TraceHull( pos, lowerPos, + Vector( -halfWidth, -halfWidth, stepHeight ), Vector( halfWidth, halfWidth, hullHeight ), + bot->GetBodyInterface()->GetSolidMask(), &filter, &result ); + + if ( result.fraction >= 1.0f ) + { + to->pos.x += sgn * lat * side.x; + to->pos.y += sgn * lat * side.y; + pushDist = push; + found = true; + break; + } + } + } + } + } +#endif + Vector startDrop( to->pos.x + pushDist * dir.x, to->pos.y + pushDist * dir.y, to->pos.z ); Vector endDrop( startDrop.x, startDrop.y, to->area->GetZ( to->pos ) );