fix(physics): port retail stair edge backprobe
This commit is contained in:
parent
5a0f9868a6
commit
2dcb4f1d94
6 changed files with 218 additions and 6 deletions
|
|
@ -98,6 +98,38 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #271 — Stair-side collision reverses uphill movement and rapidly slides the player down
|
||||
|
||||
**Status:** DONE — 2026-07-31 (implementation + user live gate)
|
||||
**Severity:** MEDIUM (movement feel and navigation)
|
||||
**Component:** physics / `edge_slide` / `precipice_slide`
|
||||
|
||||
**Symptom:** while running diagonally up an outdoor staircase and pressing
|
||||
against its side, the character could suddenly move backward and slide rapidly
|
||||
to the bottom.
|
||||
|
||||
**Root cause:** a 677-quantum live trace caught the first bad frame. A valid
|
||||
X side-wall collision turned a requested `+0.88377` uphill Y displacement into
|
||||
`-0.34059`, followed three frames later by a 1.52 m snap to terrain.
|
||||
`EdgeSlideAfterStepDownFailed` promoted ACDream's separately retained
|
||||
`LastWalkable` polygon into the current `SPHEREPATH::walkable` slot. At the
|
||||
stair side this could be the preceding tread, so `PrecipiceSlide` projected
|
||||
against stale geometry and reversed the tangent.
|
||||
|
||||
Named-retail `CTransition::edge_slide @ 0x0050B3D0` never substitutes an older
|
||||
polygon: when current `walkable` is null it back-probes at the current sphere
|
||||
center, restores the failed candidate, and only then invokes
|
||||
`SPHEREPATH::precipice_slide @ 0x0050CC80`. Both stale-history substitutions
|
||||
are removed. The exact captured frame is pinned against the installed stair
|
||||
fixture: pre-fix output moved downhill to Y `75.346481`, while the retail-flow
|
||||
result advances uphill to Y `76.078186` and Z `60.016247`. Core Release passes
|
||||
4,108 tests / 2 skips; the complete Release solution passes 10,062 tests /
|
||||
5 skips. The user then repeatedly climbed the same stairs while pressing into
|
||||
their sides and confirmed the rapid downhill slide was gone. Evidence:
|
||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
||||
|
||||
---
|
||||
|
||||
## #270 — Stuck spell animations + intermittently missing monster attack animations
|
||||
|
||||
**Status:** CLOSED 2026-07-31 — both symptoms user-verified fixed (stuck casts: exhaustion-edge gate `a46c8e65`; missing monster attack animations: spawn settle placement `21b3a3f3` + lost-cell retry `807fdb5f`). Final settle-session log: 14/15 spawn settles grounded; Falling-refusal spam 2,954 → 15 transient pre-settle lines. All #270 probes stripped.
|
||||
|
|
|
|||
|
|
@ -389,6 +389,16 @@ root-caused, retail-ported, and user-accepted in the same session:
|
|||
are now ported, focused/full gates pass, and the user accepted repeated
|
||||
slope jumps. Evidence:
|
||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
||||
- **#271 closed 2026-07-31** — a bounded stair-side
|
||||
trace proved ACDream could bypass retail's current-position edge back-probe
|
||||
by promoting a stale `LastWalkable` tread. That made PrecipiceSlide reverse
|
||||
an uphill tangent and rapidly carry the player down the stairs. The two
|
||||
stale-history substitutions are removed; current-walkable, back-probe, and
|
||||
no-walkable outcomes now follow `CTransition::edge_slide @ 0x0050B3D0`.
|
||||
The exact captured frame is pinned in the existing installed-stair fixture
|
||||
and the complete Release suite passes 10,062 tests / 5 skips. The user
|
||||
accepted repeated uphill runs while pressing into the stair sides. Evidence:
|
||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
||||
|
||||
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
||||
flat pop, uphill landing, and #269's slope-stop feel
|
||||
|
|
|
|||
108
docs/research/2026-07-31-271-stair-side-slide-capture.md
Normal file
108
docs/research/2026-07-31-271-stair-side-slide-capture.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# #271 — Stair-side uphill reversal capture
|
||||
|
||||
**Date:** 2026-07-31
|
||||
|
||||
**Status:** closed; retail control flow restored and user live gate passed
|
||||
|
||||
## Symptom
|
||||
|
||||
When the local player ran diagonally uphill while pressing into the side of
|
||||
an outdoor staircase, the character could suddenly move backward and rapidly
|
||||
slide to the bottom. The symptom was intermittent because it required the
|
||||
forward candidate to hit the side wall while the step-down recovery crossed a
|
||||
tread edge.
|
||||
|
||||
This is not an RDP, render-rate, animation, or gravity symptom. It reproduced
|
||||
inside the pure Core collision resolver from one captured input frame.
|
||||
|
||||
## Live evidence
|
||||
|
||||
The bounded capture is under the ignored local artifact pointer:
|
||||
|
||||
`artifacts/issue271-stair-side/LATEST.txt`
|
||||
|
||||
It contains 677 local-player physics quanta plus the matching resolver stream.
|
||||
The first decisive frame is quantum 310:
|
||||
|
||||
```text
|
||||
current = (133.03775, 75.53931, 59.608147)
|
||||
target = (133.33783, 76.42308, 59.608147)
|
||||
input = forward + run
|
||||
result = (133.18779, 75.19872, 59.316677)
|
||||
normal = (-1, approximately 0, approximately 0)
|
||||
```
|
||||
|
||||
The X side-wall collision was valid, but the tangential Y component reversed:
|
||||
an uphill request of `+0.88377` produced `-0.34059`. Three frames later,
|
||||
quantum 313 snapped from Z `59.52598` to terrain Z `58.005`. A second attempt
|
||||
reproduced the same family at quanta 479–482, falling from Z `60.96376` to
|
||||
`58.005`.
|
||||
|
||||
## Retail oracle
|
||||
|
||||
Named retail:
|
||||
|
||||
- `CTransition::edge_slide` at `0x0050B3D0`
|
||||
- current-walkable branch at `0x0050B44A`
|
||||
- no-walkable back-probe at `0x0050B458–0x0050B50F`
|
||||
- `SPHEREPATH::precipice_slide` at `0x0050CC80`
|
||||
|
||||
Retail tests only the current `SPHEREPATH::walkable` pointer. If it is null,
|
||||
retail:
|
||||
|
||||
1. offsets the failed candidate back to the current sphere center;
|
||||
2. runs `step_down` there to rediscover the surface actually under the mover;
|
||||
3. restores the failed candidate;
|
||||
4. runs `precipice_slide` against the newly discovered polygon; or
|
||||
5. returns `COLLIDED_TS` when the back-probe found no walkable polygon.
|
||||
|
||||
Retail has no substitution of an older saved walkable polygon in either null
|
||||
case.
|
||||
|
||||
## ACDream divergence and root cause
|
||||
|
||||
`EdgeSlideAfterStepDownFailed` previously called
|
||||
`SpherePath.RestoreLastWalkable()`:
|
||||
|
||||
- before deciding whether to enter the retail back-probe; and
|
||||
- again when the back-probe found no current walkable polygon.
|
||||
|
||||
`LastWalkable` is a separate ACDream history used by the still-open
|
||||
CliffSlide compatibility path. At a staircase side wall it could describe the
|
||||
preceding tread rather than the surface below the current player position.
|
||||
Promoting it into the current slot bypassed retail's back-probe.
|
||||
`PrecipiceSlide` then projected the failed forward candidate along the stale
|
||||
tread edge, producing the backward/downhill displacement seen in the capture.
|
||||
|
||||
The fix removes both stale-history promotions from the edge-slide dispatch.
|
||||
Current walkable state still takes retail's direct precipice path; absent
|
||||
state now always takes retail's current-position back-probe.
|
||||
|
||||
## Deterministic regression
|
||||
|
||||
`Issue185OutdoorStairsSeamReplayTests` reuses the captured
|
||||
`0x01000AC5` staircase collision fixture and the exact quantum-310 position,
|
||||
contact plane, movement delta, player flags, and 1.5 m Setup step-down height.
|
||||
|
||||
Pre-fix:
|
||||
|
||||
```text
|
||||
out = (133.187790, 75.346481, 59.430882)
|
||||
```
|
||||
|
||||
Fixed:
|
||||
|
||||
```text
|
||||
out = (133.187790, 76.078186, 60.016247)
|
||||
```
|
||||
|
||||
The regression requires meaningful positive uphill progress and forbids a
|
||||
downhill Z displacement. The complete Core Release suite passes 4,108 tests /
|
||||
2 skips; the complete Release solution passes 10,062 tests / 5 skips.
|
||||
|
||||
## Live acceptance
|
||||
|
||||
The user repeatedly ran uphill while pressing into both sides of the affected
|
||||
staircase. Movement remained stable and the former rapid downhill reversal did
|
||||
not recur. The client then closed through the normal logout path, with ACE
|
||||
confirming graceful logout.
|
||||
|
|
@ -87,3 +87,15 @@ InputDispatcher / PlayerMovementController
|
|||
plane. Both rules are now ported; focused/full tests and the user's repeated
|
||||
slope-jump gate pass. See
|
||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
||||
- 2026-07-31: #271 stair-side uphill reversal. Never promote ACDream's
|
||||
retained `LastWalkable` history into retail's current
|
||||
`SPHEREPATH::walkable` slot inside `edge_slide`. When current walkable is
|
||||
null, retail `CTransition::edge_slide @ 0x0050B3D0` must back-probe at the
|
||||
current sphere center and either use that newly discovered polygon or
|
||||
collide. The stale substitution selected a preceding stair tread, reversed
|
||||
the uphill tangent, and dropped the player rapidly down the stairs. The
|
||||
exact live frame is pinned in `Issue185OutdoorStairsSeamReplayTests`; Core
|
||||
passes 4,108 / 2 skips and the complete Release solution passes 10,062 /
|
||||
5 skips. The user accepted repeated uphill stair-side runs on 2026-07-31.
|
||||
See
|
||||
`docs/research/2026-07-31-271-stair-side-slide-capture.md`.
|
||||
|
|
|
|||
|
|
@ -2192,9 +2192,18 @@ public sealed class Transition
|
|||
return CliffSlide(cliffPlane);
|
||||
}
|
||||
|
||||
if (!sp.HasWalkablePolygon)
|
||||
sp.RestoreLastWalkable();
|
||||
|
||||
// Retail tests only SPHEREPATH::walkable here. When the failed
|
||||
// step-down has no current walkable polygon it MUST continue into the
|
||||
// back-probe below, which rediscovers the polygon under CurPos before
|
||||
// testing the failed candidate against its edge
|
||||
// (CTransition::edge_slide 0x0050B3D0, 0x0050B44A-0x0050B50F).
|
||||
//
|
||||
// #271 (2026-07-31): acdream previously promoted its separately
|
||||
// retained LastWalkable polygon into the current slot at this point.
|
||||
// At a staircase side wall that polygon could be the preceding tread.
|
||||
// PrecipiceSlide then projected the forward candidate along the stale
|
||||
// tread edge, reversing its uphill tangent and dropping the player
|
||||
// rapidly down the stairs. Do not restore stale history here.
|
||||
if (sp.HasWalkablePolygon)
|
||||
{
|
||||
// L.4-walkable-steep (2026-04-30): the stored Walkable polygon
|
||||
|
|
@ -2293,9 +2302,8 @@ public sealed class Transition
|
|||
ci.ContactPlaneIsWater = false;
|
||||
sp.RestoreCheckPos();
|
||||
|
||||
if (!sp.HasWalkablePolygon)
|
||||
sp.RestoreLastWalkable();
|
||||
|
||||
// Retail returns Collided when the back-probe found no walkable.
|
||||
// In particular, it does not substitute a retained earlier polygon.
|
||||
if (sp.HasWalkablePolygon)
|
||||
return sp.PrecipiceSlide(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -177,6 +177,48 @@ public class Issue185OutdoorStairsSeamReplayTests
|
|||
"A continuous walkable ramp seam must not persist a horizontal sliding normal (#137 family).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #271 live capture, quantum 310: a forward/uphill displacement that also
|
||||
/// presses into the staircase's side wall must keep its uphill tangent.
|
||||
/// Pre-fix the composite retry path reversed that tangent, moving from
|
||||
/// Y=75.539 to Y=75.199 and rapidly carrying the player back down the stairs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void OutdoorStairs_SideWallContact_DoesNotReverseUphillTangent()
|
||||
{
|
||||
var engine = BuildStairEngine();
|
||||
var body = GroundedOnTread();
|
||||
body.Position = new Vector3(133.03775f, 75.53931f, 59.608147f);
|
||||
body.ContactPlane = new Plane(
|
||||
new Vector3(3.2782555e-07f, -0.62469506f, 0.78086877f),
|
||||
0.75193405f);
|
||||
|
||||
ResolveResult result = engine.ResolveWithTransition(
|
||||
currentPos: body.Position,
|
||||
targetPos: body.Position + new Vector3(0.30007935f, 0.8837738f, 0f),
|
||||
cellId: StairCellId,
|
||||
sphereRadius: 0.48f,
|
||||
sphereHeight: 1.835f,
|
||||
stepUpHeight: 0.6f,
|
||||
stepDownHeight: 1.5f,
|
||||
isOnGround: true,
|
||||
body: body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||
movingEntityId: 0x01000000u);
|
||||
|
||||
_out.WriteLine(
|
||||
$"out=({result.Position.X:F6},{result.Position.Y:F6},{result.Position.Z:F6}) " +
|
||||
$"collision={result.CollisionNormalValid} " +
|
||||
$"normal=({result.CollisionNormal.X:F3},{result.CollisionNormal.Y:F3},{result.CollisionNormal.Z:F3})");
|
||||
|
||||
Assert.True(result.Position.Y > body.Position.Y + 0.25f,
|
||||
$"Side-wall response failed to preserve meaningful uphill motion: " +
|
||||
$"{body.Position.Y:F6} -> {result.Position.Y:F6}.");
|
||||
Assert.True(result.Position.Z >= body.Position.Z - 0.001f,
|
||||
$"Side-wall response dropped the grounded player downhill: " +
|
||||
$"{body.Position.Z:F6} -> {result.Position.Z:F6}.");
|
||||
}
|
||||
|
||||
private static string SolutionRoot()
|
||||
{
|
||||
var dir = AppContext.BaseDirectory;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue