#331 reported that `PhysicsEngine.ResolveWithTransition` refuses ALL uphill
motion whenever a `body:` is supplied. It does not. It refuses a step whose
sub-step offset is exactly anti-parallel to a live sliding normal — the
#137-family absorb this project already recorded as retail-faithful.
Measured on the same fixture, same gradient, same body, varying only the
heading relative to the slope gradient:
(0, -0.1, 0) cross-slope 0 -> zero movement, latched
(0.0001,-0.1, 0) cross-slope 0.0001 m -> zero movement, latched
(0.001, -0.1, 0) cross-slope 0.001 m -> climbs 0.176 m in 5 ticks
(0.01, -0.1, 0) cross-slope 0.01 m -> climbs 0.176 m in 5 ticks
The threshold is retail's own F_EPSILON small-offset abort (0.0002 m): about
0.11 degrees off the exact gradient at a 0.1 m step. `RemoteRampHarness`
builds a ramp whose gradient is exactly along Y and the original probe pushed
exactly along -Y, so it hit the measure-zero case with probability 1.
The latch itself is production-real in mechanism — a pure gravity fall under
the production RuntimeRemotePhysicsUpdater, with no fixture settle seam
involved, lands leaving Contact|OnWalkable|Sliding with slidingNormal (0,1,0)
— but every link is faithful to retail, verified in the PDB-paired binary
rather than Binary Ninja (BN typed find_transitional_position `void` and
dropped the load-bearing return value):
validate_walkable sets collision_normal from the terrain plane when
OBJECTINFO CONTACT is clear 0x0050d251 / 0x0050d261 / 0x0050d26c
validate_transition converts it unconditionally 0x0050ac19-0x0050ac30
set_sliding_normal zeroes Z AND re-normalizes 0x0050a060
SetPositionInternal persists SLIDING_TS 0x005154c2 / 0x005154e1
get_object_info re-seeds it next frame 0x00511d44 / 0x00511d4f
find_transitional_position returns
`i != 0 && state == OK` on the step-0 abort 0x0050c0ed -> 0x0050c089
ACE agrees (Transition.cs:1027, CollisionInfo.cs:58). No production code
changed; no divergence introduced, so no register row.
What lands is the coverage whose absence made this invisible — nothing in the
suite asserted that a body-bearing mover makes uphill progress on a walkable
slope, and the test that found #331 passed vacuously because the body never
moved:
RuntimeRemoteUphillProgressTests.ARemoteWithABodyClimbsAWalkableSlopeAndKeepsItsFeetOnIt
per-tick climb + surface tracking under a realistic off-gradient heading.
SAB-A1 AdjustOffset -> Vector3.Zero reddens at tick 1
SAB-A2 fixture gradient -> 0 (flat) reddens at tick 1
RuntimeRemoteUphillProgressTests.AnExactlyUpSlopeOffsetIsAbsorbedByThePersistedSlidingNormal
characterization pin for the absorb, with the retail anchors inline.
SAB-B1 delete the get_object_info sliding seed reddens (climbs to 57.7544)
SAB-A1 reddens
SAB-A2 reddens
NON-discriminating, measured and documented: making the final tick
exactly up-slope leaves it green — by then the latch is already cleared.
RemoteRampHarness gains a warning block naming the axis-alignment trap so the
next vacuous uphill assertion is caught at authoring time.
Suite re-measured from a full clean (43 bin/obj removed): 11,198 passed /
4 skipped / 0 failed, against the 11,196/4/0 baseline at 0d62a5ff — exactly
the two tests added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0d62a5ffeb
commit
ec29a732f5
3 changed files with 319 additions and 75 deletions
163
docs/ISSUES.md
163
docs/ISSUES.md
|
|
@ -74,98 +74,111 @@ been vacuous evidence.
|
|||
|
||||
---
|
||||
|
||||
## #331 — `ResolveWithTransition` refuses ALL uphill motion on a constant-gradient terrain ramp (fixture-or-production unresolved)
|
||||
## #331 — an exactly-up-slope step is absorbed by the sliding normal a landing leaves behind (headline claim REFUTED; behaviour is retail-faithful)
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** **RAISED from UNKNOWN 2026-08-06 at the AD-10 architecture
|
||||
review — the discriminator is now known and it is NOT the fixture.**
|
||||
**Status:** DONE — settled 2026-08-06. Not a defect. The headline "refuses ALL
|
||||
uphill motion" is **false**; it was an artifact of an axis-aligned fixture
|
||||
driven by axis-aligned motion. Coverage added, fixture annotated.
|
||||
**Severity:** none as a defect. The mechanism below is real and reachable in
|
||||
production, but it is retail-faithful at the instruction level and already on
|
||||
the #137 DO-NOT-RETRY list.
|
||||
|
||||
The deciding variable is the **`body:` parameter**, not terrain publication:
|
||||
### The verdict
|
||||
|
||||
- `body: null` → the same uphill sweep climbs fine: `ok=True`, moved
|
||||
`(0, −0.0999, +0.060)`.
|
||||
- `body:` supplied → `ok=False` and **zero** movement.
|
||||
- It reproduces under a call profile **identical to the local player's**
|
||||
(`IsPlayer | EdgeSlide` plus the human two-sphere Setup list).
|
||||
- A **diagonal** request keeps its cross-slope X and zeroes only the up-slope
|
||||
Y — i.e. the slope is behaving as a wall in exactly one direction.
|
||||
- It fires on a **1.1° ramp**, not just steep ones.
|
||||
`ResolveWithTransition` refuses a step whose **sub-step offset is exactly
|
||||
anti-parallel to a live sliding normal**, not uphill motion as such. On the
|
||||
same fixture, at the same gradient, with the same body:
|
||||
|
||||
So the original first lead (terrain publication / synthetic fixture) is now the
|
||||
*less* likely explanation, and "confined to the fixture" is no longer a
|
||||
comfortable default: the failing call shape is the shape production uses.
|
||||
Nothing in the suite asserts uphill progress on a walkable slope, which is why
|
||||
this has never been caught — the test that found it passed **vacuously**,
|
||||
because the body never moved.
|
||||
| per-tick root motion | cross-slope component | result over 5 ticks |
|
||||
|---|---|---|
|
||||
| `(0, -0.1, 0)` | 0 | zero movement, latched |
|
||||
| `(0.0001, -0.1, 0)` | 0.0001 m | zero movement, latched |
|
||||
| `(0.001, -0.1, 0)` | 0.001 m | **climbs 0.176 m**, latch clears on tick 1 |
|
||||
| `(0.01, -0.1, 0)` | 0.01 m | **climbs 0.176 m**, latch clears on tick 1 |
|
||||
|
||||
Prior severity note, retained: if it reproduces on DAT terrain it is severe and
|
||||
affects the local player as much as remotes; if it is confined to the synthetic
|
||||
fixture it is a test-infrastructure defect that silently voids any uphill
|
||||
assertion written against that fixture — which is how it was found.
|
||||
**Filed:** 2026-08-06, while measuring AD-10 (commits `fe6ee877`, `886333a2`).
|
||||
**Not caused by AD-10, and unaffected by its deletion** — the behaviour is
|
||||
identical with the pre-sweep slope projection enabled and disabled.
|
||||
The escape threshold is exactly retail's `F_EPSILON` small-offset abort: the
|
||||
adjusted offset must exceed 0.0002 m. At a 0.1 m sub-step that is a heading
|
||||
more than about 0.11° off the exact gradient — a ±0.11° window out of 360°.
|
||||
|
||||
### What was measured
|
||||
### The mechanism, end to end
|
||||
|
||||
Against `tests/AcDream.Runtime.Tests/Physics/RemoteRampHarness.cs` — a single
|
||||
synthetic landblock whose terrain is one constant-gradient plane, published via
|
||||
`PhysicsEngine.AddLandblock` with empty `CellSurface[]` and `PortalPlane[]` —
|
||||
`PhysicsEngine.ResolveWithTransition` returns `ok=False` and the UNCHANGED input
|
||||
position for every uphill step, while the identical downhill step returns
|
||||
`ok=True` with a correct slope-following result.
|
||||
1. A landing (or the spawn settle that compresses it, `SpawnPlacementSettler`)
|
||||
reaches `OBJECTINFO::validate_walkable` with the OBJECTINFO `CONTACT` bit
|
||||
clear, so it calls `set_collision_normal` with the **terrain** plane normal.
|
||||
Verified in the PDB-paired binary: `0x0050d251 test byte ptr [ebp+4],1 /
|
||||
jne` then `0x0050d261 test eax,eax` (`step_down`) then
|
||||
`0x0050d26c call set_collision_normal`. acdream `TransitionTypes.cs`
|
||||
`ValidateWalkable` matches (`!oi.Contact && !sp.StepDown`).
|
||||
2. `CTransition::validate_transition` unconditionally converts it:
|
||||
`0x0050ac19 test eax,eax / 0x0050ac21 je / 0x0050ac30 call
|
||||
set_sliding_normal`. acdream matches.
|
||||
3. `COLLISIONINFO::set_sliding_normal` (`0x0050a060`) zeroes Z **and
|
||||
re-normalizes**, so even a 1° slope produces a **full-length horizontal
|
||||
normal pointing downhill**. acdream matches.
|
||||
4. `SetPositionInternal` persists it as `SLIDING_TS`
|
||||
(`0x005154c2` / `0x005154e1`); `get_object_info` re-seeds it next frame
|
||||
(`0x00511d44 test / 0x00511d4f call init_sliding_normal`). acdream matches.
|
||||
5. `CTransition::adjust_offset` sees `dot(offset, sliding) < 0` and projects
|
||||
the step onto the crease `cross(sliding, contact)` — a purely horizontal,
|
||||
purely cross-slope axis. An exactly-up-slope offset has zero component on
|
||||
it.
|
||||
6. The sweep aborts at step 0 and reports failure:
|
||||
`0x0050c0ed test ebx,ebx / jne 0x0050c089` -> `cmp [esp+14h],1 / jne` ->
|
||||
`xor eax,eax`. Retail returns `i != 0 && state == OK` — **byte-identical to
|
||||
acdream's `FindTransitionalPosition`** (Binary Ninja typed this function
|
||||
`void` and dropped the return value; the disassembly settles it).
|
||||
7. Because the transition failed, the writeback never runs, so the sliding
|
||||
state is never cleared -> self-latching until a step with a surviving
|
||||
component succeeds.
|
||||
|
||||
```
|
||||
from=<96, 96.11029, 57.53382> to=<96, 96.01029, 57.53382> (uphill)
|
||||
=> pos=<96, 96.11029, 57.53382> ok=False onWalkable=False
|
||||
from=<96, 96.11029, 57.53382> to=<96, 96.21029, 57.53382> (downhill)
|
||||
=> pos=<96, 96.18382, 57.489704> ok=True onWalkable=True
|
||||
```
|
||||
Cross-checked against ACE (`Transition.cs:1027`,
|
||||
`CollisionInfo.cs:58`) — same shape.
|
||||
|
||||
The downhill answer is right in detail: the XY advance is 0.0735 m for a 0.1 m
|
||||
request, exactly the `cos^2(theta)` shortening `Transition.AdjustOffset`'s
|
||||
away-plane arm produces at this gradient (see AD-65). So terrain IS being seen
|
||||
and the sweep IS projecting; only the uphill direction fails.
|
||||
### Why it read as "ALL uphill motion"
|
||||
|
||||
Ruled out by probe, all with `sphereRadius: 0.48`, `stepUpHeight: 0.4`,
|
||||
`body:` supplied and a valid walkable contact plane at tick start:
|
||||
`RemoteRampHarness` builds a ramp whose gradient is exactly along Y, and the
|
||||
probe pushed exactly along -Y. Axis-aligned fixture x axis-aligned motion hits
|
||||
the measure-zero anti-parallel case with probability 1. Everything the original
|
||||
report ruled out (gradient, step size, cell boundaries, Z seating, AD-10) was
|
||||
correctly ruled out; the variable it did not vary was the **heading relative to
|
||||
the gradient**.
|
||||
|
||||
- **Not gradient-dependent.** Fails at 0.05 (2.9 degrees) exactly as at 0.6
|
||||
(31 degrees). A 2.9-degree slope is ordinary terrain.
|
||||
- **Not step-size-dependent.** Fails for a 0.1 m and a 0.5 m request.
|
||||
- **Not a cell-boundary artifact.** Fails from (96,96) — which is exactly on
|
||||
both cell boundaries — and equally from (100,100), (100,96), (96,100) and
|
||||
(110,110) with the cell id recomputed for each.
|
||||
- **Not a Z-seating artifact.** Fails with the body lifted 0.05 m off the
|
||||
surface as well as seated on it.
|
||||
- **Not an axis bug — it follows UPHILL.** With the ramp inverted so it rises
|
||||
along +Y instead of descending, it is +Y that returns `ok=False` and -Y that
|
||||
succeeds. The failing direction tracks the slope, not the coordinate.
|
||||
### Production reachability — stated honestly
|
||||
|
||||
### Why this is not obviously a production defect
|
||||
The latch is production-real in mechanism: a pure gravity fall driven by the
|
||||
production `RuntimeRemotePhysicsUpdater`, with no fixture settle seam involved,
|
||||
lands on the ramp and leaves `Contact | OnWalkable | Sliding` with
|
||||
`slidingNormal = (0, 1, 0)`. The local player runs the same
|
||||
`ResolveWithTransition` with the same body and the same
|
||||
`IsPlayer | EdgeSlide` profile. So in production:
|
||||
|
||||
Players demonstrably walk uphill in acdream, and the local player runs the same
|
||||
`ResolveWithTransition`. So either production terrain differs from what this
|
||||
fixture publishes in some way that matters, or something in the live call
|
||||
arguments does. The fixture publishes terrain through `AddLandblock` only and
|
||||
registers no flat-collision statics; production goes through
|
||||
`LandblockPhysicsContentBuilder.PublishStaticCollision`. That difference was
|
||||
NOT traced end to end and is the first thing to check.
|
||||
- **After any landing on a slope, the first step's up-slope component is
|
||||
deleted** (one frame). This is retail behaviour.
|
||||
- **Holding a heading within ~0.11° of the exact gradient sticks you until you
|
||||
turn.** Also retail behaviour as written, and only reachable where a real
|
||||
terrain triangle's gradient happens to align with the held heading.
|
||||
|
||||
### How it was found, and why it matters regardless
|
||||
**NOT established:** a live DAT-terrain / connected-client reproduction. It was
|
||||
not run because the discriminator turned out to be offset-vs-gradient
|
||||
alignment, not terrain provenance — the same triangle plane is produced either
|
||||
way. If a player ever reports "stuck facing uphill until I turn", this is the
|
||||
mechanism.
|
||||
|
||||
An uphill counterpart to the AD-10 surface-tracking test was written and
|
||||
PASSED — vacuously. The body never moved, so it "stayed on the surface" by
|
||||
standing still. The test was dropped rather than shipped. Any future assertion
|
||||
about uphill movement written against this harness will be vacuous in the same
|
||||
way until this is resolved, which is reason enough to fix or document it even
|
||||
if production turns out to be fine.
|
||||
### What landed
|
||||
|
||||
### Next step
|
||||
- `tests/AcDream.Runtime.Tests/Physics/RuntimeRemoteUphillProgressTests.cs` —
|
||||
the missing coverage (`ARemoteWithABodyClimbsAWalkableSlopeAndKeepsItsFeetOnIt`,
|
||||
per-tick climb + surface tracking under a realistic heading) plus a
|
||||
characterization pin for the absorb, both sabotage-verified in both
|
||||
directions.
|
||||
- `RemoteRampHarness` now carries a warning block naming the axis-alignment
|
||||
trap, so the next vacuous uphill assertion is caught at authoring time.
|
||||
|
||||
Re-run the same two-position probe against a real DAT-published landblock (the
|
||||
bake/pak path, or a live capture with `ACDREAM_PROBE_RESOLVE=1` while walking
|
||||
uphill) and compare `ok`. That single comparison decides severity.
|
||||
**Do NOT** patch the small-offset abort, add per-frame sliding clearing, or
|
||||
special-case walkable planes in `validate_walkable` — all three are on the
|
||||
#137 DO-NOT-RETRY list and all three would be deliberate retail divergences.
|
||||
If the one-frame deletion is ever judged unacceptable, the lever is the
|
||||
**provenance** of the sliding normal, and it needs its own brainstorm.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue