diff --git a/docs/research/2026-07-30-ts4-116-oracle-plan.md b/docs/research/2026-07-30-ts4-116-oracle-plan.md index b209594f..466a4275 100644 --- a/docs/research/2026-07-30-ts4-116-oracle-plan.md +++ b/docs/research/2026-07-30-ts4-116-oracle-plan.md @@ -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 `0.99999536f` with `0.98480775f` (cos 10°), cite this addendum, retire 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. diff --git a/src/AcDream.Core/Physics/BSPQuery.cs b/src/AcDream.Core/Physics/BSPQuery.cs index a66c5d90..7e314906 100644 --- a/src/AcDream.Core/Physics/BSPQuery.cs +++ b/src/AcDream.Core/Physics/BSPQuery.cs @@ -1854,6 +1854,17 @@ public static class BSPQuery 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. // Emitted before path selection so the captured state reflects // 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, 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 // FULL HIT (eax_10 != 0 → step_sphere_up early-return) from // 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, 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) { // Sphere 1 (head) full hit → slide_sphere. @@ -2234,33 +2266,38 @@ public static class BSPQuery 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); - // L.4 slide-tangent: same steep-poly slide for head-sphere. - 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; + collisions.SetCollisionNormal(worldNormal1); // L.2d slice 1 (2026-05-13): diagnostic side-channel. if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled) PhysicsDiagnostics.LastBspHitPoly = hitPoly1; - return TransitionState.Adjusted; + return TransitionState.Collided; } } }