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
|
## #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.
|
**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
|
are now ported, focused/full gates pass, and the user accepted repeated
|
||||||
slope jumps. Evidence:
|
slope jumps. Evidence:
|
||||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
`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,
|
Matrix rows accepted so far: speed parity, roof slide, downhill bounce,
|
||||||
flat pop, uphill landing, and #269's slope-stop feel
|
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
|
plane. Both rules are now ported; focused/full tests and the user's repeated
|
||||||
slope-jump gate pass. See
|
slope-jump gate pass. See
|
||||||
`docs/research/2026-07-31-269-slope-stop-capture.md`.
|
`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);
|
return CliffSlide(cliffPlane);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sp.HasWalkablePolygon)
|
// Retail tests only SPHEREPATH::walkable here. When the failed
|
||||||
sp.RestoreLastWalkable();
|
// 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)
|
if (sp.HasWalkablePolygon)
|
||||||
{
|
{
|
||||||
// L.4-walkable-steep (2026-04-30): the stored Walkable polygon
|
// L.4-walkable-steep (2026-04-30): the stored Walkable polygon
|
||||||
|
|
@ -2293,9 +2302,8 @@ public sealed class Transition
|
||||||
ci.ContactPlaneIsWater = false;
|
ci.ContactPlaneIsWater = false;
|
||||||
sp.RestoreCheckPos();
|
sp.RestoreCheckPos();
|
||||||
|
|
||||||
if (!sp.HasWalkablePolygon)
|
// Retail returns Collided when the back-probe found no walkable.
|
||||||
sp.RestoreLastWalkable();
|
// In particular, it does not substitute a retained earlier polygon.
|
||||||
|
|
||||||
if (sp.HasWalkablePolygon)
|
if (sp.HasWalkablePolygon)
|
||||||
return sp.PrecipiceSlide(this);
|
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).");
|
"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()
|
private static string SolutionRoot()
|
||||||
{
|
{
|
||||||
var dir = AppContext.BaseDirectory;
|
var dir = AppContext.BaseDirectory;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue