Repository navigation
More tweaks to bot ladder approaches, climbs and dismounts - #2176
sunzenshen merged 23 commits into
Conversation
CGameMovement::LadderMove() turns the part of a move that pushes into a ladder's face into climbing up. Going down, forward therefore only descends once the view points further down than into the face; with the view still level it climbs the bot up and, near the top, off the ladder. With the climb claimed (NeotokyoRebuild#2176), PlayerLocomotion's descend states steer every claimed descent, and they pressed forward at once: ApproachDescendingLadder() kept steering towards the mount point after the bot was already on the ladder, and Approach() presses forward on any ladder it is descending. - IsForwardDownLadder(): would a forward press move the bot down. A press facing away from the face is refused too: standing on a floor, LadderMove() pushes the bot off the ladder for it. - Approach() presses nothing on a descent until that holds. - DescendLadder() looks down into the face (the face of the ladder brush the bot holds, its back when grabbed from behind), not away. - ApproachDescendingLadder() hands a bot already on the ladder straight to DescendLadder(), before its "within a jump of the bottom" check lets go of it at the top of a short ladder. - CNEOBotLadderClimb holds forward the same way, does not count the wait for the view as a stall, and leaves the aim of a claimed descent to the locomotion: two MANDATORY aims at different points hold each other back, since PlayerBody rejects a same-priority aim until the head has been steady for 0.3 s.
c6c506e to
94200b6
Compare
…00b6) into the research branch
From tmp-phase2-measure-sq 8fbeda4 (never pushed, never part of a PR). The research branch already carries the forensic POS/KILL/ROUND/STUCK/ LEAVEGROUND lines, the round/match-end markers and sv_neo_ghost_spawn_pos, so only these come over: NEO_FORENSIC_LADDER per TraverseLadder transition, the climb's "end" line, and neo_harness_ladder_pr_off (1 = release, claim and the on-ladder ascend check off, i.e. upstream behavior).
Never pushed, never part of a PR. Before this, neo_harness_ladder_pr_off 1 turned off only the release half of the squashed NeotokyoRebuild#2176 (94200b6), so the "off" arm still ran the descent half (ex-NeotokyoRebuild#2187): a hybrid. Now: pr_off 1 whole squash off = c930bb4's ladder code descent_off 1 descent half off, release half on (= 9f0805f) both 0 94200b6 Each gate's off path, per hunk of git diff c930bb4 94200b6 (Off = NEOHarnessLadderDescentOff() = pr_off || descent_off): NextBotPlayerLocomotion.cpp @-17 constants no runtime effect, not gated @-45 Reset() init state read only by HandleUnwantedLadder @-52 new helper bodies reached only via the gated call sites @-88 HandleUnwantedLadder pr_off: false && -> SetMoveType(WALK) @-115 ascend on-ladder ret pr_off: skipped -> upstream too-far check @-149 descend on-ladder ret Off: skipped -> upstream mount approach @-281 DescendLadder aim Off: goal reassigned to the #else line @-489 Approach() hold Off: if false -> upstream if/else chain NextBotPlayerLocomotion.h 2 hunks, declarations only neo_bot_ladder_approach.cpp @-86 hang point Off: offset skipped, targetPos = m_top @-99 look down Off: skipped, eye-level center look @-107 mount gate + @-132 (same block) Off: upstream if/else copied verbatim, then return Continue() as upstream neo_bot_ladder_approach.h HANG_CLEARANCE, constant only neo_bot_ladder_climb.cpp @-103 ClaimLadder at start pr_off: ClaimLadder returns at once @-218 re-claim pr_off: same no-op hold for view Off: bHoldForView false, no timer reset @-251 early jump-off Off: bBelowTopFloor true -> upstream if @-329 aim / forward Off: aim always; bHoldForView false -> PressForwardButton(0.1f) as upstream neo_bot_ladder_climb.h ClaimLadder declaration only On path (both 0): every gate is "!Off() && X", "X || Off()" or an "if ( Off() )" block, so it reduces to 94200b6's expression; the added calls read cvars only. The off mount block in the approach was diffed against c930bb4 after removing one tab: identical. Not built (game servers running); the orchestrator builds and smoke-tests.
Some ladders top out on a barrier the bot reaches from behind the ladder's plane: a roof parapet level with the ladder's top, a catwalk handrail just above it. The descent approach heads for the hang point in front of the face, so from the floor behind the barrier it walks into the barrier until its timeout; from on top it walks off forward, falls past the face moving away from it, and LadderMove() never sees a move into the ladder. A descent that starts behind the ladder's plane with the top more than a step over its floor now goes over the top. The bot faces the face, just below level, lines up behind the ladder, hops onto the barrier from a standstill and backs out over the drop, crouched so it leaves the top slowly. PlayerLocomotion::CatchLadderBelowTop() then presses forward and down on the tick the bot drops past the ladder's top: bot updates come ten times a second, too seldom for the engine's 2 u reach, so the press is made in Upkeep(). Other descents are unchanged.
CNEOBotLadderClimb::ResolveExitArea() took the first area from the path follower's current goal that is not a ladder. When that goal is still the area at the ladder's foot, a climb up resolves its exit below it, and the dismount's jump-off kicks the bot backwards off the top. The exit is now the first area after this ladder in the path.
Ladders 3, 8, 10, 13, 14 and 15 were climb only: their tops stand on the roofs' 38 u parapets (10, 13, 14, 15) or behind the catwalk's handrail (3, 8), and bots could not get onto them going down. NeotokyoRebuild/neo#2176 now mounts such descents over the top edge, so each ladder's top area lists it going down again. Ladder 14's and 15's tops move from the 11 u walkway strips against the north and south parapets, where no player hull fits, to the roof floor in front of each ladder. The strips are removed, and the parapet-top exits beside those ladders, reached only from the strips, are linked from the ramp next to them (a 43.5 u climb with a drop back). Re-analyzed in game.
Ladders 3, 8, 10, 13, 14 and 15 were climb only: their tops stand on the roofs' 38 u parapets (10, 13, 14, 15) or behind the catwalk's handrail (3, 8), and bots could not get onto them going down. NeotokyoRebuild/neo#2176 now mounts such descents over the top edge, so each ladder's top area lists it going down again. Ladder 14's and 15's tops move from the 11 u walkway strips against the north and south parapets, where no player hull fits, to the roof floor in front of each ladder. The strips are removed, and the parapet-top exits beside those ladders, reached only from the strips, are linked from the ramp next to them (a 43.5 u climb with a drop back). Re-analyzed in game.
NeotokyoRebuild#2176's commits 4a54c61 and 57d8040 (yard's parapet / handrail ladders, dusk and dawn ladder 4), ported onto the research branch.
A ladder that rises over a wall cap has its top area on the cap, shallower than a hull. The path follower's ledge search looks for the ledge's floor a full look-ahead past the wall, past a cap this thin, so a bot at the launch point of the climb onto it never jumped, and no bot descended that ladder (ntre_rise_ctg ladder 4). When the area after a climb is a ladder's top and is shallower than a hull along the approach, the follower now takes the mesh's climb link at the launch point, as it already does on an authoritative mesh. The rule meets 8 walk links across the ntre_ meshes: rise L4 and L5, dawn and dusk L2, saitama L11. Research measurement: rise L4 down, 6 runs on the edited mesh, 14 descents mounted and 14 reached the foot (0 before); dawn and dusk L2 and saitama L11, up and down, 3 runs each: every climb reached the ghost.
A climb that stops rising part-way is usually snagged on something at the ladder's edge rather than blocked outright. On ntre_subsurface_ctg ladder 1 the landing plate overlaps the ladder column by about 1 u, so a bot centered on the nav ladder heads into its underside at the same height every time, while one 2 u off center climbs past (169 of 198 mid-climb stall endings were there). On ntre_saitama_ctg ladder 2 bots catch the rim of the freight elevator hatch the same way. Before jumping off, a stalled climb now shimmies sideways, right then left, 0.3 s longer each time, up to three times, and gives up only if none of that restores the climb. Research measurement: subsurface ladder 1 up-climbs 34 -> 72 %; saitama ladder 2, 12 v 12 runs, rim snags 21 -> 0, start-solid traps in the slot below 207 -> 2, up-climbs 163 -> 177. Every-map screen, 4 ghost pins, 106 matches an arm: stuck events 6.22 -> 5.69 a match, falls off a ladder 1.25 -> 1.01, no new stuck spot.
A bot that spawns within reach of a ladder starts its approach during the pre-round freeze, and the freeze holds it in place. A descent over a level top reads that as being blocked behind the ladder's face, drops back to the ordinary approach and walks off the top past the face; the freeze also runs out an approach's timeout before the bot can move. The approach now waits while the freeze lasts. Measured on ntre_ridgeline_ctg ladder 1 (descents from the catwalk, 12 bots, 2 s freeze, 8 wide): down approaches started in the freeze mounted over the top 0 of 125 times before, 125 of 125 after (those started after the freeze: 120 of 126, then 121 of 127). On the no-drop twin of the map, over-top mounts 49 of 108 -> 98 of 99. Up approaches timing out in the freeze 60 of 145 -> 1 of 136. No fall deaths either way.
When a climb's way up is blocked, the climb teleports the bot in front of the ladder's face. Two spots it could pick hold the bot there for the rest of the round, still on the ladder with no velocity: - When the hull does not fit even clear of the face, the fallback spot half a hull in front of the nav ladder can be inside the ladder brush itself (ntre_rise_ctg ladder 6, whose brush stands in front of its nav line). - The spot keeps the old feet height, and on a slope the floor there can be a few units higher. A hull sunk into a displacement does not start solid, and a ducked bot unducking on the ladder then drops its feet by the crouch height into the floor (ntre_ridgeline_ctg ladder 1, a bot 17 u under the floor, the Assault crouch height). The hull now stands on the floor under the spot, and a spot still in solid is skipped, so the bot climbs from where it is. Measured (12 bots, 8 wide): rise ladder 6 up, 22 of 51 teleports took the fallback into the brush and 2 of 16 runs lost a bot there for the round; after, 23 teleports skipped, 0 of 24 runs, up climbs reaching the top 17 of 23 -> 24 of 25. ridgeline: 0 traps in 80 runs, up climbs completed 161 of 168.
The ladder approach waited while the round's pre-round freeze time was left, but that time is also counted at warmup, at the ready-up countdown and while the server idles, when nobody is frozen: a bot in a ladder approach then stood still for the length of the freeze. Test the bot's own NEO_FL_FREEZETIME flag instead, as the locomotion and the breakable-clearing behavior do. It is set exactly while the bot cannot move, so the wait is unchanged in the pre-round freeze.
HandleUnwantedLadder() looped over every nav ladder on every locomotion update a bot spent on a ladder it did not ask for, though only the adoption at the end of a half-second bout needs to know which ladder it is. Run the scan after the bout test instead. The caller lets go of the ladder whenever this returns false, before and after. The one difference: a contact the scan matches to no nav ladder now counts toward the bout, so a bot that slides from such a spot to a recorded ladder within one bout is taken over sooner.
IsLadderCapClimb() answered yes or no, and its caller then took the cap area from the path again, relying on the test having checked it. LadderCapAhead() returns the area itself, or NULL, so the launch uses the area the test examined. Say what the test measures (an axis-aligned depth along the approach) and how it differs from the dismount's IsNarrowLanding(), and that the repeated launch lines are kept apart from the authoritative branch on purpose. No behavior change.
Climbing() runs on every path update, and the climb onto a wall cap by the mesh link is gated only by the goal and the bot's range to the launch point: a bot that launched and fell back to that point jumped again on every landing until the path was planned again. Count the launches for the current path segment and, after three, leave the climb to the ordinary ledge search. The count starts over on a new goal segment and on every new path. The ladders that take this link most (rise L4 down, saitama L11 down, dawn and dusk L2) are the re-measure set for this change.
Since the dismount aims at a landing's near point, m_exitAreaCenter holds the exit area's center only for a narrow landing, and lines such as zDistToExit = currentZ - m_exitAreaCenter.z read as the center. Call it m_dismountPos. No behavior change.
Review edits, no behavior change: - The climb's IsNarrowLanding() and the path follower's LadderCapAhead() say what each measures: a landing under half a hull is crossed, not stood on, while a top up to a hull deep still holds the bot's center though the ledge search misses it. - The teleport's old spot is named as the unchecked fallback, with its gap as LADDER_PLACE_GAP, and the floor-trace comment reads in order. - Constants give their origin: NARROW_LANDING_HULLS (dawn and dusk L1, climbs ending at the exit 16 of 21 with it, 8 of 24 without), LANDING_INSET (half a hull), STALL_TIME (ridgeline L1, approach timeouts 15 -> 0), STALL_NUDGES (subsurface L1 up 34 -> 72 %, saitama L2 rim snags 21 -> 0), LADDER_MIN_DESCENT_RATE (a view 55 degrees below level) and LADDER_CATCH_OVERLAP (twice the grab reach); the descent test cites LadderMove(). - Approach() drops the empty hold branch: both of its moves now skip the hold, which presses nothing as before. - LadderEndArea() names the ladder end the climb leaves by, two traces wrap at the mask, and m_nNudges joins the constructor list.
A bot that climbs down a ladder whose foot hangs above the floor drops onto the floor crouched. If it then heads back up, the crouched hull fits under the ladder's bottom, so it never touches the ladder and the approach times out and restarts in place, for up to 40 s on sentinel_ctg's 75 u stub ladders. Going up such a ladder, release the crouch, and while still crouched step out to just in front of the face, where the bot stands and can press onto the ladder.
sunzenshen
left a comment
There was a problem hiding this comment.
For manual testing, the following script will stop any bots that are not under command (collected via the +use key, and moved via the +ping key), so one can take their time looking at the maps and ordering bots around different locations:
sv_neo_preround_freeze_time 2; mp_chattime 2;
sv_neo_bot_cmdr_enable 1;
sv_neo_bot_cmdr_debug_pause_uncommanded 1;
neo_ctg_round_timelimit 999;
Disclosure: This code was developed through an autonomous agentic testing loop, by overriding the spawn points of one of the teams onto nav areas near both the top and bottom of ladders, and setting their destination with a spawn overridden ghost objective near the opposite end of the ladder. The various edge cases in this PR were evaluated by checking hundreds of telemetry-injected logs of bots climbing or failing to climb ladders.
Previously the neo_bot_ladder_ implementations were developed also by me through LLM-assisted coding, but the wide spread of different permutations of ladders became too overwhelming to iterate through manual testing, hence this PR.
We probably want to put extra attention to the NextBot/ code files, since those are mixed with implementations original from the Source Engine SDK Base examples.
I wouldn't actually mind if someone else made an attempt at solving this problem with a fresh perspective, but I warn you that this path is paved towards madness.
Anyway, with this PR, bots are slightly more capable at climbing and descending ladders that have this kind of cursing overhang at the top:
sunmachine
left a comment
There was a problem hiding this comment.
Let me know if this is a real problem; I'm calling it an inconsistency between the intents as written but it may ultimately be OK?
04eb3b4 to
eab8ed8
Compare
The stall branch started each shimmy's timer at 0.3 s times the shimmy count, but the progress check that starts the next shimmy or the jump-off comes every STUCK_CHECK_INTERVAL (0.4 s) regardless, so the shimmies ran 0.3, 0.4 and 0.4 s, not the 0.3, 0.6 and 0.9 s the comment described. sunmachine's review of this PR found the mismatch. Start every shimmy at the check interval instead, so the code says what it does, and drop STALL_NUDGE_TIME. The measured behavior is unchanged: the numbers cited for this change were taken with the 0.3 / 0.4 / 0.4 s shimmies. Measured against the growing version (the stuck check following the shimmy timer, 0.3 / 0.6 / 0.9 s) and the code as it was, Release builds on the rebased branch: subsurface ladder 1 up and saitama ladder 2 down, 8 runs an arm, and 20 ladder directions at the snag sites (dawn, dusk, rise, saitama, sentinel), 3 runs an arm, 312 matches. A second or third shimmy happened in under 5 % of stalls; where it happens most, the top of rise ladder 3, every arm ends in the jump-off. Climbs finished, near-ladder stuck events and falls did not differ between arms.
neo#2176 merged at 092203f with every follow-up of the research branch's ladder PR. The ladder files (NextBotPlayerLocomotion, neo_bot_ladder_approach, neo_bot_ladder_climb) are a 3-way merge: research tip b808069 over the PR state f710510, onto the merged code. Research arms the merged PR now does by default are removed: neo_bot_ladder_exit_closest (+ the thin-exit rule), _teleport_reach, _teleport_view, _flush_top, _catch_above_top, _stall_nudge, and the dead patch 109 cvar neo_bot_precise_move_direct. The TEMP NeotokyoRebuild#2176 A/B switch (neo_harness_ladder_pr_off / _descent_off) is removed: it chose pre-NeotokyoRebuild#2176 code that no longer exists. Patch 75 (lateral hold) and 77 (teleport grab) stay layered on the merged nudge and teleport. Folds the ladder-file hunks of these research commits: 557d58b Ladder research: claim ladders for PlayerLocomotion, mount hop, dismount exit, telemetry 5173fde Ladder research: shimmy past a snag before abandoning a climb 71481eb Ladder research: mount side-landing ladders from in front when going down 3cf88a4 Ladder research: side-landing descent walks off the landing before pushing in 09262ba Ladder research: finish a ladder past halfway instead of jumping off at a threat f0e786a Ladder research: hold a climbing bot inside the ladder's width d0c5704 Ladder research: make the climb's MoveUp / MoveDown presses reach the user command 02b99aa Ladder research: a claimed ladder the bot is already on is never 'too far below' e88b779 Ladder research: keep a teleported climb on the ladder d21eba5 Ladder research: aim the ladder approach inside its slot 5807c4f Ladder research: drop off a descent only below the top floor 691ffe0 Ladder research: press the climb's direction at the mount 8cb511d Ladder research: a bot already on the ladder starts to climb c4264ff Ladder research: mount every descent from the front 20b78da TEMP: ladder transition log and the NeotokyoRebuild#2176 A/B switch 8f6572a TEMP: gate all of NeotokyoRebuild#2176 behind the ladder switch 687a619 Fix the research port: a stray brace and the patch 109 twin 5e7595a Research: turn the ladder stall nudge back on
…embers) Both this branch and NeotokyoRebuild#2176 add constants, constructor lines and header members next to the same places in NextBotPathFollow; keep both.
…embers) Both this branch and NeotokyoRebuild#2176 add constants, constructor lines and header members next to the same places in NextBotPathFollow; keep both.
Description
Bots reach, climb and leave ladders without hanging, bouncing back or falling off
In bot matches on the
ntre_maps, ladders were the most repeated movement failure. Bots whose path only passed a ladder hung on it. Bots that started down a ladder from its top ended back on top and tried again (95 % of the descents ofntre_saitama_ctg's elevator hatch). On a ladder whose brush is turned off the world axes (ntre_ridgeline_ctgladder 1) bots were held in the brush at its foot for the rest of the round, and bots reaching the top of some ladders were kicked back off it. In the ladder tests below, master finishes 52 % of the climbs bots start and this branch 87 %; in normal 30-bot matches on the same maps, climbs up finish 57 % -> 83 % and descents 35 % -> 81 %.The changes are in
PlayerLocomotion's ladder states (NextBotPlayerLocomotion.{h,cpp}), the two ladder behaviors (neo_bot_ladder_approach.{h,cpp},neo_bot_ladder_climb.{h,cpp}), one block inPathFollower::Climbing()(NextBotPathFollow.{h,cpp}) and aCNavLadder::GetTopArea()helper (nav_ladder.h) that the climb and the locomotion both use to find a ladder's top area, sincenav_generateputs it in whichever top slot its landing is in, often not the forward one. Engine movement (CGameMovement) is not changed, and no convar switches any of it off: with a fix disabled a bot would hit the original defect. Climbing up a ladder the bot meant to climb, from the foot with nothing in the way, is unchanged.Dependencies and status
Needs (code PRs this one depends on):
Helped by:
Pending before review:
Navmesh PRs that rely on this
Needs this PR: NeotokyoRebuild/neoAssets#140 (
ntre_terminal_jgr), #143 (ntre_ridgeline_ctg), #149 (ntre_tarmac_ctg), #154 (ntre_yard_jgr). Measured to help: #146 (ntre_saitama_ctg), #148 (ntre_subsurface_ctg), #150 (ntre_dusk_ctg), #151 (ntre_dawn_ctg), #157 (ntre_apparatus_dm), and the meshes measured only on a build that carries this PR: #160 (ntre_rise_ctg), #161 (ntre_threadplate_ctg), #162 (ntre_sentinel_ctg), #163 (ntre_ghost_ctg), #164 (ntre_skyline_ctg), #165 (ntre_sentinel_jgr), #166 (ntre_vtol_ctg); per-mesh ladder results are under "Results".What this PR does
In the order a bot meets them: walking past a ladder, the approach, the mount, the climb, the dismount. Numbers are master against this branch unless a line says otherwise; the tests are described under "Results".
Walking past a ladder
NextBotPlayerLocomotion.cpp).CGameMovement::LadderMove()puts a player on any ladder their wish direction points at, andPlayerLocomotion::TraverseLadder()'sNO_LADDERbranch drops the move type back to walking whenever the bot did not ask for a climb. A path that only brushes past a ladder leaves the two toggling every tick: the bot hangs there, and since it never stops moving, no stuck event fires. Once the contact has lasted half a second, the locomotion now adopts the ladder and leaves it by the nearer end. Unwanted ladder time 11.0 -> 7.8 s a match (x0.71 [0.58, 0.86], 216 matches on 7 maps).The approach
neo_bot_ladder_approach.cpp). A bot that spawns within reach of a ladder starts its approach during the freeze, which holds it still: a descent over a level top (change 5) reads that as being blocked behind the ladder, and an approach's 3 s timeout runs out before the bot can move. The approach now waits while the bot's own freeze flag is set (NEO_FL_FREEZETIME, as the bot locomotion and the breakable-clearing behavior test it). Ridgeline ladder 1: descents begun in the freeze that mount the ladder 0 of 125 -> 125 of 125; climbs up begun in the freeze that time out 60 of 145 -> 1 of 136 (this branch without and with the change). Only the approach waits because it is the one ladder step with a stall timer and a timeout; the climb starts only from the approach, so it cannot begin in the freeze.neo_bot_ladder_approach.{h,cpp}). The engine grabs a bot as soon as its move points at the ladder, often while it still stands on the landing or on the brush's top, and from there no press takes it down. The approach now heads for where a descending bot hangs, half a hull plus 2 u in front of the face at the ladder's top, looks down the ladder, and mounts only once the bot is below the top. With changes 10 and 15, saitama's hatch descents ending back on the car roof went 95 % -> 5 %; across every ladder in the ladder tests, descents finished 43 of 107 -> 164 of 201 (normal play: 35 % -> 81 %).neo_bot_ladder_approach.{h,cpp},NextBotPlayerLocomotion.{h,cpp}). Some ladders top out behind a roof parapet or a catwalk handrail (ntre_yard_jgrladders 3, 4, 5, 8, 9, 10, 13, 14, 15). From behind the barrier the approach walked into it until its timeout; from on top the bot walked off forward and fell past the face. A descent that starts behind the ladder's plane with the top more than a step above its floor now hops onto the barrier and backs out over the drop, crouched.PlayerLocomotion::CatchLadderBelowTop()then presses forward and down on the tick the bot is in the air below the top: bot updates come ten times a second, too seldom for the engine's 2 u grab reach, so the press is made inUpkeep(), every tick, while a catch is armed. Yard's nine barrier ladders going down, three runs each: master finished 10 of 27 descents, this branch 117 of 124.neo_bot_ladder_approach.{h,cpp},NextBotPlayerLocomotion.cpp). Ridgeline ladder 1's top is level with the catwalk, so a bot behind it stands on the brush's own top and walks off. Change 4's mount now covers it too; a bot held behind the face for 0.6 s goes back to the ordinary approach, and the catch presses from a step above the nav ladder's top, since the brush often stands above it (13 u there). Ridgeline, with the 140 u drop beside the ladder removed so that descents must use it: descents mounted over the top, 0 of 171 down approaches on master, 97 of 98 on this branch.neo_bot_ladder_approach.{h,cpp}). Some ladders end above open floor (ntre_sentinel_ctgladders 3 and 6, whose foot hangs 54 u up). A bot that climbs down drops the rest of the way and lands crouched, since the bot locomotion presses crouch in the air. If it then heads back up, the approach walks it to the ladder's bottom, under the brush, where the crouched hull fits and cannot stand: it never touches the ladder, and the approach times out and starts again every 3 s, for up to 40 s. Master has the same stall; it shows more on this branch because more descents finish there. Going up a ladder whose bottom is more than a step above the bot's feet, the approach now releases the crouch and, while the bot is still crouched, walks it to just in front of the face, where it stands and mounts as usual. Sentinel ladders 3 and 6, 12 ladder runs a build: near-ladder OnStuck 123 a run on master, 79 on this branch without the change, 46 with it; stalls under the foot 14, 9 and 0. Normal play on sentinel, 18 matches a build: near-ladder stuck 16.6 -> 5.3 a match (this branch without and with the change; master 3.8 and 10.2 in two batches).The mount
neo_bot_ladder_climb.cpp,NextBotPlayerLocomotion.cpp).CNEOBotLadderClimbput the bot on a ladder without tellingPlayerLocomotion, so change 1 would treat every behavior climb as unwanted and take it over by the nearer end: at the foot, the bottom, reversing the climb. The climb now callsClimbLadder()/DescendLadder()at the mount, claims the ladder again when the engine reports the bot off it for a tick mid-climb, and a claim restarts change 1's half-second wait. Behavior climbs finished from a teleported start 78 % -> 88 % (same 216 matches as change 1).neo_bot_ladder_climb.cpp). When a climb starts with its way up blocked,OnStart()places the bot in front of the face. The hull is axis-aligned, so on a turned ladder a corner of it went into the brush; on a slope the kept feet height could be under the floor; and where the hull did not fit, the fallback spot half a hull off the nav ladder could be inside the brush (ntre_rise_ctgladder 6, whose brush stands in front of its nav line). The placement now clears the hull's corners and slides in until it touches the face, stands the hull on the floor under the spot, and skips a spot still in solid, so the bot climbs from where it is. Ridgeline ladder 1, runs with a bot held at the foot for the rest of the round: 80 of 80 on master, 0 of 80. Rise ladder 6 up, runs that lost a bot inside the brush: 2 of 16 -> 0 of 24 (this branch without and with the solid check).neo_bot_ladder_climb.cpp). The teleport set the body's angles but not the view (SnapEyeAngles), so the first forward press climbed off the face's normal and slid the bot off the side of a narrow ladder. Ridgeline ladder 1, teleported climbs that ended back at the foot: 32 of 102 -> 19 of 92 (this branch without and with the change, on an earlier test build).The climb
NextBotPlayerLocomotion.{h,cpp},neo_bot_ladder_climb.cpp).LadderMove()turns the part of a move that pushes into the face into climbing up: facing the ladder, forward goes down only while the view points more than about 45 degrees down. Descents pressed forward before the view was down and climbed back off the top.PlayerLocomotion::IsForwardDownLadder()says whether a forward press moves the bot down, from the view and the face it holds;Approach()and the climb press nothing on a descent until it does, andDescendLadder()looks down into the face. With changes 3 and 15, descents ending back on top on eight other ladder maps: 112 of 137 -> 0 of 32 (the measurement of parts 4-6 below).neo_bot_ladder_climb.{h,cpp}). A climb that stops rising part-way is usually caught on something at the ladder's edge: onntre_subsurface_ctgladder 1 the landing plate overlaps the ladder by about 1 u, and on saitama ladder 2 bots catch the hatch rim. A stalled climb now shimmies sideways, right then left, three times of 0.4 s each (one progress-check interval), before it jumps off. Subsurface ladder 1 climbs up 34 -> 72 % finished, saitama ladder 2 rim snags 21 -> 0 (measured on an earlier test build carrying the same change). Climbs abandoned with "Got stuck on something climbing" across every ladder tested: 79 on master, 5. The first version of this change meant the shimmies to grow (0.3, 0.6, 0.9 s) but its progress check cut each one at 0.4 s, which sunmachine's review found; the numbers above were measured with the 0.4 s shimmies, and a 312-match comparison on this branch of the as-is code, the growing version and three equal 0.4 s shimmies found no difference in climbs finished, stuck events or falls (a second or third shimmy happens in under 5 % of stalls), so the code now says what was measured.NextBotPathFollow.{h,cpp}). This is the one change outside the ladder code. To go down a ladder whose top area is a wall cap shallower than a hull, the bot first climbs onto the cap, and the path follower's ledge search looks for the ledge's floor a full look-ahead past the wall, past a cap that thin: the bot never jumped, and no bot descendedntre_rise_ctg's ladder 4. When the area after aCLIMB_UPis a ladder's top and shallower than a hull along the approach,Climbing()now takes the mesh's climb link at the launch point, as its authoritative-mesh branch already does, at most three times a path segment. The rule meets 8 walk links on thentre_meshes (rise ladders 4 and 5, dawn and dusk ladder 2, saitama ladder 11). Rise ladder 4 down, on rise's navmesh with its cap edit: no descent before, 14 of 14 reaching the foot after (measured on an earlier test build carrying the same change). With the three-launch bound, the ladders that take this link most, three runs each with the bound: dawn and dusk ladder 2 up finished 26 of 26 and 22 of 22 climbs (master 7 of 17 and 10 of 20, with 8 falls from the top each), saitama ladder 11 down 3 of 4 (master 0 of 1); rise ladder 4 down is not used by either build on Animations networking #160's current navmesh, which lacks the cap edit.The dismount
neo_bot_ladder_climb.cpp).ResolveExitArea()took the first non-ladder area from the path follower's current goal. When that goal was still the area at the ladder's foot, a climb up resolved its exit below it and the dismount kicked the bot backwards off the top:ntre_dusk_ctg/ntre_dawn_ctgladder 4 did this 71 times in 627 climbs, 4 in 604 with the change (part 8 below). The exit is now the area the ladder lands on, from the path after this ladder. Dawn and dusk ladder 4 up on this branch: 18 of 18 finished each.neo_bot_ladder_climb.{h,cpp}). The dismount aimed at the exit area's center, which on a long, narrow landing lies off to one side: on ridgeline ladder 1 it is the middle of the catwalk, and the kick toward it carried bots off the 30 u slab. The dismount now aims half a hull inside the landing's point nearest the ladder and finishes once the bot stands on the landing. A landing under half a hull deep is a wall cap the climb crosses (dawn and dusk ladder 1, 7 u deep), so it keeps the center aim. Ridgeline ladder 1, 16 runs each, measured on an earlier test build carrying the same change: climbs up ending on the catwalk 58 of 111 -> 69 of 94, falls from the top 11 -> 0, near-ladder OnStuck 67 -> 12. Dawn and dusk ladder 1 up: master 0 of 1 and 0 of 1 started, this branch 12 of 20 and 9 of 15.neo_bot_ladder_climb.cpp). The climb jumps off once the exit is within a safe drop below, which on a short ladder is at the mount, and where the top floor overhangs the exit (saitama's car roof) that lands the bot back on top. Going down, the jump-off now waits until the bot is a standing height below the ladder's top. Part of saitama's 95 % -> 5 % (change 3).How to test
Build this branch, install the map's navmesh from its neoAssets PR into
maps/nav/, thenmap <map>,neo_bot_quota 12and join spectator.sv_cheats 1; nb_debug behaviorprints each ladder step a bot takes ("Approaching ladder down", "Mounting ladder over its top", "Reached the ground, ending climb", "Got stuck on something climbing");nb_debug pathalso prints the climb's teleport.sv_neo_wep_dmg_modifier 0.1keeps bots alive near a ladder longer, andnav_edit 1draws the nav ladders. A build without this PR on the same navmesh shows master.ntre_ridgeline_ctgladder 1 (fadeout plays after death #143), at (1086, 3077), foot z -35 to the catwalk at 112, on a brush turned 60 degrees. Master: bots held in the brush at the foot. This branch: climbs that end on the catwalk, and descents over the level top.ntre_yard_jgrparapet and handrail ladders ([DOC] Document how to enable menus from sourcemod plugin #154): roof ladders 10 (-660, -640), 13 (-248, 236), 14 (311, 1398) and 15 (310, -1886), top z 249; catwalk ladders 3 (640, -553) and 8 (640, 66), top z 111. Descents over the barrier.ntre_saitama_ctgelevator hatch (Spectators able to see player outlines #146): ladders 4 (88, 2122) and 13 (88, 2040), hatch top z 446-466. Descents that reach the bottom instead of the car roof; the crowding at the hatch top (under "Risk").ntre_terminal_jgrladder 8 (Actors dismember when heavily damaged #140), at (-1568, 847), z 20 to 80: bots walking past and climbing.ntre_tarmac_ctgladder 1 (The lean and aimhold toggles should work on dedicated servers #149), the 32 u stub on a pillar at (1093, 1239), z -452 to -420: descents past the rail.ntre_rise_ctgladder 6 (Animations networking #160), at (-118, -951), z -290 to -208: climbs that start with the teleport.Risk
Moderate. Ladder handling changes on every map: every behavior climb now runs through
PlayerLocomotion's ladder states, every descent mounts from the hang point, and changes 4 and 5 act only on descents that start behind the ladder's plane. Change 6 acts only on a bot going up a ladder whose foot is more than a step above its floor, and change 12 only where the area after a climb is a ladder's top shallower than a hull.What was checked: every nav ladder on the 22
ntre_maps with a draft navmesh, 168 of 169 (apparatus ladder 1 has no top area), both directions, one run each on master and this branch; the 98 ladder directions where either build showed a fault, three more runs each; and ridgeline ladder 1 in 80 runs a build. These ran on the build before the hanging-foot fix (change 6); sentinel_ctg ladders 3 and 6 (12 runs a build) and ridgeline ladder 1 (64 runs a build) were run again with it. Every run reached match end with no crash. A Debug build of this branch before change 6 ran one 30-bot match each on ridgeline, saitama, tarmac, rise, yard and terminal plus 16 of the ridgeline ladder runs (22 matches): all reached match end, with zero assertion failures and no crash. The same 22 Debug matches ran again on the branch with change 6: all reached match end, zero assertion failures, no crash. Pass marks, this branch against master: no run with a bot held still on a ladder; no repeated bounce loop (a bot mounting one ladder 5 times without finishing) where master has none; no fall death within 3 s of leaving a ladder; no ladder direction master finishes that this branch never finishes. Two directions missed the last mark on the build before change 6; with it only the first still does:ntre_sentinel_jgrladder 7 down. Master used it in 2 of 3 runs (3 of 5 finished); this branch's bots never choose it (no approach, no timeout). Route choice, not a failed climb.One spot was worse on this branch in normal play ("Normal play" under Results), and change 6 fixes it:
ntre_sentinel_ctg. Near-ladder stuck rose 69 -> 298 in 18 matches a build (3.8 -> 16.6 a match), with no deaths and no trapped bot. It was the stall at ladders 3 and 6's hanging feet (change 6); rerun with the change, this branch has 5.3 a match, master 3.8 and 10.2 in two batches.Known, not caused by this PR:
ntre_transit_ctgladder 2 down (13 % / 25 %),ntre_apparatus_dmladder 13 (climbs under a slab). Rise ladder 3 up, yard ladder 11 down and rise ladder 4 down are used by neither build; rise ladder 4 down needs Animations networking #160's cap edit (change 12), which that PR's current navmesh lacks.trigger_hurtafter walking off a catwalk they climbed to, master 19 / this branch 13 in the all-ladder runs and 5 / 15 in the reruns, with more finished climbs on this branch in both.ntre_sentinel_ctgladders 3 and 6: a bot that lands under the ladder's brush crouched while following a path or fighting, not approaching the ladder, can stay wedged there (1 bot in 18 matches on this branch, 5 on master).ntre_sentinel_ctgladder 3: a bot standing at the face in its approach and not grabbed by the ladder for 30-42 s (1 bot on each build).CNavLadder::SetDir()takes from the brush face, so a turned ladder is climbed the same way as a square one.Cost
Per bot. Worst case on a server tick: a virtual
Upkeep()call and a null check for every bot; while a catch is armed (up to 0.5 s after each over-the-top approach update) adynamic_cast, anEyeVectors, aGetPosAtHeightand five dot products, no trace. Worst case on a bot update (10 a second): one hull trace while a bot stands behind a barrier lined up for change 4, plus twoIsForwardDownLadder()calls on a descent (anEyeVectorsand two dot products each), plus four float compares in the approach (and, while a bot going up a hanging-foot ladder is still crouched, one point in front of the face). Per event:HandleUnwantedLadder()GetPosAtHeighteach in the height windowClimbLadder()/DescendLadder()CNEOBotLadderClimb::OnStart()ResolveExitArea()Update()Minof two sizesPathFollower::Climbing()NextSegment, a ladder count, the area's center and size, a counter; at launch a closest point and the existingClimbUpToLedgeCLIMB_UPgoal; launch at most three times a segmentAll of it is small beside the stock
Climbing()ledge search, which runs several hull traces per update.Results
How the ladder tests run. A run is one 2-minute round with 12 bots (6 a team, 2 of each class): one team spawns at one end of a ladder and the ghost lies at the other, so the team has to use the ladder; weapon damage is a tenth and the pre-round freeze 2 s. Unless a line says otherwise, this branch's numbers are from the build before the hanging-foot fix (change 6), which acts only on a bot going up a ladder whose foot is more than a step above its floor. Both builds are Release builds with the same test-only logging (positions twice a second, stuck events, behavior changes); master is upstream at #2205, which touches none of this PR's files. Each map runs its navmesh at its neoAssets PR's head. A climb is "finished" when the bot leaves the ladder at the far end and stands there for a second; "near-ladder OnStuck" counts
OnStuckevents within 300 u of the ladder in the round.ntre_ridgeline_ctgladder 1, #143's navmesh. Climbs up: the team at the foot, the ghost on the catwalk; down: the reverse. The copy without the drop is a test-only copy of the navmesh with the 140 u drop link beside the ladder cut, so descents must use the ladder.Rerun with change 6 (32 up and 32 down runs), this branch held a bot in 0 of 64 runs, finished 174 of 182 climbs up and 95 of 122 descents, with 0 fall deaths and at most 5 near-ladder OnStuck in a run. Master's column was measured in an earlier batch of the same scenarios. With the drop link kept, about 4 in 10 bots still take the drop, which costs no health; on the copy without it, 95 of 96 first descents used the ladder.
Every ladder on the
ntre_maps (ladder runs, not normal play). All 336 directions of 168 ladders, one run each, pooled:These ran before changes 2 and 8 took their final form. The 98 directions where either build showed a fault (a hold, a bounce loop, a failed direction, high OnStuck), plus the ladders this PR names, were then run three more times each on this branch before its last review edits (which change no behavior at these ladders; the ladders change 12's bound touches were rerun after it, above), master's three runs being the same build. On that set master finishes 410 of 959 climbs (43 %), this branch 921 of 1136 (81 %); of this branch's 349 descent mounts, 63 are a bot brushing the ladder's top lip or foot while walking, and of the 286 real descents 240 finish (84 %, master 32 of 129).
The 98 watch directions ran three more times on the branch with change 6: climbs up finished 83.2 % of starts, descents 77.3 % (85.6 % of the descents that reached the ladder), 81.4 % overall, with a finish in 83 directions against master's 69 and this branch's 81 before the fix. No run with a bot held still on a ladder, no bounce loop where master has none, no fall death within 3 s of a ladder. Rise ladder 6 up had no held bot in 66 climbs. saitama ladder 13 down still misses (crowding at the hatch top);
ntre_sentinel_jgrladder 7 down no longer does. Sentinel ladders 3 and 6 up finished 25 of 27 and 37 of 38 (before the fix 18 of 19 and 25 of 26).Normal play. Bot matches with 30 bots at default weapon damage, every pinned ghost or juggernaut spawn of each ladder map twice, one round, 8 matches at a time; master and this branch with the same spawns and the same machine load. These are ordinary matches, not ladder runs: the bots go where the game sends them. The table and the pooled line below predate change 6; sentinel_ctg was rerun with it (below). Stuck and kills are per match, stuck counted within 300 u of a ladder; fall deaths are totals (a death by the world within 300 u of a ladder). The last column is ghost captures of the CTG rounds; for the juggernaut maps it is the rounds ending at the score limit and at the time limit, and for the DM and TDM maps the matches ending at the score limit.
All 22 maps, 344 matches a build: stuck 4666 / 3705 (13.6 / 10.8 a match), near-ladder stuck 2069 / 1041 (6.0 / 3.0 a match), kills 11742 / 11767, CTG captures 25 / 27 of 306 rounds, fall deaths within 3 s of leaving a ladder 0 / 0 (the 1 and 2 world deaths near a ladder, on rogue and bullet, were bots walking on flat floor). Climbs up finished 1715 of 3007 (57.0 %) / 2329 of 2823 (82.5 %), descents 211 of 598 (35.3 %) / 756 of 937 (80.7 %). Runs with a bot standing still within 64 u of a ladder for 10 s: 17 / 6, 6 of master's 17 on a ladder, none of this branch's; bounce loops 39 / 2.
At the places the ladder runs singled out: ridgeline ladder 1's foot went 684 -> 0 near-ladder stuck events in 12 matches (master wedged bots in 3 of the 12), with no new spot within 300 u on this branch. Terminal ladder 8: mounts that returned to the foot 425 -> 0 over 10 rounds, bounce loops 32 -> 0, and 194 of this branch's 221 mounts finished. Tarmac ladder 1: near-ladder stuck 92 -> 10. Yard's barrier ladders going down: 1 mount on master, 17 on this branch, 15 of them reaching the bottom. Saitama ladder 13 down, the direction that missed in the ladder runs, did not come up: no bot mounted it in 34 matches on either build, so the crowding at the hatch top did not show. The one large rise is
ntre_sentinel_ctg(the next largest are subsurface 19 -> 34 and transit 10 -> 23 events): near-ladder stuck 69 -> 298 (3.8 -> 16.6 a match), with no deaths and no trapped bot, and 3-4 times more descents finished on its ladders 3 and 6. Bots that came down there and then headed back up stalled under the ladders' hanging feet (change 6). Rerun with change 6, 18 matches a build: near-ladder stuck 10.2 a match on master, 5.3 on this branch, 1 event within 300 u of ladder 3 (master 114). Master's own rate moves 3.8 -> 10.2 between the two batches, so at this size it is noisy.Approaches tried and dropped
CGameMovement): clearedntre_skyline_ctgladder 1's approach timeouts (2.4 -> 0), but it changes movement for every player, humans included; left for its own PR.Measurements of the earlier parts against the upstream of their time
These were measured while the PR was built up, each part against the code before it, on upstream as it was then (some with the SeekWeapon PR #2186, merged since), in 30-bot matches at default weapon damage, 8 at a time, with a test-only switch per part. Parts 1-3 here are changes 1 and 7 above; parts 4-6 are changes 3, 10 and 15; part 7 is change 4, part 8 change 13, part 9 change 5, part 10 changes 8 and 9. "Stuck" is
OnStuckplusFAIL_STUCKmove failures without the round-start spawn crowd; intervals are 95 % bootstrap intervals of the paired ratio.Parts 1-3, on the navmeshes then in progress; subsurface, sentinel CTG, sentinel JGR, saitama, rise (6 pinned spawns x 3), bullet TDM and apparatus DM (12 each). 216 matches, 108 a build. Ladder time is sampled every 0.5 s and split into a behavior climb, a locomotion climb, or neither (an unwanted ladder).
The stuck total is flat; each map's rise is one bot in one or two matches repeating a stuck event 40+ times at one spot (capped at 20 events a bot: x1.02 [0.89, 1.18]). Part 1 alone on all 29 maps, 213 matches a build: unwanted ladder time 10.2 -> 6.8 s.
Parts 4-6 against parts 1-3, 384 matches, each paired with the other build on the same map, spawn and repeat. A descent "ends back on top" when the bot is within 24 u of the ladder's top when the climb ends.
ntre_saitama_ctg, 68 pairs (every ghost spawn twice, on master's navmesh and on #146's):Parts 1-3 alone more than doubled the tower's stuck events (x0.41 [0.23, 0.75] with their behavior switched off, 34 pairs): a bot left on the roof was adopted by a tower ladder and climbed on into its walls. That is why both halves are one PR.
Other ladder maps, 72 pairs (transit, tarmac, subsurface, marketa, rogue, ghost CTG, yard JGR, bullet TDM): descents ending back on top 82 % (112 of 137) -> 0 % (0 of 32); stuck x0.92 [0.72, 1.16]; kills x0.99 [0.94, 1.03]; fall and hurt-volume deaths 9 -> 10 (rogue's canal, bullet's track). Transit ladder 2's descent began to work (94 % back on top -> 0 %).
Parts 7-8 against parts 1-6, 420 matches.
ntre_yard_jgr, the juggernaut pinned at each of its 3 spawns, 10 matches each, on a navmesh that keeps the barrier ladders climb-only and on #154's, which links them going down (120 matches): with parts 1-8, 77 of 84 finished descents reached the bottom (7 dropped off, 0 timed out); with parts 1-6 on the linked navmesh, 1 of 184 (109 timed out against the parapet or rail). Stuck x1.01 [0.79, 1.26] against the climb-only navmesh; kills a minute x1.00 [0.96, 1.05]; fall deaths 0.ntre_dusk_ctgandntre_dawn_ctg, 5 ghost spawns x 3 each, 60 matches: ladder 4 climbs kicked backwards off the top 71 of 627 -> 4 of 604; stuck at ladder 4's top 94 -> 30; stuck x0.87 [0.67, 1.11]; kills x0.99 [0.96, 1.02].Other ladder maps, 44 pairs: stuck x0.96 [0.71, 1.31], kills x1.08 [0.97, 1.20], fall deaths 0 / 0.
Parts 9-10 on ridgeline with #143's navmesh, 12-bot ladder runs: bot held in the brush at the foot 16 of 16 on upstream, 7 of 32 with part 9, 0 of 32 with part 10. Part 9 with bots spawned on top, 8 runs a build: descents mounted from the top 0 -> 11, descent approach timeouts 15 -> 0. Part 10 on rise ladder 4 down, 12 runs a build: every run reached the goal in both, near-ladder stuck within the run-to-run spread (median 741 against 576, Mann-Whitney p = 0.26).
Toolchain
Linked Issues