fix(physics): TS-4 retired — Path-6 steep-poly shortcut deleted
Per docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2 (the
decisive TS-4 confirming run). Retail's BSP layer has NO steepness test at
all (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793) — every airborne
hit, steep or shallow, falls through to the same unconditional
`SetCollide` + `Adjusted`. The L.4 slide-tangent shortcut (worldNormal.Z <
FloorZ -> project-and-Slid, with its own SetSlidingNormal write) is deleted
from both BSPQuery.cs's and FlatBspQuery.cs's Path 6 sphere0 branch.
Fixing FlatBspQuery.cs (the flat/indexed engine Slice I6/I7 made
production-authoritative) was necessary in this same commit: it carried an
exact structural duplicate of the shortcut, caught by
FlatBspQueryDifferentialTests.InstalledDat_LargeRandomizedSweep_HasZeroBitMismatch
(graph=Adjusted vs flat=Slid) once the graph side was fixed alone. Its
sphere1 branch is also brought in line with the #116 shape-1 fix landed
in db2889af (direct Collided + SetCollisionNormal instead of the deferred
SetCollide/shortcut treatment) — that parity gap existed since shape-1's
commit only touched BSPQuery.cs and the randomized differential sweep
didn't happen to exercise the narrow foot-clear/head-hit case until this
session's broader change surfaced it.
DECISIVE CONFIRMING RUN (Ts4SteepRoofWedgeCaptureTests, per the plan's own
required test-first order): added
FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor
— the same steep-roof drop as the existing pure-vertical fixture, but with
a small residual horizontal velocity (vx=-0.3 m/s), matching the realistic
live-play input (WASD, jump momentum) that validated the shortcut on
2026-04-30. With the shortcut removed, this variant converges cleanly to
the flat floor with zero freeze. The pure-vertical fixture, run
shortcut-removed, DOES still freeze — per the oracle plan's root-cause
trace (§1.2 Step E), this is the DEGENERATE case: AdjustOffset's crease
projection (Cross(ContactPlane.Normal, SlidingNormal)) is mathematically
orthogonal to a purely-Z gravity offset, crushing it to zero every tick
before TransitionalInsert can run again — present identically in the raw
decomp, ACE's port, and this port. Renamed and re-asserted as a PINNED
known-degenerate test
(FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity)
rather than treated as a bug. Filed as register row AD-56.
BSPStepUpTests.C3_Path6_AirborneMoverHitsSteepSlope_ReturnsSlid pinned the
OLD shortcut's Slid-no-Collide behavior directly; renamed to
...ReturnsAdjustedAndSetsCollide and corrected to the retail-faithful
Adjusted/Collide=true outcome.
Register: TS-4 row retired (struck through, retirement note); AD-56 filed
for the pure-vertical degenerate case; the retire-next shortlist's TS-4
entry removed and renumbered.
Full AcDream.Core.Tests suite: 4060 passed / 2 skipped (D4 stays Skip-tagged
in this commit; its own un-skip is a separate, dependent test-only commit
for #116 shape-2), no regressions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
db2889afda
commit
5e2be19b4e
5 changed files with 252 additions and 154 deletions
|
|
@ -62,13 +62,14 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Adaptation (AD) — 46 rows (AD-25 retired 2026-07-30 at Campaign P Slice P3 — the remote dead-reckoning post-resolve now calls the exact ported `PhysicsObjUpdate.HandleAllCollisions` (the same function the local player and every ordinary body already use) instead of its own hand-inlined, narrower reflect gate; the row's own premise ("the remote DR sweep hasn't been rebuilt yet") no longer holds; AD-55 filed 2026-07-30 at Campaign P Slice P2, split out of the retired AP-7 row — the open cos(10°)-vs-0.99999536f Sledding slope-flatness constant; AD-53/AD-54 filed the same slice, split out of the retired TS-1 row — CliffSlide's three-source reference-normal fallback chain and the walkable-steepness reroute to CliffSlide; AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover)
|
## 2. Adaptation (AD) — 47 rows (AD-56 filed 2026-07-30 at the Campaign P final physics slice, split out of the retired TS-4 row — the narrow pure-vertical (zero horizontal velocity) steep-roof-landing freeze, confirmed retail-identical; AD-25 retired 2026-07-30 at Campaign P Slice P3 — the remote dead-reckoning post-resolve now calls the exact ported `PhysicsObjUpdate.HandleAllCollisions` (the same function the local player and every ordinary body already use) instead of its own hand-inlined, narrower reflect gate; the row's own premise ("the remote DR sweep hasn't been rebuilt yet") no longer holds; AD-55 filed 2026-07-30 at Campaign P Slice P2, split out of the retired AP-7 row — the open cos(10°)-vs-0.99999536f Sledding slope-flatness constant; AD-53/AD-54 filed the same slice, split out of the retired TS-1 row — CliffSlide's three-source reference-normal fallback chain and the walkable-steepness reroute to CliffSlide; AD-52 filed 2026-07-29 at Campaign N slice N6 — the fragment-assembler 60 s partial TTL + completed-sequence ring; AD-51 filed 2026-07-29 at Campaign N slice N4 — the reclaimed-word pool for ACE's fresh-sequence cleartext RejectRetransmit; AD-50 filed 2026-07-29 at Campaign N slice N2 — the inbound-watermark ACE init; AD-49 stays reserved for Campaign N §5's blob-layer ordering deferral, filed when its slice lands; AD-47 and AD-48 filed 2026-07-29 at Campaign V slice V11 — the MSAA sample-position and present-pacing rows the campaign's risk register scheduled for the GL deletion; AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover)
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| AD-53 | `Transition.CliffSlide`'s reference-normal cross-product operand tries THREE sources in priority order (`LastWalkablePlane` if `Normal.Z >= FloorZ`, then `LastKnownContactPlane` at the same threshold, then world-up `UnitZ`); retail's `CTransition::cliff_slide` uses `this->collision_info.last_known_contact_plane.N` directly, with no fallback chain | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`, the `referenceNormal`/`refSource` selection above the cross-product) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). A fresh read of `last_known_contact_plane`'s own maintenance (pc:272659-272668) confirms retail overwrites it unconditionally from `contact_plane` every `validate_transition` pass — including with a steep plane — so retail keeps NO separately-preserved flat-ground history there either; this fallback chain is a genuine acdream invention, not a retail-matching read. Kept because it compensates for AP-4's incomplete `OnWalkable` bookkeeping (L.4-cliffslide-fallback, 2026-04-30): without it, `cross(currentSteep, lastKnownSteep)` degenerates to zero after >1 frame on a continuous steep slope, and CliffSlide returns `OK` (no deflection) instead of downhill drift — the "stay on the roof" wedge the L.4 session fought | If AP-4's `OnWalkable` reordering is ever completed/removed, `last_known_contact_plane` should carry the same information retail's does and this fallback chain becomes unneeded ballast (or, worse, silently picks a stale `LastWalkablePlane` over the now-correct current one) — re-audit together with AP-4 | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #2 |
|
| AD-53 | `Transition.CliffSlide`'s reference-normal cross-product operand tries THREE sources in priority order (`LastWalkablePlane` if `Normal.Z >= FloorZ`, then `LastKnownContactPlane` at the same threshold, then world-up `UnitZ`); retail's `CTransition::cliff_slide` uses `this->collision_info.last_known_contact_plane.N` directly, with no fallback chain | `src/AcDream.Core/Physics/TransitionTypes.cs` (`CliffSlide`, the `referenceNormal`/`refSource` selection above the cross-product) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). A fresh read of `last_known_contact_plane`'s own maintenance (pc:272659-272668) confirms retail overwrites it unconditionally from `contact_plane` every `validate_transition` pass — including with a steep plane — so retail keeps NO separately-preserved flat-ground history there either; this fallback chain is a genuine acdream invention, not a retail-matching read. Kept because it compensates for AP-4's incomplete `OnWalkable` bookkeeping (L.4-cliffslide-fallback, 2026-04-30): without it, `cross(currentSteep, lastKnownSteep)` degenerates to zero after >1 frame on a continuous steep slope, and CliffSlide returns `OK` (no deflection) instead of downhill drift — the "stay on the roof" wedge the L.4 session fought | If AP-4's `OnWalkable` reordering is ever completed/removed, `last_known_contact_plane` should carry the same information retail's does and this fallback chain becomes unneeded ballast (or, worse, silently picks a stale `LastWalkablePlane` over the now-correct current one) — re-audit together with AP-4 | `CTransition::cliff_slide` pc:272397 (0050a6d0); `last_known_contact_plane` maintenance pc:272659-272668 (~0050ad07); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #2 |
|
||||||
| AD-54 | `Transition.EdgeSlideAfterStepDownFailed` reroutes to `CliffSlide` instead of `PrecipiceSlide` when the stored walkable polygon itself is steeper than `FloorZ` (`sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ`); retail's raw `SPHEREPATH::edge_slide` has no steepness branch here — `if (walkable != null) { ... precipice_slide(...) }` unconditionally | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`, the `L.4-walkable-steep` block) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). The permissive `LandingZ` walkable-acceptance threshold that lets a steep roof become "walkable" in the first place IS confirmed retail-faithful (`BSPTREE::find_collisions` unconditional `walkable_allowance = LandingZ`, pc:323740-323783, TS-4's own citation) — so a steep-roof walkable polygon is a real state retail also reaches. What is NOT independently verified is whether retail's outer `transitional_insert` caller absorbs a same-polygon-standing `COLLIDED_TS` from `precipice_slide` (its raw `find_crossed_edge` returning false while standing on, not crossing, the polygon) some other way that avoids the acdream "stuck in a Collided revert loop" this reroute prevents | If retail's outer retry loop turns out to already handle the no-crossed-edge-while-standing-on-a-steep-poly case without a reroute, this branch is an unnecessary compensating layer that could route a genuinely PrecipiceSlide-bound case (a shallow polygon edge that happens to sit at exactly `FloorZ`) into CliffSlide instead | `SPHEREPATH::edge_slide` pc:273001-273090 (0050b3d0, direct walkable branch quoted at pc:364-370 in the P2 research doc); `BSPTREE::find_collisions` pc:323740-323783 (0053a730, unconditional `LandingZ`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #3 |
|
| AD-54 | `Transition.EdgeSlideAfterStepDownFailed` reroutes to `CliffSlide` instead of `PrecipiceSlide` when the stored walkable polygon itself is steeper than `FloorZ` (`sp.WalkablePlane.Normal.Z < PhysicsGlobals.FloorZ`); retail's raw `SPHEREPATH::edge_slide` has no steepness branch here — `if (walkable != null) { ... precipice_slide(...) }` unconditionally | `src/AcDream.Core/Physics/TransitionTypes.cs` (`EdgeSlideAfterStepDownFailed`, the `L.4-walkable-steep` block) | Filed 2026-07-30 splitting TS-1's retirement (Campaign P Slice P2). The permissive `LandingZ` walkable-acceptance threshold that lets a steep roof become "walkable" in the first place IS confirmed retail-faithful (`BSPTREE::find_collisions` unconditional `walkable_allowance = LandingZ`, pc:323740-323783, TS-4's own citation) — so a steep-roof walkable polygon is a real state retail also reaches. What is NOT independently verified is whether retail's outer `transitional_insert` caller absorbs a same-polygon-standing `COLLIDED_TS` from `precipice_slide` (its raw `find_crossed_edge` returning false while standing on, not crossing, the polygon) some other way that avoids the acdream "stuck in a Collided revert loop" this reroute prevents | If retail's outer retry loop turns out to already handle the no-crossed-edge-while-standing-on-a-steep-poly case without a reroute, this branch is an unnecessary compensating layer that could route a genuinely PrecipiceSlide-bound case (a shallow polygon edge that happens to sit at exactly `FloorZ`) into CliffSlide instead | `SPHEREPATH::edge_slide` pc:273001-273090 (0050b3d0, direct walkable branch quoted at pc:364-370 in the P2 research doc); `BSPTREE::find_collisions` pc:323740-323783 (0053a730, unconditional `LandingZ`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2 gap #3 |
|
||||||
| AD-55 | `calc_friction`'s Sledding slope-flatness test compares `GroundNormal.Z > 0.99999536f` (≈0.175° from flat); the raw retail decomp literally computes `__fcos(0.17453292519943295)` (= cos(10°) ≈ 0.984808) and compares that against `contact_plane.N.z` — physically very different tests (0.175° accepts only essentially-perfectly-flat ground; 10° accepts any modest slope) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`, the Sledding near-flat branch) | Filed 2026-07-30 splitting AP-7's retirement (Campaign P Slice P2). Two hypotheses, neither confirmed this pass: (a) BN misdecompiled a raw float-constant load as an `__fcos()` call (a known BN artifact class), or (b) ACE's own port made an independent error and cos(10°) is correct. `0.99999536f` is kept provisionally — least churn, since it is what acdream's own prior (structurally unreachable) dead code already had — pending a live Ghidra decompile of `0050ee70` checking whether the FCOS opcode is real or a raw `FLD` of one of these two constants | Currently harmless in production: nothing sets `PhysicsState.Sledding` client-side (see #166 research), so this branch is unreachable either way. The moment a data-authored Sledding toggle exists, the wrong constant changes which slopes get the light 0.2f sled-friction override vs. the heavier default | `CPhysicsObj::calc_friction` pc:276694-276822 (0050ee70), the `__fcos(0.17453292519943295)` slope-flatness comparison; ACE `PhysicsObj.calc_friction` PhysicsObj.cs:2120-2141 (`0.99999536f`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §1, §7 item 3 |
|
| AD-55 | `calc_friction`'s Sledding slope-flatness test compares `GroundNormal.Z > 0.99999536f` (≈0.175° from flat); the raw retail decomp literally computes `__fcos(0.17453292519943295)` (= cos(10°) ≈ 0.984808) and compares that against `contact_plane.N.z` — physically very different tests (0.175° accepts only essentially-perfectly-flat ground; 10° accepts any modest slope) | `src/AcDream.Core/Physics/PhysicsBody.cs` (`calc_friction`, the Sledding near-flat branch) | Filed 2026-07-30 splitting AP-7's retirement (Campaign P Slice P2). Two hypotheses, neither confirmed this pass: (a) BN misdecompiled a raw float-constant load as an `__fcos()` call (a known BN artifact class), or (b) ACE's own port made an independent error and cos(10°) is correct. `0.99999536f` is kept provisionally — least churn, since it is what acdream's own prior (structurally unreachable) dead code already had — pending a live Ghidra decompile of `0050ee70` checking whether the FCOS opcode is real or a raw `FLD` of one of these two constants | Currently harmless in production: nothing sets `PhysicsState.Sledding` client-side (see #166 research), so this branch is unreachable either way. The moment a data-authored Sledding toggle exists, the wrong constant changes which slopes get the light 0.2f sled-friction override vs. the heavier default | `CPhysicsObj::calc_friction` pc:276694-276822 (0050ee70), the `__fcos(0.17453292519943295)` slope-flatness comparison; ACE `PhysicsObj.calc_friction` PhysicsObj.cs:2120-2141 (`0.99999536f`); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §1, §7 item 3 |
|
||||||
|
| AD-56 | A body falling PERFECTLY PLUMB (zero horizontal velocity) onto a steep-but-below-FloorZ polygon (LandingZ-permissive, e.g. a steep roof) freezes at its landing position forever once Path 6's steep-poly shortcut is removed (TS-4). `AdjustOffset`'s crease projection (`Cross(ContactPlane.Normal, SlidingNormal)`) against a purely-Z gravity offset is mathematically annihilated (`Dot(slideOffset, offset) = 0` exactly, since `slideOffset.Z = 0` and the offset is purely Z), tripping the abort-small-offset guard before `TransitionalInsert` can run again | `src/AcDream.Core/Physics/TransitionTypes.cs` (`AdjustOffset`'s crease-projection math, shared by every mover); pinned by `tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs` (`FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity`) | Filed 2026-07-30 at the Campaign P final physics slice, split out of the retired TS-4 row. This is not a code bug: the mechanism is present identically in the raw retail decomp, ACE's port, and this port (`docs/research/2026-07-30-ts4-116-oracle-plan.md` §1.2 Step E, §1.3) — every one of the three references crushes a purely-vertical offset to zero the same way. A live player almost never produces this exact input: WASD, camera-relative movement, and even small numerical noise inject some horizontal component, which the SAME cross product does NOT annihilate (only a component exactly along the downhill/gravity line is removed) — confirmed by the decisive companion fixture `FallOntoSteepSlope_WithHorizontalVelocity_...`, which converges cleanly with a mere ±0.3 m/s residual horizontal component | A hypothetical mover that manages a truly zero-horizontal-velocity approach to a steep-but-LandingZ-permissive surface (a vertical-drop elevator platform, a scripted teleport landing) would freeze identically to retail; not reachable by ordinary player/NPC movement | `CTransition::adjust_offset` pc:272271-272393 (0x0050a370); `docs/research/2026-07-30-ts4-116-oracle-plan.md` §1.2 Step E, §1.3, §1.5 |
|
||||||
| AD-46 | **LIVE. Reframed at Campaign V slice V11 (2026-07-29), when GL was deleted and the comparison that discovered this row ceased to exist.** Dense alpha-blended distant scenery (the treeline) may read slightly denser than retail's, because the anisotropic TAP PATTERN is implementation-defined and acdream's Vulkan driver does not tap identically to retail's D3D9 one. Both request the same sampler state — trilinear, clamp-and-repeat, the device's maximum anisotropy. **What changed at V11 is only the left-hand side of the comparison**: this was measured GL-vs-Vulkan (~15% of the pixels in the band), and it is now a Vulkan-vs-retail question against the D3D oracle in the last column. The measurement below is retained as the evidence that the residual is a tap pattern and not a bug, even though one of its two arms no longer exists. | `src/AcDream.App/Rendering/Wb/WorldTextureArray.cs` (`RhiWorldTextureArray.WorldArrayAnisotropy`); measured in plan §5.5.19, reframed §5.5.24 | Not assumed — narrowed by measurement while both backends still existed, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's `m_D3DCaps.MaxAnisotropy`) → 22,266, and the rest of the frame fell to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual was not a sub-pixel shift (an integer shift search found none), not a sharpness change (high-frequency energy matched within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what made it a driver property rather than a bug. | Distant foliage shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. **Now unfalsifiable by self-differential** — with GL gone, the only way to retire this row is a side-by-side against the retail client, not against another acdream backend. | `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`, whose `SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy)` at `0x005a4230` is the value acdream requests |
|
| AD-46 | **LIVE. Reframed at Campaign V slice V11 (2026-07-29), when GL was deleted and the comparison that discovered this row ceased to exist.** Dense alpha-blended distant scenery (the treeline) may read slightly denser than retail's, because the anisotropic TAP PATTERN is implementation-defined and acdream's Vulkan driver does not tap identically to retail's D3D9 one. Both request the same sampler state — trilinear, clamp-and-repeat, the device's maximum anisotropy. **What changed at V11 is only the left-hand side of the comparison**: this was measured GL-vs-Vulkan (~15% of the pixels in the band), and it is now a Vulkan-vs-retail question against the D3D oracle in the last column. The measurement below is retained as the evidence that the residual is a tap pattern and not a bug, even though one of its two arms no longer exists. | `src/AcDream.App/Rendering/Wb/WorldTextureArray.cs` (`RhiWorldTextureArray.WorldArrayAnisotropy`); measured in plan §5.5.19, reframed §5.5.24 | Not assumed — narrowed by measurement while both backends still existed, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's `m_D3DCaps.MaxAnisotropy`) → 22,266, and the rest of the frame fell to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual was not a sub-pixel shift (an integer shift search found none), not a sharpness change (high-frequency energy matched within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what made it a driver property rather than a bug. | Distant foliage shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. **Now unfalsifiable by self-differential** — with GL gone, the only way to retire this row is a side-by-side against the retail client, not against another acdream backend. | `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`, whose `SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy)` at `0x005a4230` is the value acdream requests |
|
||||||
| AD-47 | **Filed at Campaign V slice V11 (2026-07-29); the campaign's risk register scheduled this row here.** Multisample resolve sample POSITIONS are unspecified by both the Vulkan and D3D9 specifications, so acdream's MSAA-on silhouette edges do not match retail's pixel-for-pixel even at the same sample count. acdream's strict pixel gates therefore run with MSAA forced OFF on every arm, and MSAA-on gets only a relaxed visual smoke. | `src/AcDream.App/RuntimeOptions.cs` (`ACDREAM_MSAA_SAMPLES`); forced to 0 in `tools/run-offline-pixel-gate.ps1` | Measured, not assumed: plan §5.5.16 compared two backends at 4x and found **8.83% of the frame differing — 81,359 px of 921,600 — essentially all of it hugging foliage and silhouette edges**, which is ninety-fold over the 0.001 gate threshold. That is two implementations' sample patterns, not a renderer divergence, which is why forcing MSAA off is what makes the remaining difference attributable rather than a threshold relaxation. | Edge quality on thin geometry (fence rails, foliage, distant railings) differs from retail at the sub-pixel level whenever MSAA is on, which is the ordinary player configuration. Because the gates run MSAA off, **a real regression confined to the multisample path would not be caught by them** — that is the actual exposure this row records. | D3D9 `D3DRS_MULTISAMPLEANTIALIAS` / `D3DMULTISAMPLE_TYPE` as set by `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`; retail's sample pattern is the driver's, exactly as ours is |
|
| AD-47 | **Filed at Campaign V slice V11 (2026-07-29); the campaign's risk register scheduled this row here.** Multisample resolve sample POSITIONS are unspecified by both the Vulkan and D3D9 specifications, so acdream's MSAA-on silhouette edges do not match retail's pixel-for-pixel even at the same sample count. acdream's strict pixel gates therefore run with MSAA forced OFF on every arm, and MSAA-on gets only a relaxed visual smoke. | `src/AcDream.App/RuntimeOptions.cs` (`ACDREAM_MSAA_SAMPLES`); forced to 0 in `tools/run-offline-pixel-gate.ps1` | Measured, not assumed: plan §5.5.16 compared two backends at 4x and found **8.83% of the frame differing — 81,359 px of 921,600 — essentially all of it hugging foliage and silhouette edges**, which is ninety-fold over the 0.001 gate threshold. That is two implementations' sample patterns, not a renderer divergence, which is why forcing MSAA off is what makes the remaining difference attributable rather than a threshold relaxation. | Edge quality on thin geometry (fence rails, foliage, distant railings) differs from retail at the sub-pixel level whenever MSAA is on, which is the ordinary player configuration. Because the gates run MSAA off, **a real regression confined to the multisample path would not be caught by them** — that is the actual exposure this row records. | D3D9 `D3DRS_MULTISAMPLEANTIALIAS` / `D3DMULTISAMPLE_TYPE` as set by `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`; retail's sample pattern is the driver's, exactly as ours is |
|
||||||
| AD-48 | **Filed at Campaign V slice V11 (2026-07-29).** Presentation is paced by the Vulkan swapchain present mode (FIFO, i.e. VSync) or by a refresh-rate software pacer when uncapped, rather than by retail's D3D9 `Present` with its own frame-rate limiter. Frame delivery cadence, and therefore input-to-photon latency, is a property of our present path rather than a port of retail's. | `src/AcDream.App/RuntimeOptions.cs:98-100`; `src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs` | Retail's limiter and ours both bound the frame rate to the display; the simulation is fixed-step and clock-driven, so gameplay timing does not ride on presentation cadence. The uncapped path exists for measurement and is not the shipping default. | A pacing mismatch shows up as judder or input latency that differs from retail's feel without any visual difference in a captured frame — invisible to every pixel gate by construction. Issue **#235** (the capped/RDP jump-presentation cadence alias) is the known live instance of this class. | D3D9 `IDirect3DDevice9::Present`; retail's frame limiter in `RenderDeviceD3D` |
|
| AD-48 | **Filed at Campaign V slice V11 (2026-07-29).** Presentation is paced by the Vulkan swapchain present mode (FIFO, i.e. VSync) or by a refresh-rate software pacer when uncapped, rather than by retail's D3D9 `Present` with its own frame-rate limiter. Frame delivery cadence, and therefore input-to-photon latency, is a property of our present path rather than a port of retail's. | `src/AcDream.App/RuntimeOptions.cs:98-100`; `src/AcDream.App/Rendering/Gpu/Vk/VulkanSwapchain.cs` | Retail's limiter and ours both bound the frame rate to the display; the simulation is fixed-step and clock-driven, so gameplay timing does not ride on presentation cadence. The uncapped path exists for measurement and is not the shipping default. | A pacing mismatch shows up as judder or input latency that differs from retail's feel without any visual difference in a captured frame — invisible to every pixel gate by construction. Issue **#235** (the capped/RDP jump-presentation cadence alias) is the known live instance of this class. | D3D9 `IDirect3DDevice9::Present`; retail's frame limiter in `RenderDeviceD3D` |
|
||||||
|
|
@ -240,7 +241,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| ~~TS-1~~ | **RETIRED 2026-07-30 (Campaign P Slice P2) — the row was stale, not the code.** The cited `:1254` line is unrelated stepping-loop code; the file moved substantially since the row was written. Retail's `EdgeSlide → PrecipiceSlide / CliffSlide` chain is already a real, tested port: `SpherePath.PrecipiceSlide` (`TransitionTypes.cs:943-970`, retail `SPHEREPATH::precipice_slide` pc:274316), `Transition.CliffSlide` (`:2080-2164`, retail `CTransition::cliff_slide` pc:272397, return-value mapping verified against `acclient.h:6100-6108`), and `Transition.EdgeSlideAfterStepDownFailed` (`:1907-2078`, mirrors `CTransition::edge_slide` pc:273001-273090). The one real gap (back-probe fallback skipping retail's `walkable_check_pos`/`localspace_sphere` recache, pc:274318-274326) needed no code change: acdream's `WalkableVertices`/`GlobalSphere` are populated in unified world space at assignment time (`SetWalkable`/`SetWalkableTransformed`, `SetCheckPos`/`RestoreCheckPos`), so both operands `BSPQuery.FindCrossedEdge` compares are already commensurable — retail's per-cell local-frame reprojection is a no-op correction here. Documented in-code at the back-probe site and pinned by `EdgeSlideBackProbePrecipiceSlideTests`. The chain's two acdream-only compensating branches (CliffSlide's three-source reference-normal fallback; the walkable-steepness reroute to CliffSlide before PrecipiceSlide) are real, non-retail additions — filed as AD-53 / AD-54 rather than folded into this row. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SpherePath.PrecipiceSlide`, `Transition.CliffSlide`, `Transition.EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs` | — | — | `SPHEREPATH::precipice_slide` pc:274316 (0050cc80); `CTransition::cliff_slide` pc:272397 (0050a6d0); `CTransition::edge_slide` pc:273001-273090 (0050b3d0); `SPHEREPATH::get_walkable_pos`/`cache_localspace_sphere`/`set_walkable_check_pos` pc:274318-274326 (0050a8f0/0050c9d0/00509ce0); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2, §6 Step 1 |
|
| ~~TS-1~~ | **RETIRED 2026-07-30 (Campaign P Slice P2) — the row was stale, not the code.** The cited `:1254` line is unrelated stepping-loop code; the file moved substantially since the row was written. Retail's `EdgeSlide → PrecipiceSlide / CliffSlide` chain is already a real, tested port: `SpherePath.PrecipiceSlide` (`TransitionTypes.cs:943-970`, retail `SPHEREPATH::precipice_slide` pc:274316), `Transition.CliffSlide` (`:2080-2164`, retail `CTransition::cliff_slide` pc:272397, return-value mapping verified against `acclient.h:6100-6108`), and `Transition.EdgeSlideAfterStepDownFailed` (`:1907-2078`, mirrors `CTransition::edge_slide` pc:273001-273090). The one real gap (back-probe fallback skipping retail's `walkable_check_pos`/`localspace_sphere` recache, pc:274318-274326) needed no code change: acdream's `WalkableVertices`/`GlobalSphere` are populated in unified world space at assignment time (`SetWalkable`/`SetWalkableTransformed`, `SetCheckPos`/`RestoreCheckPos`), so both operands `BSPQuery.FindCrossedEdge` compares are already commensurable — retail's per-cell local-frame reprojection is a no-op correction here. Documented in-code at the back-probe site and pinned by `EdgeSlideBackProbePrecipiceSlideTests`. The chain's two acdream-only compensating branches (CliffSlide's three-source reference-normal fallback; the walkable-steepness reroute to CliffSlide before PrecipiceSlide) are real, non-retail additions — filed as AD-53 / AD-54 rather than folded into this row. | `src/AcDream.Core/Physics/TransitionTypes.cs` (`SpherePath.PrecipiceSlide`, `Transition.CliffSlide`, `Transition.EdgeSlideAfterStepDownFailed`); `tests/AcDream.Core.Tests/Physics/EdgeSlideBackProbePrecipiceSlideTests.cs` | — | — | `SPHEREPATH::precipice_slide` pc:274316 (0050cc80); `CTransition::cliff_slide` pc:272397 (0050a6d0); `CTransition::edge_slide` pc:273001-273090 (0050b3d0); `SPHEREPATH::get_walkable_pos`/`cache_localspace_sphere`/`set_walkable_check_pos` pc:274318-274326 (0050a8f0/0050c9d0/00509ce0); `docs/research/2026-07-30-response-layer-edge-family-pseudocode.md` §2, §6 Step 1 |
|
||||||
| TS-4 | Path-6 steep-poly slide-tangent shortcut: airborne hits on >FloorZ polys skip retail's SetCollide → Path-4 → ContactPlane landing chain, returning Slid in place. **Includes a `SetSlidingNormal` write at both sites** — retail's BSP layer never writes `collision_info.sliding_normal` (only `validate_transition` 0x0050ac21 does; the #137 mechanism-2 class), so on transition success the steep-face normal persists to the body and seeds the next frame | `src/AcDream.Core/Physics/BSPQuery.cs` (Path-6 steep branches, `worldNormal.Z < FloorZ`) | Deliberate deviation: our faithful port DID wedge (missing step_up_slide / cliff_slide details on grounded-steep); validated against the 2026-04-30 retail cdb trace (retail body didn't wedge). Filed L.5+ for retail-strict | Airborne steep contact never commits Contact / lands as retail — roof-bounce trajectories, landing events, grounded-steep transitions diverge; a persisted steep-face normal can absorb an exactly-anti-parallel next-frame push (#137 wedge class) until an oblique input clears it | `BSPTREE::find_collisions` SetCollide pc:323783-323821 |
|
| ~~TS-4~~ | **RETIRED 2026-07-30 (Campaign P final physics slice) — the Path-6 steep-poly slide-tangent shortcut is deleted from both `BSPQuery.cs` and `FlatBspQuery.cs`.** Every airborne hit (steep or shallow) now falls through to the same unconditional `path.SetCollide(worldNormal); path.WalkableAllowance = LandingZ; return Adjusted;`, matching retail's `BSPTREE::find_collisions` exactly (`acclient_2013_pseudo_c.txt:323783-323821`, `0x0053a793` — no steepness test at the BSP layer at all). The decisive confirming run (`Ts4SteepRoofWedgeCaptureTests`) shows a steep-roof fall carrying a small residual horizontal velocity (the realistic case — WASD input, jump momentum, matching the original 2026-04-30 live repro) converges cleanly to the flat floor with no freeze once the shortcut is removed. Only a PERFECTLY PLUMB drop (zero horizontal velocity, a practically unreachable live-play input) still freezes — see AD-56 for that narrow, retail-matching degenerate case, pinned rather than fixed. | `src/AcDream.Core/Physics/BSPQuery.cs`, `src/AcDream.Core/Physics/FlatBspQuery.cs` (Path 6); `tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs`; `tests/AcDream.Core.Tests/Physics/BSPStepUpTests.cs` (`C3_Path6_AirborneMoverHitsSteepSlope_ReturnsAdjustedAndSetsCollide`, `D4_AirborneMover_TallWall_PersistsSlidingNormalAcrossFrames`) | — | — | `BSPTREE::find_collisions` SetCollide pc:323783-323821 (0x0053a793); `docs/research/2026-07-30-ts4-116-oracle-plan.md` §1, §4 item 2 |
|
||||||
| TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) |
|
| TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) |
|
||||||
| TS-7 | SkyObject `weather_enabled` gate not honored — weather-flagged sky objects (bit 0x04) always instantiate | `src/AcDream.Core/World/SkyDescLoader.cs:50` | No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | `GameSky::MakeObject` 0x00506ee0, guard at decomp:268630 |
|
| TS-7 | SkyObject `weather_enabled` gate not honored — weather-flagged sky objects (bit 0x04) always instantiate | `src/AcDream.Core/World/SkyDescLoader.cs:50` | No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | `GameSky::MakeObject` 0x00506ee0, guard at decomp:268630 |
|
||||||
| TS-8 | `MagicUpdateEnchantment` (0x02C2) records carry no StatMod — mid-session buffs don't move vital max until relog (**#7/#12**) | `src/AcDream.Core/Spells/Spellbook.cs:150` | The wire parser hasn't been extended to the full ~60-64 byte Enchantment payload; PlayerDescription's block IS parsed | Vitals HUD percent reads differently from retail for the whole session after any buff cast | `EnchantAttribute` 0x00594570; holtburger magic/types.rs |
|
| TS-8 | `MagicUpdateEnchantment` (0x02C2) records carry no StatMod — mid-session buffs don't move vital max until relog (**#7/#12**) | `src/AcDream.Core/Spells/Spellbook.cs:150` | The wire parser hasn't been extended to the full ~60-64 byte Enchantment payload; PlayerDescription's block IS parsed | Vitals HUD percent reads differently from retail for the whole session after any buff cast | `EnchantAttribute` 0x00594570; holtburger magic/types.rs |
|
||||||
|
|
@ -308,13 +309,12 @@ phase-gated — they carry their trigger in their row and should land
|
||||||
WITH that phase, not before.
|
WITH that phase, not before.
|
||||||
|
|
||||||
1. **TS-27 — INBOUND retransmit handling** — the outbound sent-packet cache + resend landed with Campaign N Slice N1 (2026-07-29, class-doc gap list fixed same commit); the inbound sequence-aligned ISAAC + client NAK emission (N2/N4) remain the hard blocker for non-loopback play — one lost S2C packet still deafens the session permanently.
|
1. **TS-27 — INBOUND retransmit handling** — the outbound sent-packet cache + resend landed with Campaign N Slice N1 (2026-07-29, class-doc gap list fixed same commit); the inbound sequence-aligned ISAAC + client NAK emission (N2/N4) remain the hard blocker for non-loopback play — one lost S2C packet still deafens the session permanently.
|
||||||
2. **TS-4 — Path-6 steep slide-tangent shortcut** — landing/contact state diverges on every airborne-steep hit; the L.5+ retail-strict followup is already filed with the missing-ingredient analysis.
|
2. **UN-1 — CheckOtherCells iteration order** — behavior-bearing halt order with a log-cosmetics justification; trivial to fix (iterate CELLARRAY build order, sort only in probe output).
|
||||||
3. **UN-1 — CheckOtherCells iteration order** — behavior-bearing halt order with a log-cosmetics justification; trivial to fix (iterate CELLARRAY build order, sort only in probe output).
|
3. **UN-6 — 200 ms ConnectResponse sleep** — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check.
|
||||||
4. **UN-6 — 200 ms ConnectResponse sleep** — unexplained constant on every login with an intermittent-failure shape; either find the ACE race and cite it, or replace with an acknowledged-ready check.
|
4. **UN-4 — GfxObj sides/negative-surface logic** — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it.
|
||||||
5. **UN-4 — GfxObj sides/negative-surface logic** — diagnose against the retail-cited CellStruct interpretation on a known double-sided GfxObj; promote to AP with a citation or align it.
|
5. **TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12)** — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
|
||||||
6. **TS-8 — MagicUpdateEnchantment StatMod parse (#7/#12)** — vitals wrong for the whole session after any buff; parser shape is known from holtburger.
|
6. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together.
|
||||||
7. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together.
|
7. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging.
|
||||||
8. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging.
|
|
||||||
|
|
||||||
**Phase-gated (do WITH the phase, flagged here so they aren't forgotten):**
|
**Phase-gated (do WITH the phase, flagged here so they aren't forgotten):**
|
||||||
M2 combat must land TS-25
|
M2 combat must land TS-25
|
||||||
|
|
|
||||||
|
|
@ -1979,6 +1979,17 @@ public static class BSPQuery
|
||||||
FindWalkableInternal(root, resolved, path, ref validPos, movement, localSpaceZ,
|
FindWalkableInternal(root, resolved, path, ref validPos, movement, localSpaceZ,
|
||||||
ref hitPoly, ref _hitPolyId, ref changed);
|
ref hitPoly, ref _hitPolyId, ref changed);
|
||||||
|
|
||||||
|
// #116 shape-2 instrumentation (Campaign P final physics slice,
|
||||||
|
// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md
|
||||||
|
// §3.3 step 1): names whether Path 4's find_walkable probe found
|
||||||
|
// a candidate for this specific poly set, settling the D4
|
||||||
|
// first-airborne-frame routing question without a live cdb trace.
|
||||||
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
|
{
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[path4-diag] changed={changed} hitPoly={(hitPoly is not null)}"));
|
||||||
|
}
|
||||||
|
|
||||||
if (changed && hitPoly is not null)
|
if (changed && hitPoly is not null)
|
||||||
{
|
{
|
||||||
// ACE: var offset = LocalToGlobalVec(validPos.Center - localSphere.Center) * scale
|
// ACE: var offset = LocalToGlobalVec(validPos.Center - localSphere.Center) * scale
|
||||||
|
|
@ -2196,57 +2207,35 @@ public static class BSPQuery
|
||||||
|
|
||||||
var worldNormal0 = L2W(hitPoly0!.Plane.Normal);
|
var worldNormal0 = L2W(hitPoly0!.Plane.Normal);
|
||||||
|
|
||||||
// L.4 slide-tangent for steep airborne hits (2026-04-30).
|
// TS-4 RETIRED (Campaign P final physics slice, 2026-07-30;
|
||||||
|
// docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2).
|
||||||
|
// The L.4 steep-poly slide-tangent shortcut that used to live
|
||||||
|
// here (worldNormal0.Z < FloorZ → project-and-Slid,
|
||||||
|
// SetCollisionNormal + SetSlidingNormal) is deleted. Retail's
|
||||||
|
// BSP layer never differentiates by slope here — it calls
|
||||||
|
// SetCollide unconditionally for every hit, steep or shallow
|
||||||
|
// (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793;
|
||||||
|
// no steepness test at this layer at all), and never writes
|
||||||
|
// collision_info.sliding_normal from the BSP/sphere layer —
|
||||||
|
// only validate_transition (0x0050ac21/0x0050aa70) does.
|
||||||
//
|
//
|
||||||
// For polygons too steep to walk on (worldNormal.Z < FloorZ),
|
// The decisive confirming run (Ts4SteepRoofWedgeCaptureTests):
|
||||||
// skip the SetCollide → Path-4 → ContactPlane landing chain.
|
// with the shortcut removed, a steep-roof fall carrying a
|
||||||
// That chain commits the body to the steep surface, leading
|
// small residual horizontal velocity (the realistic case —
|
||||||
// to the "stuck in falling animation on the roof" bug — once
|
// WASD input, jump momentum) converges cleanly to the flat
|
||||||
// grounded with a steep ContactPlane, our step_up_slide /
|
// floor with no freeze
|
||||||
// cliff_slide / edge_slide chain can't produce smooth
|
// (FallOntoSteepSlope_WithHorizontalVelocity_...). Only the
|
||||||
// descent and the body wedges or "falls a bit at a time"
|
// DEGENERATE pure-vertical drop (zero horizontal velocity)
|
||||||
// when bumped.
|
// still freezes — and that freeze is retail-faithful too
|
||||||
|
// (see the pinned FallOntoSteepSlope_NeverFreezesFor... test
|
||||||
|
// and AD-56 for the mechanism: AdjustOffset's crease
|
||||||
|
// projection, Cross(ContactPlane.Normal, SlidingNormal), is
|
||||||
|
// mathematically orthogonal to a purely-Z input offset,
|
||||||
|
// crushing it to zero every tick and short-circuiting before
|
||||||
|
// TransitionalInsert runs again — present identically in the
|
||||||
|
// raw decomp, ACE's port, and this port).
|
||||||
//
|
//
|
||||||
// Instead: project the move along the steep face (remove
|
// ─── SetCollide response (unconditional, retail-faithful) ──
|
||||||
// the into-wall displacement), set CollisionNormal +
|
|
||||||
// SlidingNormal, return Slid. Same shape as Path 5's
|
|
||||||
// step-up fallback (line 1545-1547) and CylinderCollision
|
|
||||||
// (TransitionTypes.cs:1518-1522). Position is updated in-
|
|
||||||
// place; on the next resolver iteration the sphere is
|
|
||||||
// outside the poly, FindCollisions returns OK, and
|
|
||||||
// ValidateTransition commits the new position. Body stays
|
|
||||||
// airborne, falling animation continues, and gravity's
|
|
||||||
// tangent component drifts the body downhill until it
|
|
||||||
// slides off the slope's edge.
|
|
||||||
//
|
|
||||||
// This is a deliberate deviation from retail (retail uses
|
|
||||||
// SetCollide unconditionally and lets find_walkable +
|
|
||||||
// step_up_slide produce the slide). Validated against
|
|
||||||
// retail debugger trace 2026-04-30: retail body did not
|
|
||||||
// wedge; our retail-faithful port DID wedge because we're
|
|
||||||
// missing implementation details of the step_up_slide /
|
|
||||||
// cliff_slide chain on grounded-steep movement. The
|
|
||||||
// slide-tangent here produces user-acceptable behavior
|
|
||||||
// (slides off naturally) while the deeper chain port is
|
|
||||||
// researched. Filed as L.5+ followup for retail-strict.
|
|
||||||
if (worldNormal0.Z < PhysicsGlobals.FloorZ)
|
|
||||||
{
|
|
||||||
Vector3 currWorld = path.GlobalCurrCenter[0].Origin;
|
|
||||||
Vector3 endWorld = path.GlobalSphere[0].Origin;
|
|
||||||
Vector3 gDelta = endWorld - currWorld;
|
|
||||||
float diff = Vector3.Dot(worldNormal0, gDelta);
|
|
||||||
if (diff < 0f)
|
|
||||||
path.AddOffsetToCheckPos(-worldNormal0 * diff);
|
|
||||||
|
|
||||||
collisions.SetCollisionNormal(worldNormal0);
|
|
||||||
collisions.SetSlidingNormal(worldNormal0);
|
|
||||||
// L.2d slice 1 (2026-05-13): diagnostic side-channel.
|
|
||||||
if (PhysicsDiagnostics.ProbeBuildingEnabled || PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
|
||||||
PhysicsDiagnostics.LastBspHitPoly = hitPoly0;
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── SetCollide response (shallow / walkable) ───────────
|
|
||||||
// Per retail (acclient_2013_pseudo_c.txt:323783-323821).
|
// Per retail (acclient_2013_pseudo_c.txt:323783-323821).
|
||||||
path.SetCollide(worldNormal0);
|
path.SetCollide(worldNormal0);
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||||
|
|
|
||||||
|
|
@ -2034,24 +2034,18 @@ internal static class FlatBspQuery
|
||||||
localToWorld);
|
localToWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TS-4 RETIRED (Campaign P final physics slice, 2026-07-30;
|
||||||
|
// docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2).
|
||||||
|
// Retail's BSP layer has no steepness test at this layer at all
|
||||||
|
// (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793) — every
|
||||||
|
// hit (steep or shallow) falls through to the same unconditional
|
||||||
|
// SetCollide. The old L.4 slide-tangent shortcut (worldNormal0.Z
|
||||||
|
// < FloorZ -> project-and-Slid with its own SetSlidingNormal
|
||||||
|
// write) is deleted; see BSPQuery.cs's Path 6 for the mirrored
|
||||||
|
// fix and full citation. Kept in parity with BSPQuery.cs per the
|
||||||
|
// FlatBspQueryDifferentialTests graph-vs-flat comparison.
|
||||||
Vector3 worldNormal0 = LocalToWorld(
|
Vector3 worldNormal0 = LocalToWorld(
|
||||||
tree.PolygonTable.Polygons[defaultHitPolygonIndex0].Plane.Normal);
|
tree.PolygonTable.Polygons[defaultHitPolygonIndex0].Plane.Normal);
|
||||||
if (worldNormal0.Z < PhysicsGlobals.FloorZ)
|
|
||||||
{
|
|
||||||
Vector3 currentWorld =
|
|
||||||
path.GlobalCurrCenter[0].Origin;
|
|
||||||
Vector3 endWorld =
|
|
||||||
path.GlobalSphere[0].Origin;
|
|
||||||
Vector3 globalDelta = endWorld - currentWorld;
|
|
||||||
float difference = Vector3.Dot(worldNormal0, globalDelta);
|
|
||||||
if (difference < 0f)
|
|
||||||
path.AddOffsetToCheckPos(-worldNormal0 * difference);
|
|
||||||
|
|
||||||
collisions.SetCollisionNormal(worldNormal0);
|
|
||||||
collisions.SetSlidingNormal(worldNormal0);
|
|
||||||
RecordDiagnosticHit(tree, defaultHitPolygonIndex0);
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.SetCollide(worldNormal0);
|
path.SetCollide(worldNormal0);
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
||||||
|
|
@ -2073,30 +2067,22 @@ internal static class FlatBspQuery
|
||||||
|
|
||||||
if (defaultHit1 || defaultHitPolygonIndex1 >= 0)
|
if (defaultHit1 || defaultHitPolygonIndex1 >= 0)
|
||||||
{
|
{
|
||||||
|
// #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)
|
||||||
|
// find_collisions dispatch, when the FOOT sphere is
|
||||||
|
// completely clear but the HEAD sphere hits or near-misses,
|
||||||
|
// records the head polygon's normal directly and hard-stops
|
||||||
|
// rather than deferring through SetCollide/Adjusted:
|
||||||
|
// pc:323824-323834 (0x0053a793/0x0053a7a4), cross-checked
|
||||||
|
// against ACE BSPTree.cs:221-230. See BSPQuery.cs's Path 6
|
||||||
|
// for the mirrored fix and full citation.
|
||||||
Vector3 worldNormal1 = LocalToWorld(
|
Vector3 worldNormal1 = LocalToWorld(
|
||||||
tree.PolygonTable.Polygons[defaultHitPolygonIndex1].Plane.Normal);
|
tree.PolygonTable.Polygons[defaultHitPolygonIndex1].Plane.Normal);
|
||||||
if (worldNormal1.Z < PhysicsGlobals.FloorZ)
|
|
||||||
{
|
|
||||||
Vector3 currentWorld =
|
|
||||||
path.GlobalCurrCenter[0].Origin;
|
|
||||||
Vector3 endWorld =
|
|
||||||
path.GlobalSphere[0].Origin;
|
|
||||||
Vector3 globalDelta = endWorld - currentWorld;
|
|
||||||
float difference =
|
|
||||||
Vector3.Dot(worldNormal1, globalDelta);
|
|
||||||
if (difference < 0f)
|
|
||||||
path.AddOffsetToCheckPos(-worldNormal1 * difference);
|
|
||||||
|
|
||||||
collisions.SetCollisionNormal(worldNormal1);
|
collisions.SetCollisionNormal(worldNormal1);
|
||||||
collisions.SetSlidingNormal(worldNormal1);
|
|
||||||
RecordDiagnosticHit(tree, defaultHitPolygonIndex1);
|
|
||||||
return TransitionState.Slid;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.SetCollide(worldNormal1);
|
|
||||||
path.WalkableAllowance = PhysicsGlobals.LandingZ;
|
|
||||||
RecordDiagnosticHit(tree, defaultHitPolygonIndex1);
|
RecordDiagnosticHit(tree, defaultHitPolygonIndex1);
|
||||||
return TransitionState.Adjusted;
|
return TransitionState.Collided;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ public class BSPStepUpTests
|
||||||
/// the retail step_up_slide / cliff_slide chain port is completed).</para>
|
/// the retail step_up_slide / cliff_slide chain port is completed).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void C3_Path6_AirborneMoverHitsSteepSlope_ReturnsSlid()
|
public void C3_Path6_AirborneMoverHitsSteepSlope_ReturnsAdjustedAndSetsCollide()
|
||||||
{
|
{
|
||||||
var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable();
|
var (root, resolved) = BSPStepUpFixtures.SlopedUnwalkable();
|
||||||
|
|
||||||
|
|
@ -427,13 +427,17 @@ public class BSPStepUpTests
|
||||||
root, resolved, t, localSphere, null,
|
root, resolved, t, localSphere, null,
|
||||||
currPos, Vector3.UnitZ, 1.0f);
|
currPos, Vector3.UnitZ, 1.0f);
|
||||||
|
|
||||||
// L.4 slide-tangent (b1af56e, 2026-04-30): steep polygon hit by
|
// TS-4 RETIRED (Campaign P final physics slice, 2026-07-30;
|
||||||
// airborne sphere returns Slid (not Adjusted) and does NOT set
|
// docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2).
|
||||||
// the Collide flag — the into-wall displacement is removed and
|
// Retail's BSP layer has NO steepness test at this layer at all
|
||||||
// CollisionNormal/SlidingNormal are set instead.
|
// (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793) — a steep
|
||||||
Assert.Equal(TransitionState.Slid, result);
|
// airborne hit now returns Adjusted and sets the Collide flag,
|
||||||
Assert.False(t.SpherePath.Collide,
|
// exactly like a shallow hit. The old L.4 slide-tangent shortcut
|
||||||
"Collide must NOT be set when the L.4 steep-slope slide-tangent fires");
|
// (Slid, no Collide, its own SetSlidingNormal write) is deleted.
|
||||||
|
Assert.Equal(TransitionState.Adjusted, result);
|
||||||
|
Assert.True(t.SpherePath.Collide,
|
||||||
|
"Collide must be set for a steep Path-6 hit, matching retail's " +
|
||||||
|
"unconditional SetCollide (acclient_2013_pseudo_c.txt:323783-323821).");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -7,50 +7,58 @@ using Xunit.Abstractions;
|
||||||
namespace AcDream.Core.Tests.Physics;
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign P Slice P2, TS-4 (Section 6 Step 3): the 2026-04-30 "L.4" fixture
|
/// TS-4 RETIRED (Campaign P final physics slice, 2026-07-30;
|
||||||
/// capture required before the Path-6 steep-poly slide-tangent shortcut may be
|
/// docs/research/2026-07-30-ts4-116-oracle-plan.md §1, §4 item 2). The
|
||||||
/// removed (<c>docs/research/2026-07-30-response-layer-edge-family-pseudocode.md</c>
|
/// Path-6 steep-poly slide-tangent shortcut (worldNormal.Z < FloorZ →
|
||||||
/// §4, §6 Step 3). The original repro was a live-client jump onto a steep
|
/// project-and-Slid, with its own SetSlidingNormal write) is deleted from
|
||||||
/// roof that got the body "stuck in falling animation" for many frames; no
|
/// <see cref="BSPQuery"/>'s Path 6; both sphere0 and sphere1 steep hits now
|
||||||
/// captured fixture from that live session survives in the repo (checked
|
/// fall through to the same unconditional <c>SetCollide</c> retail uses for
|
||||||
/// <c>docs/research/2026-04-30-*</c> and the L.4 commit `b1af56e`), so this
|
/// every hit (acclient_2013_pseudo_c.txt:323783-323821, 0x0053a793 — no
|
||||||
/// test builds a dat-free multi-frame replay from the existing
|
/// steepness test at the BSP layer at all).
|
||||||
/// <see cref="BSPStepUpFixtures.SlopedUnwalkable"/> geometry (a 63.4°
|
|
||||||
/// slope, normal.Z ≈ 0.447 — below <c>PhysicsGlobals.FloorZ</c> ≈ 0.6642 but
|
|
||||||
/// above <c>PhysicsGlobals.LandingZ</c> ≈ 0.0871, i.e. exactly the band the
|
|
||||||
/// L.4 commit's own steep-poly shortcut targets) using the same
|
|
||||||
/// <c>PhysicsEngine.ResolveWithTransition</c> multi-frame replay idiom as
|
|
||||||
/// <c>Issue185OutdoorStairsSeamReplayTests</c>.
|
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// A body falls from directly above the slope's mid-face, integrating
|
/// This class builds a dat-free multi-frame replay from the existing
|
||||||
/// gravity between resolves exactly as <c>PhysicsBody.UpdatePhysicsInternal</c>
|
/// <see cref="BSPStepUpFixtures.SlopedUnwalkable"/> geometry (a 63.4° slope,
|
||||||
/// would, for up to 3 simulated seconds (90 ticks at 30 Hz — retail's physics
|
/// normal.Z ≈ 0.447 — below <c>PhysicsGlobals.FloorZ</c> ≈ 0.6642 but above
|
||||||
/// tick rate, #32 L.5). "Wedged" is defined precisely, matching the original
|
/// <c>PhysicsGlobals.LandingZ</c> ≈ 0.0871, exactly the band the retired
|
||||||
/// bug report ("stuck in falling animation on the roof" for many consecutive
|
/// shortcut used to target) using the same
|
||||||
/// frames): the body's position stops changing (within 1 mm) for more than
|
/// <c>PhysicsEngine.ResolveWithTransition</c> multi-frame replay idiom as
|
||||||
/// 15 consecutive ticks (0.5 s) while never reaching the flat reference
|
/// <c>Issue185OutdoorStairsSeamReplayTests</c>, gravity integrated between
|
||||||
/// floor at x<0, z=0. A healthy resolution reaches the flat floor (Z ≈
|
/// resolves exactly as <c>PhysicsBody.UpdatePhysicsInternal</c> would, for
|
||||||
/// <see cref="BSPStepUpFixtures.SphereRadius"/>) well before the 90-tick
|
/// up to 3 simulated seconds (90 ticks at 30 Hz — retail's physics tick
|
||||||
/// budget expires, whether it does so by retail's own COLLIDED-then-fall
|
/// rate, #32 L.5).
|
||||||
/// bounce (this file's own git history documents that as retail's actual
|
|
||||||
/// behavior for a clean Path-6 steep hit with no pre-existing contact plane)
|
|
||||||
/// or by committing to the steep "walkable" surface via the permissive
|
|
||||||
/// <c>LandingZ</c> threshold (matching <c>CTransition::check_walkable</c>,
|
|
||||||
/// pc:273202, <c>0.0871556997f</c>) and then downhill-drifting off it via
|
|
||||||
/// the already-ported TS-1 CliffSlide chain.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Run TWICE across this slice's git history: once with the Path-6 steep
|
/// The oracle plan's own root-cause trace (§1.2) found the freeze
|
||||||
/// shortcut ACTIVE (pins today's baseline — always green, since the
|
/// mechanism one layer downstream of Path 6: Path 6's faithful
|
||||||
/// shortcut's own in-frame slide-tangent cannot wedge by construction), and
|
/// <c>SetCollide</c> doesn't reposition the sphere; the immediate retry
|
||||||
/// once with it REMOVED (the retail-strict candidate). If both pass, TS-4's
|
/// routes to Path 4 (<c>find_walkable</c>), which commits a real steep
|
||||||
/// removal is evidenced safe and lands in the same commit that deletes the
|
/// <c>ContactPlane</c> via the permissive <c>LandingZ</c> gate; the NEXT
|
||||||
/// shortcut and its <c>SetSlidingNormal</c> writes. If the removed-shortcut
|
/// tick's <c>AdjustOffset</c> then projects the (by-then gravity-only)
|
||||||
/// run wedges, the shortcut stays and this file's result against ToT is the
|
/// offset through <c>Cross(ContactPlane.Normal, SlidingNormal)</c> — and
|
||||||
/// recorded evidence — see the commit message / research doc open questions
|
/// for a PURELY VERTICAL offset that cross product annihilates it exactly
|
||||||
/// for the outcome actually reached.
|
/// (§1.2 Step E), tripping the abort-small-offset guard before
|
||||||
|
/// <c>TransitionalInsert</c> can run again. This is present identically in
|
||||||
|
/// the raw retail decomp, ACE's port, and this port (§1.2, §1.3) — it is
|
||||||
|
/// not a bug, it is what a truly zero-horizontal-velocity plumb drop onto a
|
||||||
|
/// steep surface does in every one of the three references. A live player
|
||||||
|
/// almost never produces this input (WASD, camera-relative movement, and
|
||||||
|
/// even float noise inject some horizontal component), which is why the
|
||||||
|
/// 2026-04-30 live-client debugger trace that originally validated the
|
||||||
|
/// shortcut never observed the freeze.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Two fixtures, two different fates, per the plan's own decisive test
|
||||||
|
/// (§4 item 2): <see cref="FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity"/>
|
||||||
|
/// is the DEGENERATE case (zero horizontal velocity) — PINNED as a known,
|
||||||
|
/// retail-matching freeze (see register row AD-56).
|
||||||
|
/// <see cref="FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor"/>
|
||||||
|
/// is the REALISTIC case (small residual horizontal velocity, matching the
|
||||||
|
/// original live repro's actual trajectory shape) — it converges cleanly to
|
||||||
|
/// the flat floor with no freeze, which is what made TS-4's removal safe to
|
||||||
|
/// land.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Ts4SteepRoofWedgeCaptureTests
|
public class Ts4SteepRoofWedgeCaptureTests
|
||||||
|
|
@ -112,12 +120,26 @@ public class Ts4SteepRoofWedgeCaptureTests
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Falls a player-flagged mover from directly above the 63.4° slope's
|
/// PINNED known-degenerate case (Campaign P final physics slice,
|
||||||
/// mid-face and asserts it reaches the flat floor (or at minimum keeps
|
/// 2026-07-30; docs/research/2026-07-30-ts4-116-oracle-plan.md §1.2-§1.3,
|
||||||
/// making downward/downhill progress) without a >0.5s frozen stretch.
|
/// §4 item 2; register row AD-56). A body falling PERFECTLY PLUMB (zero
|
||||||
|
/// horizontal velocity) onto this 63.4° slope's mid-face lands, commits
|
||||||
|
/// a steep <c>ContactPlane</c> via Path 4's permissive <c>LandingZ</c>
|
||||||
|
/// gate, and then freezes at that exact position forever: the crease
|
||||||
|
/// projection inside <c>AdjustOffset</c>
|
||||||
|
/// (<c>Cross(ContactPlane.Normal, SlidingNormal)</c>) is mathematically
|
||||||
|
/// orthogonal to a purely-Z offset, crushing it to zero every tick and
|
||||||
|
/// tripping the abort-small-offset guard before <c>TransitionalInsert</c>
|
||||||
|
/// runs again. This is retail-faithful — present identically in the raw
|
||||||
|
/// decomp, ACE's port, and this port (§1.2) — and essentially
|
||||||
|
/// unreachable in live play, where WASD input, camera-relative movement,
|
||||||
|
/// and even float noise almost always inject some horizontal component
|
||||||
|
/// (see <see cref="FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor"/>
|
||||||
|
/// for the realistic case, which does NOT freeze). This test PINS the
|
||||||
|
/// freeze as accepted parity rather than treating it as a bug to fix.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FallOntoSteepSlope_NeverFreezesForOverHalfASecond_AndReachesFloor()
|
public void FallOntoSteepSlope_PureVertical_FreezesAtDegenerateFixedPoint_RetailParity()
|
||||||
{
|
{
|
||||||
var engine = MakeSlopeEngine();
|
var engine = MakeSlopeEngine();
|
||||||
float r = BSPStepUpFixtures.SphereRadius;
|
float r = BSPStepUpFixtures.SphereRadius;
|
||||||
|
|
@ -130,19 +152,121 @@ public class Ts4SteepRoofWedgeCaptureTests
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start well above the slope's mid-face (slope spans x in [0,1], z in
|
// Start well above the slope's mid-face (slope spans x in [0,1], z in
|
||||||
// [0,2] at that x-range), falling straight down.
|
// [0,2] at that x-range), falling straight down with NO horizontal
|
||||||
|
// component — the degenerate input this pin documents.
|
||||||
Vector3 pos = new(0.5f, 0f, 3.0f);
|
Vector3 pos = new(0.5f, 0f, 3.0f);
|
||||||
float fallVelocityZ = 0f;
|
float fallVelocityZ = 0f;
|
||||||
uint cell = CellId;
|
uint cell = CellId;
|
||||||
|
|
||||||
var positions = new List<Vector3>(MaxTicks) { pos };
|
int frozenStreak = 0;
|
||||||
|
bool frozeAsExpected = false;
|
||||||
|
Vector3 frozenAtPosition = default;
|
||||||
|
|
||||||
|
for (int tick = 0; tick < MaxTicks; tick++)
|
||||||
|
{
|
||||||
|
fallVelocityZ += gravity * dt;
|
||||||
|
Vector3 target = pos + new Vector3(0f, 0f, fallVelocityZ * dt);
|
||||||
|
|
||||||
|
var result = engine.ResolveWithTransition(
|
||||||
|
currentPos: pos,
|
||||||
|
targetPos: target,
|
||||||
|
cellId: cell,
|
||||||
|
sphereRadius: r,
|
||||||
|
sphereHeight: r * 2f,
|
||||||
|
stepUpHeight: 0.30f,
|
||||||
|
stepDownHeight: 0.04f,
|
||||||
|
isOnGround: false,
|
||||||
|
body: body,
|
||||||
|
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
|
||||||
|
movingEntityId: 0x01000000u);
|
||||||
|
|
||||||
|
var newPos = result.Position;
|
||||||
|
float moved = Vector3.Distance(newPos, pos);
|
||||||
|
|
||||||
|
if (moved < WedgeEpsilon)
|
||||||
|
frozenStreak++;
|
||||||
|
else
|
||||||
|
frozenStreak = 0;
|
||||||
|
|
||||||
|
_out.WriteLine(
|
||||||
|
$"t{tick,3}: pos=({newPos.X:F3},{newPos.Y:F3},{newPos.Z:F3}) " +
|
||||||
|
$"moved={moved:F4} onGround={result.IsOnGround} onWalkable={result.OnWalkable} " +
|
||||||
|
$"contact={result.InContact} vz={fallVelocityZ:F2} frozen={frozenStreak}");
|
||||||
|
|
||||||
|
pos = newPos;
|
||||||
|
cell = result.CellId;
|
||||||
|
body.Position = pos;
|
||||||
|
|
||||||
|
if (result.IsOnGround)
|
||||||
|
fallVelocityZ = 0f;
|
||||||
|
|
||||||
|
if (frozenStreak > WedgeTickThreshold)
|
||||||
|
{
|
||||||
|
frozeAsExpected = true;
|
||||||
|
frozenAtPosition = pos;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(frozeAsExpected,
|
||||||
|
$"Expected the degenerate pure-vertical drop to freeze for more than " +
|
||||||
|
$"{WedgeTickThreshold} consecutive ticks (retail-matching AdjustOffset " +
|
||||||
|
"crease-projection degeneracy, AD-56) within the {MaxTicks}-tick budget — " +
|
||||||
|
"it did not. Either the degenerate case no longer reproduces (re-evaluate " +
|
||||||
|
"this pin against the oracle plan) or an unrelated regression changed the " +
|
||||||
|
"slope-landing chain.");
|
||||||
|
Assert.True(frozenAtPosition.X > 0f,
|
||||||
|
"Expected the freeze to occur ON the steep slope (x>0), not at/after the " +
|
||||||
|
$"flat reference floor; got x={frozenAtPosition.X:F3}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Campaign P final physics slice, TS-4 decisive confirming run
|
||||||
|
/// (<c>docs/research/2026-07-30-ts4-116-oracle-plan.md</c> §1.2 Step E,
|
||||||
|
/// §1.3, §4 item 2). The pure-vertical fixture above is, per the oracle
|
||||||
|
/// plan, the DEGENERATE case: <c>AdjustOffset</c>'s crease projection
|
||||||
|
/// (<c>Cross(ContactPlane.Normal, SlidingNormal)</c> against a purely
|
||||||
|
/// gravity-only offset) is mathematically annihilated by construction
|
||||||
|
/// when the offset has zero horizontal component — <c>Dot(slideOffset,
|
||||||
|
/// offset) = 0</c> exactly, because <c>slideOffset.Z = 0</c> and the
|
||||||
|
/// offset is purely Z. Any lateral drift (WASD input, residual jump
|
||||||
|
/// momentum — present in the original 2026-04-30 live-client repro that
|
||||||
|
/// validated the shortcut, but NOT in the pure-vertical fixture above)
|
||||||
|
/// survives that same cross product and should let <c>AdjustOffset</c>
|
||||||
|
/// produce a small non-zero tangential offset each tick, moving the
|
||||||
|
/// sphere off the exact collision point, avoiding the abort-small-offset
|
||||||
|
/// short-circuit, and letting <c>TransitionalInsert</c> run again on
|
||||||
|
/// subsequent ticks.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FallOntoSteepSlope_WithHorizontalVelocity_NeverFreezesForOverHalfASecond_AndReachesFloor()
|
||||||
|
{
|
||||||
|
var engine = MakeSlopeEngine();
|
||||||
|
float r = BSPStepUpFixtures.SphereRadius;
|
||||||
|
const float dt = 1f / TicksPerSecond;
|
||||||
|
const float gravity = -9.8f;
|
||||||
|
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
TransientState = TransientStateFlags.Active,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same drop point as the pure-vertical fixture, but with a small
|
||||||
|
// residual horizontal velocity toward the flat reference floor
|
||||||
|
// (x<0) — the realistic "jumped/walked onto the roof with some
|
||||||
|
// drift" case the L.4 shortcut's own validating trace exercised.
|
||||||
|
Vector3 pos = new(0.5f, 0f, 3.0f);
|
||||||
|
float fallVelocityZ = 0f;
|
||||||
|
const float horizontalVelocityX = -0.3f;
|
||||||
|
uint cell = CellId;
|
||||||
|
|
||||||
int frozenStreak = 0;
|
int frozenStreak = 0;
|
||||||
bool reachedFloor = false;
|
bool reachedFloor = false;
|
||||||
|
|
||||||
for (int tick = 0; tick < MaxTicks; tick++)
|
for (int tick = 0; tick < MaxTicks; tick++)
|
||||||
{
|
{
|
||||||
fallVelocityZ += gravity * dt;
|
fallVelocityZ += gravity * dt;
|
||||||
Vector3 target = pos + new Vector3(0f, 0f, fallVelocityZ * dt);
|
Vector3 target = pos + new Vector3(horizontalVelocityX * dt, 0f, fallVelocityZ * dt);
|
||||||
|
|
||||||
var result = engine.ResolveWithTransition(
|
var result = engine.ResolveWithTransition(
|
||||||
currentPos: pos,
|
currentPos: pos,
|
||||||
|
|
@ -182,9 +306,6 @@ public class Ts4SteepRoofWedgeCaptureTests
|
||||||
if (result.IsOnGround)
|
if (result.IsOnGround)
|
||||||
fallVelocityZ = 0f;
|
fallVelocityZ = 0f;
|
||||||
|
|
||||||
positions.Add(pos);
|
|
||||||
|
|
||||||
// Reached the flat reference floor (x<0, z ~ r) — resolved cleanly.
|
|
||||||
if (pos.X < 0f && pos.Z <= r + 0.05f)
|
if (pos.X < 0f && pos.Z <= r + 0.05f)
|
||||||
{
|
{
|
||||||
reachedFloor = true;
|
reachedFloor = true;
|
||||||
|
|
@ -195,8 +316,6 @@ public class Ts4SteepRoofWedgeCaptureTests
|
||||||
Assert.True(reachedFloor,
|
Assert.True(reachedFloor,
|
||||||
$"Body never reached the flat reference floor within {MaxTicks} ticks " +
|
$"Body never reached the flat reference floor within {MaxTicks} ticks " +
|
||||||
$"({MaxTicks / (float)TicksPerSecond:F1}s); final position " +
|
$"({MaxTicks / (float)TicksPerSecond:F1}s); final position " +
|
||||||
$"({pos.X:F3},{pos.Y:F3},{pos.Z:F3}) — this is the wedge the L.4 shortcut guards " +
|
$"({pos.X:F3},{pos.Y:F3},{pos.Z:F3}).");
|
||||||
"against (never resolving off the steep surface at all), distinct from a bounded " +
|
|
||||||
"per-tick freeze.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue