fix(physics): #116 shape-1 — Path-6 head-sphere direct Collided return
Per docs/research/2026-07-30-ts4-116-oracle-plan.md §2.3-§2.4: retail's
airborne (not-yet-Contact) BSPTREE::find_collisions dispatch, when the
FOOT sphere is completely clear but the HEAD sphere hits or near-misses,
does not defer through SetCollide/Adjusted (nor the steep-poly
slide-tangent shortcut) — it records the head polygon's normal directly
and hard-stops: pc:323824-323834 (0x0053a793/0x0053a7a4), independently
cross-checked against ACE BSPTree.cs:221-230 (`SetCollisionNormal` +
`return TransitionState.Collided;`), an exact structural match confirming
this isn't a BN misdecompile. BSPQuery.cs's Path 6 `hasSphere1` branch now
does the same: `collisions.SetCollisionNormal(worldNormal1); return
TransitionState.Collided;`, replacing the old steep-shortcut-or-deferred-
SetCollide handling. This mechanically retires one of TS-4's two
`SetSlidingNormal` write sites (sphere1's) ahead of TS-4's own item.
Added two permanent diagnostics gated on the existing
PhysicsDiagnostics.ProbeIndoorBspEnabled flag (`[path-dispatch]` at
FindCollisionsCore entry, `[path5-diag]` inside Path 5) to make future
BSPQuery dispatch tracing cheaper.
HONEST RESULT of the plan's own confirming instrumentation (re-run of
DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals):
this fix does NOT change the tick-22760 outcome (harness still cn=(0,0,1)
vs live cn=(0,+1,0)). The new dispatch-entry probes show the tick-22760
mover is GROUNDED (Contact set), so it never reaches Path 6 at all — it
dispatches Path 5 -> StepSphereDown (Path 3, both DoStepDown half-steps
fail) -> EdgeSlideAfterStepDownFailed -> SpherePath.PrecipiceSlide, whose
find_crossed_edge-false fallback returns Collided with NO collision-normal
write. A fresh byte-level read of retail's SPHEREPATH::precipice_slide
(pc:274316-274326, 0x0050cc80) confirms this is byte-exact retail
behavior (`if (eax == 0) { walkable = 0; return 2; }`, no
set_collision_normal call) — not a bug. The real tick-22760 divergence is
further upstream, most likely this test's simplified door registration
(BuildEngineWithDoorFixture) not placing the door's BSP where live retail
actually intersected it, or a walkable-polygon state-capture gap — see
the research doc's Addendum 2 for the full trace and open candidates.
#116 shape-1 is therefore NARROWED, not closed: the Path-6 fix is a real,
independent retail-faithfulness improvement; the tick-22760 acceptance
criterion is not met by it and needs further harness/geometry work before
any further code change.
Full AcDream.Core.Tests suite: 4059 passed / 2 skipped, no regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7e1be3def0
commit
db2889afda
2 changed files with 158 additions and 22 deletions
|
|
@ -640,3 +640,102 @@ within 10° of flat; the shipped ACE-derived constant engages only within
|
||||||
the TS-4/#116 implementation slice, which owns `PhysicsBody`): replace
|
the TS-4/#116 implementation slice, which owns `PhysicsBody`): replace
|
||||||
`0.99999536f` with `0.98480775f` (cos 10°), cite this addendum, retire
|
`0.99999536f` with `0.98480775f` (cos 10°), cite this addendum, retire
|
||||||
register row AD-55 in the same commit.
|
register row AD-55 in the same commit.
|
||||||
|
|
||||||
|
## Addendum 2 (implementation session, 2026-07-30): #116 shape-1's tick-22760
|
||||||
|
## confirming run DISAGREES with this plan's hypothesis — the real mechanism
|
||||||
|
## is one layer further upstream, and it's retail-faithful there too
|
||||||
|
|
||||||
|
Per this doc's own §4 execution order, item 1 (the `TransitionalInsert`
|
||||||
|
exhausted-loop hardcoded return) landed first — mechanical, zero
|
||||||
|
observable behavior change, confirmed by the full `AcDream.Core.Tests`
|
||||||
|
suite (4059 passed / 2 skipped, no change in pass count). Then §2.3's
|
||||||
|
shape-1 fix landed verbatim in `BSPQuery.cs`'s Path 6 `hasSphere1`
|
||||||
|
branch: a foot-clear/head-hit airborne contact now returns
|
||||||
|
`TransitionState.Collided` with a direct `collisions.SetCollisionNormal`
|
||||||
|
write, exactly matching pc:323824-323834 (`0x0053a793`/`0x0053a7a4`) and
|
||||||
|
ACE `BSPTree.cs:221-230`. This is a real, independently-decomp-confirmed
|
||||||
|
port-accuracy fix and is kept regardless of the result below.
|
||||||
|
|
||||||
|
**The confirming instrumentation run (§2.4) DISAGREES with the plan's
|
||||||
|
tick-22760 hypothesis.** Re-running
|
||||||
|
`DoorBugTrajectoryReplayTests.Diagnostic_Tick22760_DumpEngineInternals`
|
||||||
|
after the fix landed shows **no change**: harness still reports
|
||||||
|
`cn=(0,0,1)` (the `UnitZ` ground-fallback default) against live's
|
||||||
|
`cn=(0,+1,0)` (the door-face normal). Adding a dispatcher-entry probe
|
||||||
|
(`[path-dispatch]`, `[path5-diag]`, gated on the existing
|
||||||
|
`ProbeIndoorBspEnabled` flag, kept in `BSPQuery.cs` as permanent
|
||||||
|
diagnostics) traced the ACTUAL call sequence for this capture:
|
||||||
|
|
||||||
|
1. The seeded body's `TransientState` (131 = `Contact | OnWalkable |
|
||||||
|
Active`) means `ObjectInfo.State & Contact != 0` for this mover — it
|
||||||
|
is **grounded**, so `BSPQuery.FindCollisionsCore` dispatches to
|
||||||
|
**Path 5** (the Contact/grounded branch), never Path 6 at all. The
|
||||||
|
plan's shape-1 hypothesis was explicitly scoped to "the not-yet-in-
|
||||||
|
Contact branch" (§2.3) — that scoping was itself the unconfirmed
|
||||||
|
part, and it does not hold for tick-22760.
|
||||||
|
2. Path 5's own dispatch for the door's BSP shape at this exact position
|
||||||
|
finds **neither sphere hitting nor near-missing**
|
||||||
|
(`hit0=False hitPoly0=False hit1=False hitPoly1=False`) — the
|
||||||
|
simplified fixture registration this test uses
|
||||||
|
(`BuildEngineWithDoorFixture`, which places the raw GfxObj BSP
|
||||||
|
directly at its captured world-space bounding-sphere center rather
|
||||||
|
than via the faithful `ShadowShapeBuilder.FromSetup` +
|
||||||
|
`PlacementFrame` transform that `BuildFaithfulDoorEngine` uses
|
||||||
|
elsewhere in the same file) returns `OK` for the door here.
|
||||||
|
3. `TransitionalInsert`'s step-down gate then fires
|
||||||
|
(`contactInvalidOrSteep` is true because the per-substep walk loop
|
||||||
|
clears `ContactPlaneValid` before every `TransitionalInsert` call —
|
||||||
|
`TransitionTypes.cs` around the `FindValidPosition` per-step reset —
|
||||||
|
so the door BSP is queried TWICE MORE via `DoStepDown`'s two half-
|
||||||
|
height attempts, dispatching to **Path 3** (`StepSphereDown` →
|
||||||
|
`FindWalkableInternal`), which also finds no walkable candidate here
|
||||||
|
(the door face is not a floor-like polygon) and returns `OK` both
|
||||||
|
times.
|
||||||
|
4. Both `DoStepDown` calls therefore fail (return `false`), which routs
|
||||||
|
into `EdgeSlideAfterStepDownFailed`. With `ContactPlaneValid` false,
|
||||||
|
`OnWalkable` true (seeded), `EdgeSlide` true (mover flags), and the
|
||||||
|
RESTORED walkable polygon from the body's own snapshot (a flat
|
||||||
|
triangle `(144,0,94)-(144,24,94)-(120,24,94)`, `Normal.Z=1 >=
|
||||||
|
FloorZ`), execution reaches `sp.PrecipiceSlide(this)`
|
||||||
|
(`TransitionTypes.cs` "branch3/precipice-slide").
|
||||||
|
5. **`SpherePath.PrecipiceSlide` calls `BSPQuery.FindCrossedEdge` against
|
||||||
|
that seeded triangle. The player's actual sweep (X≈133, Y from 18.02
|
||||||
|
to 17.60) does not cross ANY of that triangle's three edges** (the
|
||||||
|
triangle spans roughly X∈[120,144], and its hypotenuse sits at
|
||||||
|
X+Y=144 — at X=133 that's Y≈11, far south of the player's Y range).
|
||||||
|
`FindCrossedEdge` returns false, and acdream's `PrecipiceSlide`
|
||||||
|
(`TransitionTypes.cs:1039-1054`) does exactly what retail's
|
||||||
|
`SPHEREPATH::precipice_slide` does on the identical branch — **read
|
||||||
|
fresh this session, pc:274316-274326, `0x0050cc80`**:
|
||||||
|
```
|
||||||
|
int32_t eax = CPolygon::find_crossed_edge(...);
|
||||||
|
if (eax == 0) { this->walkable = eax; return 2; /* COLLIDED_TS */ }
|
||||||
|
```
|
||||||
|
**No `set_collision_normal` call on this path in retail either.**
|
||||||
|
This is a byte-exact match, not an inference — acdream's
|
||||||
|
`ClearWalkable(); return TransitionState.Collided;` on a failed
|
||||||
|
`FindCrossedEdge` is retail-faithful. `ValidateTransition`'s
|
||||||
|
`UnitZ`-default-on-invalid-normal fires identically in both engines
|
||||||
|
for this exact mechanism.
|
||||||
|
|
||||||
|
**Conclusion: the tick-22760 divergence is NOT explained by anything this
|
||||||
|
plan identified, and the mechanism this pass traced down to (Path 5 →
|
||||||
|
StepSphereDown → EdgeSlideAfterStepDownFailed → PrecipiceSlide's
|
||||||
|
no-crossed-edge fallback) is independently confirmed retail-faithful at
|
||||||
|
every step, including a fresh byte-level read of `precipice_slide`
|
||||||
|
itself.** The remaining candidates, none guessed at here: (a) this
|
||||||
|
specific harness (`BuildEngineWithDoorFixture`) may simply not place the
|
||||||
|
door's BSP polygons where live retail's did at that exact tick — a
|
||||||
|
harness/fixture-geometry gap, not a response-layer code bug — worth
|
||||||
|
re-running this same capture through `BuildFaithfulDoorEngine`'s
|
||||||
|
Setup-based registration to check whether a REAL BSP hit against the
|
||||||
|
door (rather than the seeded generic floor triangle) changes the
|
||||||
|
outcome; (b) the seeded `WalkableVertices` triangle itself may not match
|
||||||
|
what retail's own walkable-polygon bookkeeping held at that instant
|
||||||
|
(a state-capture gap in the original 2026-05-24 live-capture tooling,
|
||||||
|
not necessarily an engine bug); (c) a genuinely different upstream
|
||||||
|
mechanism not yet traced. Per CLAUDE.md's no-guessing rule, none of
|
||||||
|
these is adopted without further evidence — #116 shape-1 stays
|
||||||
|
**narrowed, not closed**: the Path-6 fix is a real, independent
|
||||||
|
retail-faithfulness improvement, and the original tick-22760 acceptance
|
||||||
|
criterion is NOT met by it. See ISSUES.md #116 for the updated status.
|
||||||
|
|
|
||||||
|
|
@ -1854,6 +1854,17 @@ public static class BSPQuery
|
||||||
|
|
||||||
var movement = sphere0.Center - localCurrCenter;
|
var movement = sphere0.Center - localCurrCenter;
|
||||||
|
|
||||||
|
// #116 shape-1 instrumentation (Campaign P final physics slice,
|
||||||
|
// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md §2.4):
|
||||||
|
// dispatcher-entry probe naming which of Path 1-6 this call will
|
||||||
|
// take, so the tick-22760 replay's per-call path sequence can be
|
||||||
|
// read off directly instead of inferred from later probes.
|
||||||
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[path-dispatch] insertType={path.InsertType} obstructionEthereal={path.ObstructionEthereal} checkWalkable={path.CheckWalkable} stepDown={path.StepDown} collide={path.Collide} contact={((obj.State & ObjectInfoState.Contact) != 0)} hasSphere1={hasSphere1}"));
|
||||||
|
}
|
||||||
|
|
||||||
// A6.P1: snapshot dispatcher entry for the [push-back-disp] probe.
|
// A6.P1: snapshot dispatcher entry for the [push-back-disp] probe.
|
||||||
// Emitted before path selection so the captured state reflects
|
// Emitted before path selection so the captured state reflects
|
||||||
// the inputs the dispatcher routes on. The returnState=-1 sentinel
|
// the inputs the dispatcher routes on. The returnState=-1 sentinel
|
||||||
|
|
@ -2039,6 +2050,19 @@ public static class BSPQuery
|
||||||
bool hit0 = SphereIntersectsPolyInternal(root, resolved, sphere0, movement,
|
bool hit0 = SphereIntersectsPolyInternal(root, resolved, sphere0, movement,
|
||||||
ref hitPoly0, ref contact0);
|
ref hitPoly0, ref contact0);
|
||||||
|
|
||||||
|
// #116 shape-1 instrumentation (Campaign P final physics slice,
|
||||||
|
// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md
|
||||||
|
// §2.4): the tick-22760 divergence turned out to be a GROUNDED
|
||||||
|
// mover (Contact set), so it dispatches here (Path 5), not the
|
||||||
|
// not-yet-Contact Path 6 the oracle plan's shape-1 hypothesis
|
||||||
|
// targeted. This probe pins exactly which of the four Path-5
|
||||||
|
// sub-branches fires for the failing capture.
|
||||||
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[path5-diag] hit0={hit0} hitPoly0={(hitPoly0 is not null)} hasSphere1={hasSphere1}"));
|
||||||
|
}
|
||||||
|
|
||||||
// A6.P4 door inside-out fix (2026-05-25). Retail distinguishes
|
// A6.P4 door inside-out fix (2026-05-25). Retail distinguishes
|
||||||
// FULL HIT (eax_10 != 0 → step_sphere_up early-return) from
|
// FULL HIT (eax_10 != 0 → step_sphere_up early-return) from
|
||||||
// NEAR-MISS (eax_10 == 0 but var_5c != 0 → set_neg_poly_hit).
|
// NEAR-MISS (eax_10 == 0 but var_5c != 0 → set_neg_poly_hit).
|
||||||
|
|
@ -2107,6 +2131,14 @@ public static class BSPQuery
|
||||||
bool hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
|
bool hit1 = SphereIntersectsPolyInternal(root, resolved, sphere1, movement,
|
||||||
ref hitPoly1, ref contact1);
|
ref hitPoly1, ref contact1);
|
||||||
|
|
||||||
|
// #116 shape-1 instrumentation (see the hit0/hitPoly0 probe
|
||||||
|
// above for the full citation).
|
||||||
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[path5-diag] hit1={hit1} hitPoly1={(hitPoly1 is not null)}"));
|
||||||
|
}
|
||||||
|
|
||||||
if (hit1)
|
if (hit1)
|
||||||
{
|
{
|
||||||
// Sphere 1 (head) full hit → slide_sphere.
|
// Sphere 1 (head) full hit → slide_sphere.
|
||||||
|
|
@ -2234,33 +2266,38 @@ public static class BSPQuery
|
||||||
|
|
||||||
if (hit1 || hitPoly1 is not null)
|
if (hit1 || hitPoly1 is not null)
|
||||||
{
|
{
|
||||||
|
// #116 shape-1 fix (Campaign P final physics slice,
|
||||||
|
// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md
|
||||||
|
// §2.3-§2.4). Retail's airborne (not-yet-Contact)
|
||||||
|
// BSPTREE::find_collisions dispatch does NOT defer a
|
||||||
|
// foot-clear/head-hit through SetCollide+Adjusted (nor
|
||||||
|
// through the steep-poly slide-tangent shortcut) — it
|
||||||
|
// records the head polygon's normal directly and hard-
|
||||||
|
// stops, unconditionally, regardless of the poly's
|
||||||
|
// steepness:
|
||||||
|
// localtoglobalvec(sphere_path.localspace_pos, &n, &poly->plane.N);
|
||||||
|
// COLLISIONINFO::set_collision_normal(&collision_info, &n);
|
||||||
|
// return 2; // COLLIDED_TS
|
||||||
|
// (acclient_2013_pseudo_c.txt:323824-323834,
|
||||||
|
// 0x0053a793/0x0053a7a4). Independently cross-checked
|
||||||
|
// against ACE's clean-language port, BSPTree.cs:221-230
|
||||||
|
// (`collisions.SetCollisionNormal(collisionNormal);
|
||||||
|
// return TransitionState.Collided;`) — an exact
|
||||||
|
// structural match confirming this isn't a BN
|
||||||
|
// misdecompile. This was the tick-22760 divergence:
|
||||||
|
// acdream's foot-clear/head-hit case previously fell
|
||||||
|
// through to SetCollide (deferred Adjusted, no
|
||||||
|
// CollisionNormal write) or the steep-poly Slid
|
||||||
|
// shortcut, so ValidateTransition's UnitZ default
|
||||||
|
// clobbered the real door-face normal before the
|
||||||
|
// caller ever saw it.
|
||||||
var worldNormal1 = L2W(hitPoly1!.Plane.Normal);
|
var worldNormal1 = L2W(hitPoly1!.Plane.Normal);
|
||||||
|
|
||||||
// L.4 slide-tangent: same steep-poly slide for head-sphere.
|
collisions.SetCollisionNormal(worldNormal1);
|
||||||
if (worldNormal1.Z < PhysicsGlobals.FloorZ)
|
|
||||||
{
|
|
||||||
Vector3 currWorld = path.GlobalCurrCenter[0].Origin;
|
|
||||||
Vector3 endWorld = path.GlobalSphere[0].Origin;
|
|
||||||
Vector3 gDelta = endWorld - currWorld;
|
|
||||||
float diff = Vector3.Dot(worldNormal1, gDelta);
|
|
||||||
if (diff < 0f)
|
|
||||||
path.AddOffsetToCheckPos(-worldNormal1 * diff);
|
|
||||||
|
|
||||||
collisions.SetCollisionNormal(worldNormal1);
|
|
||||||
collisions.SetSlidingNormal(worldNormal1);
|
|
||||||
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
|
|
||||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
|
||||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Head sphere hit shallow surface: SetCollide.
|
|
||||||
path.SetCollide(worldNormal1);
|
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
|
||||||
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
|
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
|
||||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
|
PhysicsDiagnostics.LastBspHitPoly = hitPoly1;
|
||||||
return TransitionState.Adjusted;
|
return TransitionState.Collided;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue