docs: #338 answered — retail DOES read the authored step height, so this is real

The gating question is closed against the decomp rather than reasoned
from our source. CTransition::step_up @0x0050b610 defaults step_up_height
to 0.0399999991f and substitutes object_info.step_up_height when
(state & 2); step_down has the same shape at 0x0050b852 and reads the
authored value unconditionally at 0x0050c232.

Two things fall out. Retail's fallback is 0.04, not 0.4 — our value
matches neither the fallback nor the authored 0.600/1.500. And state
bit 0x2 is OnWalkable, so retail applies the authored height only while
standing on walkable ground. We already port that gate faithfully in
Transition.DoStepUp, including the stepDownHeight = oi.StepUpHeight
assignment that reads oddly but is exactly what retail passes. The gate
is not the defect; only the value fed into it is.

The local player is the only affected population: its controller fields
initialise to 0.4f, while remotes and live entities get Setup-derived
values. The property's doc comment names PlayerModeController.
ApplyStepHeights as the authoritative writer — that method does not
exist anywhere in the tree; the identifier appears once, in the comment.

Deliberately NOT fixed. A real writer does exist further out, and
RuntimeSetPositionMoverPreparation does compute the Setup-derived value,
so the plumbing is there. Whether it runs for the local player or runs
and is overwritten is unproven — the probe reading 0.400 says the
controller held its default, not why. Setting the field without knowing
which path won would be a coin flip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-06 22:45:45 +02:00
parent 677f9a1628
commit 801ff5fd44

View file

@ -42,18 +42,68 @@ The human Setup `0x02000001` authors `StepUpHeight = 0.600` and
`StepDownHeight = 1.500`. The live `[support]` probe lines show the player
resolving with `stepUp=0.400 stepDown=0.400`.
### What is NOT yet established
### ANSWERED 2026-08-06 — retail DOES read the authored field, so this is real
The gating question is closed. `CTransition::step_up` @0x0050b610
(`acclient_2013_pseudo_c.txt:273109-273117`):
```
0050b655 float step_up_height = 0.0399999991f; // fallback
0050b661 if ((this->object_info.state & 2) != 0) { // <- the gate
0050b665 OBJECTINFO::get_walkable_z(this);
0050b671 step_up_height = this->object_info.step_up_height; // authored
}
0050b6ba CTransition::step_down(this, step_up_height, arg2)
```
`step_down` has the same shape at `0x0050b852` (default `0.04`, conditional
substitution) and reads the authored value unconditionally at `0x0050c232`,
where it is then halved against the sphere radius if it exceeds a diameter.
**Two facts fall out of this, and the second is the more useful one.**
**(1) The fallback is `0.04`, not `0.4`.** Our value matches neither retail's
fallback nor the authored `0.600`/`1.500`. It is an order of magnitude above
retail's fallback and well below the authored value.
**(2) `state & 2` is `OnWalkable`** in our own `ObjectInfoState`. So retail
applies the authored step height ONLY while standing on walkable ground, and
drops to `0.04` otherwise. **We already port that gate correctly**
`Transition.DoStepUp` (`TransitionTypes.cs` ~5836) is a faithful copy,
including `stepDownHeight = oi.StepUpHeight`, which reads oddly but is exactly
what retail passes. **The gate is not the defect. Only the VALUE fed into it
is.**
### Where the 0.4 comes from — mapped, with one hop unproven
- `PlayerMovementController._stepUpHeight` / `_stepDownHeight` are
**initialised to `0.4f`** (`PlayerMovementController.cs:159-160`).
- The `StepUpHeight` property's own doc comment says the authoritative source
is the player's `Setup.StepUpHeight`, set by
**`PlayerModeController.ApplyStepHeights`**. **That method does not exist
anywhere in the tree** — the identifier appears exactly once, inside that
comment. Either the wiring was removed and the comment survived, or it never
landed.
- **Remotes and live entities are NOT affected.** They get Setup-derived
values: `LiveEntityMotionRuntimeController.cs:321`
(`setup.StepUpHeight * scale`, falling back to `0.4f`) and
`RuntimeSetPositionMoverPreparation.cs:180`. The local player is the odd one
out — which is the population that matters, since it is what the user feels.
**NOT ESTABLISHED, and must be before any fix.** There IS one real writer:
`RuntimeLocalPlayerPhysicsPublicationState.cs:215-216` assigns from
`command.Physics.StepUpHeight`, and that command is built by
`RuntimeSetPositionMoverPreparation` — which *does* compute the Setup-derived
value. So the plumbing exists. Whether it runs for the local player, or runs
and is then overwritten, is unproven; the live probe reading `0.400` says the
controller held its default at that moment, not why. **Print the controller's
two values at world entry and at the first resolve before changing anything.**
A fix that sets the field without knowing which path won will be a coin flip.
### Remaining open question
- Whether the authored Setup values are what retail feeds `CPhysicsObj`, or
whether retail also substitutes constants at this seam. **Grep
`named-retail` for `get_stepup_height` / `get_stepdown_height` and their
callers before touching anything** — the authored DAT field being ignored is
only a defect if retail reads it.
- Where the 0.400 comes from: a hardcoded default, a stance/posture-dependent
value, or a value that never got wired from the Setup.
- Whether it has any observable consequence. Do not open this by reasoning
from the source; the #337 lineage already burned two diagnoses that way.
### Related
Sits next to #32 (local-player cliff edge-slide), which has its own research