Skip to content

Bots climb down ladders instead of bouncing off the top - #2187

Closed
sunzenshen wants to merge 6 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-ladder-descent
Closed

sunzenshen wants to merge 6 commits into
NeotokyoRebuild:masterfrom
sunzenshen:bot-ladder-descent

Conversation

@sunzenshen

@sunzenshen sunzenshen commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Description

Bots climb down ladders instead of bouncing off the top

Depends on #2176 ("Stop bots fighting the engine over unwanted ladders") and is stacked on its branch; review the
last three commits. With #2176 the climb behavior claims the ladder for PlayerLocomotion, whose descend states then
steer every descent the behavior starts. This PR makes those states, and the two ladder behaviors around them, take a
bot down.

A bot that starts down a ladder from its top usually ends up back on the floor it started from, and tries again.
ntre_saitama_ctg's elevator hatch shows it most: 95 % of the descents of the ladder in the hatch end back on the car
roof, about 25 attempts a match, with or without #2176 (94 % on upstream's ladder code). The bots then walk a
floor-level path on the roof into its walls, the map's largest stuck spot. Three things put them back on top:

  1. Forward climbs up. CGameMovement::LadderMove() turns the part of a move that pushes into the ladder's face
    into climbing up, and moves the player along the face with the rest. Facing the ladder, forward goes down only
    while the view points more than 45 degrees down; with the view level it climbs. Every descent pressed forward before
    the view was down: the approach's last forward press stays held until the next bot update, after the grab;
    ApproachDescendingLadder() kept steering towards its mount point after the bot was already on the ladder; and
    Approach() presses forward on any ladder the bot is descending. DescendLadder() aimed away from the face while
    the climb behavior aimed into it, both at MANDATORY priority, and PlayerBody refuses a same-priority aim until the
    head has been steady for 0.3 s, so the view was slow to come round at all. Near the top, a few ticks of climbing
    lift the bot past the ladder's top and off it.
  2. Grabbed while still standing at the top. The engine puts a bot on the ladder as soon as its move points at it,
    on the landing or on top of the ladder brush, which is solid and often stands a step proud of the floor (the hatch
    ladder's top is 14 u over the roof). From there no press takes the bot down: into the face pushes into what it
    stands on, and a move away from the face on a floor makes LadderMove() push it off the ladder.
  3. The early jump-off lands it back on top. CNEOBotLadderClimb jumps off as soon as the exit area is within a
    safe drop below, which on a short ladder is at the mount, and kicks the bot towards the exit area's center. Where
    the top floor overhangs the exit (the roof over the car), that lands it back on the top floor.

Changes, in three commits

  1. Press forward down a ladder only when it descends (NextBotPlayerLocomotion.{h,cpp}, neo_bot_ladder_climb.cpp).
    PlayerLocomotion::IsForwardDownLadder() says whether a forward press moves the bot down, from the view and the face
    it holds (the ladder brush's back face when it grabbed the ladder from behind); a press facing away from the face does
    not count. Approach() presses nothing on a descent until it does, and DescendLadder() looks down into the face
    rather than away from it. ApproachDescendingLadder() passes 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. The
    climb behavior gates its own forward press 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.
  2. Mount a descent from in front of the ladder (neo_bot_ladder_approach.{h,cpp}). The approach 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 rather
    than level at its middle, and mounts only once the bot is off the top (below the ladder's top, and no longer standing
    within a step of it). It lines up on the ladder's normal line only from in front of the face, where coming in at an
    angle leaves the bot on the floor beside the drop, stepping up onto the ladder's top; from beside or behind the
    ladder it walks straight on, over the top.
  3. Keep a descent's early jump-off below the top (neo_bot_ladder_climb.cpp). Going down, the jump-off waits until
    the bot is a standing height below the ladder's top.

No convar switches any of this off. Climbing up is unchanged.

Evidence

Upstream at #2168 plus #2176 and the draft SeekWeapon PR (#2186), with harness logging added and a test-only switch
for this change; both arms run the same server build. 30 bots, default weapon damage, 8 matches at a time; each run is
paired with a run of the other arm on the same map, ghost spawn and repeat, back to back. CTG: 2 rounds of 3 minutes with
the ghost pinned at one ghost spawn; TDM and JGR: one 5-minute round. All 384 matches below reached match end with 0
asserts and no crash. "Stuck" is OnStuck plus FAIL_STUCK move failures, without the round-start spawn crowd and
weapon scavenging; intervals are 95 % bootstrap intervals of the paired ratio. A descent is a climb down the behavior
starts; it "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 the reworked one from the
companion neoAssets PR (NeotokyoRebuild/neoAssets#146):

a match #2176 + this PR paired
hatch ladder descents 25.1 2.9
... ending back on the roof 95 % (1630 of 1706) 5 % (10 of 200)
stuck 41.2 30.2 x0.71 [0.60, 0.87], 44 down / 23 up
stuck in the ladder tower 19.2 6.5 x0.35 [0.24, 0.53]
stuck elsewhere 22.0 23.7 x1.11 [0.93, 1.31]
FAIL_NO_PATH_EXISTS 4.5 4.2
kills 49.3 49.3 x1.00 [0.98, 1.02]
fall and hurt-volume deaths 0 0
  • Bots that reach the bottom stop retrying, so the hatch ladder sees a tenth of the descents it did.
  • The map's other short ladders: 32 descents, 18 back on top (56 %) with More tweaks to bot ladder approaches, climbs and dismounts #2176; 6 descents, all to the bottom, with
    this PR.
  • "Stuck elsewhere" is x1.11 on both navmeshes (each interval across 1): the bots the tower no longer holds reach the
    map's other spots, on master's navmesh mostly its 3492 / 3493. Capped at 20 stuck events a bot, the total is x0.74
    [0.62, 0.88].
  • Against the same code without the SeekWeapon PR, the same pairs measure x0.79 [0.64, 0.99] (tower x0.39 [0.26,
    0.57]): the bots that PR keeps out of the fire pit no longer end up in this trap, which its own measurement showed as
    a rise in other stuck (x1.27 [0.98, 1.67]).

Other ladder maps, 72 pairs: 5 ghost spawns twice on transit, tarmac, subsurface, marketa, rogue and ghost CTG,
yard JGR's 3 juggernaut spawns twice, 6 bullet TDM matches (the navmeshes of their companion or planned neoAssets PRs;
ghost and yard on their current research navmeshes):

#2176 + this PR
descents a match 1.9 0.4
... ending back on top 82 % (112 of 137) 0 % (0 of 32)
climbs up a match 11.8 12.0
stuck, paired x0.92 [0.72, 1.16], 35 down / 35 up
"fell off the path" 3.2 2.6 (x0.81 [0.67, 0.98])
FAIL_NO_PATH_EXISTS 6.5 6.4
kills 67.1 66.7 (x0.99 [0.94, 1.03])
fall and hurt-volume deaths 9 10 (rogue's canal, bullet's track)

By map (10 pairs; 6 for yard and bullet), every interval across 1: yard x0.57, bullet x0.72, subsurface x0.77, rogue
x0.82, marketa x0.83, tarmac x1.10, transit x1.25, ghost x1.27. Ghost has no ladder descents; its figure is one
match in which three bots logged 68-138 stuck events each (one of those spots piles up the same way in the #2176
arm). On transit the descent of ladder 2 now works (8.7 attempts a match, 94 % back on top, -> 2.6, 0 %), and more
bots reach the pilot car below it, a known stuck spot (625: 27 -> 63 events in 10 matches).

Descents from behind the ladder's plane. ntre_bullet_tdm's shaft ladders have forward tops, landings behind the
ladder's plane, which its reworked navmesh keeps as climb exits only, because descents from there failed.
On master's navmesh (8 matches an arm, one round each): ladder 1 19 of 20 descents back on top with #2176, 3 of 5
with this PR; ladder 4 27 of 27, then 0 of 8.

Cost

Two dot products per bot update while a bot descends a ladder.

Risk

Moderate. Every ladder descent changes: the approach now takes the bot to the point it hangs from and mounts only once
it is off the top, and the locomotion holds still until the view points down. Where the walk out to that point is
blocked, the approach's existing 3 s timeout ends it, as before.

Toolchain

  • Linux GCC Distro Native (Linux Mint 22.3, GCC 13.3.0)

Linked Issues

CGameMovement::LadderMove() puts a player on any ladder their wish
direction points at, while PlayerLocomotion::TraverseLadder()'s
NO_LADDER branch drops the move type straight back to walking whenever
the bot's own state machine did not ask for a climb. A path that merely
brushes past a ladder leaves the two toggling every tick: the bot hangs
there making no progress, and because it never stops moving, no stuck
event fires to notice it. Ladders whose foot hangs a chest height or
more above the floor they serve are the worst, since they lie across a
walkway rather than sitting at the foot of one.

Rather than let go harder - which does not work, the wish direction
still points at the brush - the bot gives up the argument and rides the
ladder out by whichever end is nearer, but only once the contact has
persisted for half a second. Most grabs sort themselves out sooner than
that and adopting those costs a lot of climbing nobody asked for.

Measured over 213 scenarios on all 29 maps, 213 matches an arm: time
pinned against an unwanted ladder 10.2 -> 6.8 s a match, worst bout
61.7 -> 21.4 s, climbs finished 71 -> 79 %, stuck events 155 -> 141,
capture rate level at 25 %, for 3.5 s a match of adopted climbing. Two
held-out batches on five maps the behaviour was never tuned against,
156 matches an arm, agree: pinned time -37 %, worst bout -41 %, and the
most finished climbs of any variant tried.

Taking the ladder over only upward, and taking it over on contact
without waiting, were both measured and are worse - the first strands
bots somewhere they have to come back from, the second re-adopts the
same ladder about six times a bout.

This lands as a plain behaviour change with no convar to switch it off.
The bug is not conditional: a reviewer running the branch with the fix
disabled would simply hit the original defect and judge the branch by
it, and offering the choice would ask them to review a switch as well as
a change. The strategies that lost are in this branch's history and in
the research notes rather than in the shipped code.
CNEOBotLadderClimb puts the bot on a ladder without telling
PlayerLocomotion, whose ladder state stays NO_LADDER. The locomotion
then treats the climb as a ladder it never asked for: it forces the
move type back to walking every update, and after half a second the
unwanted-ladder takeover adopts the ladder by its nearer end. At the
foot of a ladder that end is the bottom, so a bot mounted to climb up
is driven back down.

The climb now calls ClimbLadder() or DescendLadder() when it starts, so
the locomotion drives the same way as the behavior. The dismount goal
is the climb's exit area, else the ladder's own end area; without one
there is nothing to walk to and the climb is left unclaimed.

ApproachAscendingLadder() checked whether the bot is too far below the
nav ladder's bottom before checking whether it is already on the
ladder. On a ladder whose foot hangs above the floor that refused a
claimed climb and handed it to the takeover again. A bot already on the
ladder now goes straight to ascending.

Research arm, 13 ladder-heavy maps: up-climbs finished 54 -> 66 % with
the claim; the reordered check fixed 29 of 96 refused claims on a
ladder with a 55 u foot gap.
PlayerLocomotion's AscendLadder() and DescendLadder() give up the ladder
the moment the engine reports the bot off it, even for a single tick.
When CNEOBotLadderClimb carries on and the engine grabs the bot again,
the locomotion sees a ladder it never asked for, forces walking, and
after half a second adopts it by its nearer end - mid-climb often the
bottom.

While it climbs, the behavior now claims the ladder again whenever the
bot is on it and the locomotion has let go. The dismount phase is left
alone: there the bot is meant to be leaving the ladder.

Research arm, measured together with a fix to the climb's teleport
placement, on a 32 u squeeze ladder whose climbs lost their claim
this way: finished up-climbs 59 -> 76 % by position.
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.
A bot the engine puts on a ladder while it still stands at the top -
on the landing, or on top of the ladder brush where that stands a step
proud of the floor - cannot press its way down: the floor or the brush
is under its feet. The approach used to mount it there anyway.

- The approach heads for where a descending bot hangs, half a hull
  plus 2 u in front of the ladder's face at its top, and mounts only
  once the bot is off the top: below the ladder's top and no longer
  standing within a step of it.
- It lines up on the ladder's normal line only from in front of the
  face. Coming in at an angle, the bot stays on the floor beside the
  drop and steps up onto the ladder's top; from beside or behind the
  ladder the line-up would aim into the wall, so it walks straight on.
- Going down it looks down the ladder, not level at its center: the
  forward press that grabs the ladder stays held until the next bot
  update, and with a level view it climbs the bot back up.
CNEOBotLadderClimb jumps off early once the exit area is within a safe
drop below. Going down a short ladder that is at once, on the mount,
and the kick towards the exit area's center lands the bot back on the
floor it started from wherever that floor overhangs the exit (a hatch
in a roof, a landing over the room below). Going down, the jump-off
now waits until the bot is a standing height below the ladder's top.
@sunzenshen

Copy link
Copy Markdown
Contributor Author

Closing this in favor of combining with #2176, as these both cover the same ladder testing case and should be reviewed together.

@sunzenshen sunzenshen closed this Oct 1, 2026
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 1, 2026
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.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 7, 2026
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.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
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.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
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.
sunzenshen added a commit to sunzenshen/neo that referenced this pull request Oct 8, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant