test(physics): harden TS-4 production chronology

This commit is contained in:
Erik 2026-07-31 14:08:51 +02:00
parent 75b6f6b6c9
commit d3c0d9ec0e
5 changed files with 807 additions and 101 deletions

View file

@ -241,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~~ | **RETIRED 2026-07-31 (Campaign P Slice 2B).** The graph and prepared-flat Path-6 implementations now match retail's exact two-sphere split: every primary/foot polygon hit calls `SetCollide`, sets `WalkableAllowance=LandingZ`, and returns `Adjusted`; only a secondary/head hit writes `CollisionNormal` and returns `Collided`. The steep tangent shortcut and both BSP-layer `SetSlidingNormal` writes are deleted. Exact site tests pin all changed and preserved fields plus raw-bit graph/flat parity. Production-shaped multi-frame vertical/inward/tangential/uphill/downhill roof/wall/ledge traces carry accepted body state between frames, match graph/flat by raw result/body bits, reject penetration and uphill launch/bounce, and pass the complete historical collision matrix without compensation. | `src/AcDream.Core/Physics/BSPQuery.cs`; `src/AcDream.Core/Physics/FlatBspQuery.cs`; `tests/AcDream.Core.Tests/Physics/Ts4Path6ConformanceTests.cs`; `tests/AcDream.Core.Tests/Physics/RetailEdgeResponseOrderingTests.cs`; `tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs` | — | — | `BSPTREE::find_collisions` 0x0053A440: head `0x0053A793..0x0053A7A4`, foot `0x0053A7B3..0x0053A7DC`; research §10 | | ~~TS-4~~ | **RETIRED 2026-07-31 (Campaign P Slice 2B; corrective acceptance complete).** The graph and prepared-flat Path-6 implementations now match retail's exact two-sphere split: every primary/foot polygon hit calls `SetCollide`, sets `WalkableAllowance=LandingZ`, and returns `Adjusted`; only a secondary/head hit writes `CollisionNormal` and returns `Collided`. The steep tangent shortcut and every BSP-layer `SetSlidingNormal` write are deleted. Exact site tests pin all changed and preserved fields plus raw-bit graph/flat parity. A corrective 90-tick already-airborne, zero-root-motion Core suite executes acceleration, body integration, transition resolution, exact commit, and `handle_all_collisions` while retaining every behavior-bearing collision/body field used by that specialized quantum. Vertical, inward, tangential, downhill, and positive-Z uphill-jump traces match graph/flat by raw bits, reject penetration/fixed points/second launches, and pin exact terminal velocity, contact, sliding, and contact-plane state. The older resolver-only capture is explicitly historical and restored to its three-second bound. | `src/AcDream.Core/Physics/BSPQuery.cs`; `src/AcDream.Core/Physics/FlatBspQuery.cs`; `tests/AcDream.Core.Tests/Physics/Ts4Path6ConformanceTests.cs`; `tests/AcDream.Core.Tests/Physics/Ts4ProductionQuantumConformanceTests.cs`; `tests/AcDream.Core.Tests/Physics/Ts4SteepRoofWedgeCaptureTests.cs` | — | — | `BSPTREE::find_collisions` 0x0053A440: head `0x0053A793..0x0053A7A4`, foot `0x0053A7B3..0x0053A7DC`; research §10 |
| 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~~ | **RETIRED 2026-07-31 (#268 stat-chain closeout).** `EnchantmentWireReader` parses the complete 0x02C2 payload and `GameEventWiring` publishes its StatMod type/key/value and bucket through the same `ActiveEnchantmentRecord` used at login. An end-to-end dispatch test proves a mid-session skill modifier changes `LocalPlayerState.GetEffectiveSkill` immediately. | `src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs`; `src/AcDream.Core.Net/GameEventWiring.cs`; `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs` | — | — | `CEnchantmentRegistry::EnchantAttribute @ 0x00594570`; `CEnchantmentRegistry::EnchantSkill @ 0x005947B0`; holtburger `messages/magic/types.rs` | | ~~TS-8~~ | **RETIRED 2026-07-31 (#268 stat-chain closeout).** `EnchantmentWireReader` parses the complete 0x02C2 payload and `GameEventWiring` publishes its StatMod type/key/value and bucket through the same `ActiveEnchantmentRecord` used at login. An end-to-end dispatch test proves a mid-session skill modifier changes `LocalPlayerState.GetEffectiveSkill` immediately. | `src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs`; `src/AcDream.Core.Net/GameEventWiring.cs`; `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs` | — | — | `CEnchantmentRegistry::EnchantAttribute @ 0x00594570`; `CEnchantmentRegistry::EnchantSkill @ 0x005947B0`; holtburger `messages/magic/types.rs` |
@ -306,12 +306,11 @@ 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-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together.
6. **TS-55 — AdminEnvirons fog/radar presentation** — exact retail mechanism is known; port the authored ambient/fog fields, radar blanking, Clear, and `0x270F` together. 6. **TS-19 — Legacy ChaseCamera deletion** — already marked "pending the follow-up deletion commit"; its continued existence can mask or manufacture flap symptoms during debugging.
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.
**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

View file

@ -99,36 +99,30 @@ character state continuously.
### P2 — Response-layer edge family — retires TS-1, TS-4, AP-7; closes #166, #116 ### P2 — Response-layer edge family — retires TS-1, TS-4, AP-7; closes #166, #116
**Status (2026-07-30, FINAL — Campaign P final physics slice):** TS-1 and **Status (2026-07-31, FINAL):** TS-1 and AP-7 retired as originally
AP-7 retired same-day as originally recorded. **TS-4 is now ALSO recorded. TS-4's first 2026-07-30 removal was accepted by an incomplete
RETIRED** — the oracle follow-up pass resolver-only fixture, failed the live matrix with a roof wedge/uphill-bounce
(`docs/research/2026-07-30-ts4-116-oracle-plan.md`) found the freeze the regression, and was reverted. The 2026-07-31 closure began from a fresh
first implementation attempt hit was one layer downstream of Path 6 `BSPTREE::find_collisions` read and ports the exact asymmetric Path-6 split:
(inside `AdjustOffset`'s crease projection against a purely-vertical primary/foot hits use `SetCollide` + `LandingZ` + `Adjusted`, while
offset — a genuine retail-identical degeneracy, not a bug) and ran the secondary/head hits use `CollisionNormal` + `Collided`; neither writes a
plan's own decisive confirming test: `Ts4SteepRoofWedgeCaptureTests`'s sliding normal. Both graph and prepared-flat implementations match that
horizontal-velocity variant (matching the realistic live-play input that oracle.
originally validated the shortcut) converges cleanly with the shortcut
removed. The Path-6 steep-poly shortcut is deleted from both The corrective acceptance no longer calls the old horizontal-input fixture
`BSPQuery.cs` and `FlatBspQuery.cs`; the pure-vertical degenerate case is "production-shaped." `Ts4ProductionQuantumConformanceTests` executes the
pinned (not fixed) as register row AD-56. **#116 is narrowed, not already-airborne, zero-root-motion 30 Hz Core collision tail — acceleration,
closed**: shape-2 (D4 first-airborne-frame hard-stop) is CLOSED — the body integration, transition resolve, exact body/cell commit, then
oracle plan's structural dispatch-routing hypothesis was confirmed by `handle_all_collisions` — and retains its behavior-bearing cell, contact,
instrumentation with no cdb session needed, and the D4 pin is un-skipped. sliding, stationary-fall, and velocity state for 90 ticks. Graph and flat
Shape-1 (tick-22760 lateral-slide loss) got a real, independently-decomp- match by raw bits for vertical/inward/tangential/downhill cases and a genuine
confirmed fix (Path 6's foot-clear/head-hit branch now returns `Collided` positive-Z uphill jump; exact terminal state, non-penetration, no fixed point,
+ `SetCollisionNormal` directly, matching pc:323824-323834/ACE and no second launch are pinned. No further product-code correction was
`BSPTree.cs:221-230`), but the confirming replay showed this does NOT needed after that test became faithful, and there is no active AD-56 row. The
explain tick-22760 itself — that mover is grounded (dispatches through older resolver-only wedge test remains only as a historical three-second
Path 5, not Path 6) and the actual "no normal recorded" mechanism signature control. #116 shape-2 remains closed; shape-1 remains narrowed as
(`SpherePath.PrecipiceSlide`'s `find_crossed_edge`-false fallback) is recorded in its issue history. AD-55 remains retired by the raw-byte
independently confirmed byte-exact retail behavior too. The remaining `cos(10°)` proof.
divergence is most likely this test's simplified door-registration
fixture, not the response layer — see ISSUES.md #116 and the oracle
plan's Addendum 2 for the full trace and the concrete next step (re-run
against the faithful Setup-based door registration). AD-55 (the sled
slope-flatness constant, split out of AP-7's retirement) is ALSO retired
this same slice — byte-proven `cos(10°)` per the oracle plan's Addendum.
The collision *response* layer (what happens after a hit): ground The collision *response* layer (what happens after a hit): ground
friction, cliff edges, downhill landings, near-perpendicular wall friction, cliff edges, downhill landings, near-perpendicular wall
@ -145,10 +139,11 @@ binds every subagent here.
stop-at-edge. stop-at-edge.
3. **#166:** port the landing "sled" (Sledding state set/clear sites; 3. **#166:** port the landing "sled" (Sledding state set/clear sites;
the sled friction constants already sit in `calc_friction`). the sled friction constants already sit in `calc_friction`).
4. **TS-4:** replace the Path-6 steep-poly in-place-slide shortcut with 4. **TS-4:** remove the Path-6 steep-poly shortcuts and port retail's exact
retail's `SetCollide → Path-4 → ContactPlane` landing chain, and sphere split: primary/foot uses `SetCollide` + `LandingZ` + `Adjusted`;
remove the two BSP-layer `SetSlidingNormal` writes (retail's only secondary/head uses `CollisionNormal` + `Collided`. Remove every BSP-layer
in-transition writer is `validate_transition`). `SetSlidingNormal` write (retail's only in-transition writer is
`validate_transition`).
5. **#116:** the near-perpendicular lateral-slide loss + first-airborne- 5. **#116:** the near-perpendicular lateral-slide loss + first-airborne-
frame divergence, driven by the existing tick-22760 replay and D4 frame divergence, driven by the existing tick-22760 replay and D4
pins. pins.

View file

@ -599,17 +599,16 @@ manufacture a caller that sets it.
### The former shortcut (HISTORICAL FACT; removed by Slice 2B) ### The former shortcut (HISTORICAL FACT; removed by Slice 2B)
Before Slice 2B, Path-6 (the default Before Slice 2B, Path-6 (the default
`sphere_intersects_poly → collide_with_pt / SetCollide` dispatch) tested each `sphere_intersects_poly → collide_with_pt / SetCollide` dispatch) was not
hit polygon's world-space normal. For BOTH sphere0 (feet) and sphere1 (head), symmetrically wrong. The parsed-graph primary/foot branch tested the hit
if `worldNormal.Z < PhysicsGlobals.FloorZ` (steeper than ~49° from polygon's world-space normal and, below `PhysicsGlobals.FloorZ`, projected
horizontal), acdream took a SPECIAL BRANCH: the move along the face, wrote both collision and sliding normals, and
projects the move along the steep face, writes returned `Slid`. Its secondary/head branch had already been corrected by
`collisions.SetCollisionNormal(worldNormal)` **and** #116 to retail's unconditional `CollisionNormal` + `Collided` response. The
`collisions.SetSlidingNormal(worldNormal)`, and returns prepared-flat port still applied the steep shortcut to both spheres and sent
`TransitionState.Slid` immediately — bypassing `SetCollide` entirely for both shallow cases through `SetCollide` + `Adjusted`; its head branch was
steep hits. Only the shallow case (`worldNormal.Z >= FloorZ`) reaches therefore wrong for every slope. This distinction matters: retail does not
`path.SetCollide(worldNormal); path.WalkableAllowance = LandingZ; return apply one common response to both spheres.
Adjusted;`.
The in-code comment is unusually candid about why: **"This is a The in-code comment is unusually candid about why: **"This is a
deliberate deviation from retail... Validated against retail debugger deliberate deviation from retail... Validated against retail debugger
@ -621,10 +620,11 @@ was shipped SAME-DAY as (and BECAUSE) the retail-faithful
§2 above — also dated 2026-04-30, tagged "L.4") still wedged in testing §2 above — also dated 2026-04-30, tagged "L.4") still wedged in testing
when tried without this shortcut. when tried without this shortcut.
### Retail: NO steepness branch at the BSP layer (FACT, pseudo-C:323740-323783, `0053a730` region) ### Retail: NO steepness branch in either Path-6 sphere response (FACT, `0053a730` region)
Read directly from the named decomp (the `sphere_intersects_poly` / Read directly from the named decomp (the `sphere_intersects_poly` /
`set_collide` dispatch inside `BSPTREE::find_collisions`'s default path): `set_collide` dispatch for the primary/foot sphere inside
`BSPTREE::find_collisions`'s default path):
``` ```
if (sphere_intersects_poly(...) || eax_26 != 0) { if (sphere_intersects_poly(...) || eax_26 != 0) {
@ -635,11 +635,21 @@ if (sphere_intersects_poly(...) || eax_26 != 0) {
} }
``` ```
**There is no steepness test here at all.** Retail's BSP layer calls **There is no steepness test in this foot branch.** It calls `set_collide`
`set_collide` and returns `ADJUSTED_TS` **unconditionally**, for a steep and returns `ADJUSTED_TS` for a steep roof exactly as for a shallow ramp.
roof exactly the same as a shallow ramp. `walkable_allowance` is always `walkable_allowance` is always set to `LandingZ` (the permissive landing
set to `LandingZ` (the permissive landing threshold) at this layer, threshold), regardless of polygon slope. If the foot is clear but the
regardless of the actual polygon slope. This directly confirms the P2 secondary/head sphere hits, retail instead performs the other slope-agnostic
response:
```
localtoglobalvec(sphere_path.localspace_pos, &normal, &head_poly->plane.N);
COLLISIONINFO::set_collision_normal(&collision_info, &normal);
return 2; // COLLIDED_TS
```
The head branch neither calls `set_collide` nor returns `ADJUSTED_TS`.
Neither sphere response has a steepness branch. This directly confirms the P2
plan's description and the digest's #137-mechanism-2 finding plan's description and the digest's #137-mechanism-2 finding
(`memory/project_physics_collision_digest.md:1008-1014`): **retail's (`memory/project_physics_collision_digest.md:1008-1014`): **retail's
BSP/sphere collision layer never writes `collision_info.sliding_normal` BSP/sphere collision layer never writes `collision_info.sliding_normal`
@ -653,16 +663,13 @@ domain, §2 above).
### TS-4 port shape (COMPLETED 2026-07-31) ### TS-4 port shape (COMPLETED 2026-07-31)
Delete both `if (worldNormal{0,1}.Z < PhysicsGlobals.FloorZ) { ... return Delete the primary/foot steep shortcut in both representations. Every foot
TransitionState.Slid; }` blocks (`BSPQuery.cs:2200-2215` and hit must call `SetCollide`, set `WalkableAllowance=LandingZ`, and return
`:2240-2255`) entirely. Both sphere0 and sphere1 hits should fall straight `Adjusted`. Preserve the parsed-graph head branch corrected by #116, and
through to the existing `path.SetCollide(worldNormal); path.WalkableAllowance replace the prepared-flat head steep/shallow split with the same unconditional
= PhysicsGlobals.LandingZ; return TransitionState.Adjusted;` — i.e., make `SetCollisionNormal` + `Collided` response. No Path-6 branch writes a sliding
Path-6 do EXACTLY what its own shallow branch already does, for every normal. This is the exact retail foot/head split, not a shared fallback to the
hit, matching retail's unconditional `set_collide`. This mechanically foot behavior.
retires both `SetSlidingNormal` write sites (satisfying DO-NOT-RETRY §0
item 1 permanently — deleted, not just avoided) with no replacement logic
needed at this layer.
### Port-order coupling with TS-1 (historical guard, now satisfied) ### Port-order coupling with TS-1 (historical guard, now satisfied)
@ -1202,17 +1209,35 @@ representations by raw bits and pin every mutated and preserved field:
- both: a pre-existing sliding normal is preserved byte-for-byte. - both: a pre-existing sliding normal is preserved byte-for-byte.
The failed first removal was a test-harness lesson, not a retail exception. The failed first removal was a test-harness lesson, not a retail exception.
Its gravity-only replay always passed `isOnGround:false` and discarded each Its gravity-only replay always passed `isOnGround:false`, manually zeroed
accepted result's Contact/OnWalkable bits, making the nested edge/StepDown vertical velocity, and only copied part of the accepted contact state, so it
chain impossible to exercise on the next frame. The replacement replay uses could not reproduce the live caller's next-frame chronology. The replacement
the same retained `PhysicsBody` chronology as production. Parsed graph and `Ts4ProductionQuantumConformanceTests` replay executes each already-airborne,
prepared flat now match by raw result/body bits for vertical roof descent, zero-root-motion 30 Hz Core collision quantum in the production order:
downhill input, uphill pressure (including a no-launch/no-bounce assertion), `calc_acceleration`, `UpdatePhysicsInternal`,
tangential roof travel, inward-plus-tangent wall travel, and flat-roof ledge `ResolveWithTransition`, exact body/cell commit, then
rejection. Every trace asserts finite bounded motion and signed-plane non- `PhysicsObjUpdate.CommitSetPositionTransition` (the Core owner of retail's
penetration. The former #116 D4 control is active: its primary-sphere hit Contact/OnWalkable replacement plus `handle_all_collisions`). It carries the
hard-stops frame one through SetCollide, then the accepted persistent normal same body, cell, contact plane, walkable plane, sliding state, stationary-fall
permits the downward slide on frame two. counter, cached velocity, and transient flags for all 90 ticks. PositionManager
root composition, animation hooks, movement callbacks, and fresh-airborne
`LeaveGround`/`HitGround` edges are deliberately outside this already-airborne
dat-free fixture; it does not claim to replay those presentation/motion stages.
Parsed graph and prepared flat match by raw result/body bits for vertical,
inward, tangential, and downhill roof motion plus a genuine positive-Z uphill
jump. The foot-contact jump's apex precedes roof contact and every later
candidate velocity remains non-positive in Z. A separate elevated head-only
collision reaches the wall while Z velocity is still positive and pins the
valid-normal, inward-dot retail 5%-elastic reflection without adding vertical
velocity. Every direction rejects a half-second fixed point and signed-plane
penetration, while exact terminal velocity, Contact/OnWalkable/Sliding bits,
sliding normal, and contact plane are fixed by raw float bits. The older
resolver-only wedge capture remains a deliberately weaker historical control
and is restored to its original three-second bound. The former #116 D4
control remains active: its primary-sphere hit hard-stops frame one through
SetCollide, then the accepted persistent normal permits the downward slide on
frame two.
The complete historical #273/#271/#269/#265/#185/#137/#116/cellar/roof The complete historical #273/#271/#269/#265/#185/#137/#116/cellar/roof
matrix and the full Core Release suite pass without a replacement matrix and the full Core Release suite pass without a replacement

View file

@ -0,0 +1,695 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Numerics;
using System.Text;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Production-quantum acceptance for retail Path 6. Unlike the historical
/// bare-resolver fixture, every frame executes the already-airborne,
/// zero-root-motion Core collision tail used by PlayerMovementController:
/// acceleration, PhysicsBody integration, sweep, complete position/contact
/// commit, and handle_all_collisions.
/// </summary>
public sealed class Ts4ProductionQuantumConformanceTests
{
private const uint Cell = 0xA9B40001u;
private const uint GfxId = 0x0100E1B0u;
private const float Dt = 1f / 30f;
private const float Radius = 0.48f;
private const int TickCount = 90;
private const ushort RoofPolygonId = 1;
private static readonly ImmutableArray<FlatCollisionSphere> HumanSpheres =
ImmutableArray.Create(
new FlatCollisionSphere(new Vector3(0f, 0f, 0.475f), Radius),
new FlatCollisionSphere(new Vector3(0f, 0f, 1.350f), Radius));
private readonly ITestOutputHelper _output;
public Ts4ProductionQuantumConformanceTests(ITestOutputHelper output) =>
_output = output;
[Theory]
[InlineData("vertical", 0f, 0f, 0f, 0xC18415BEu, 0x00000000u, 0xC201B112u, 0x00000000u, 0x00000000u, 0xC1EB3325u)]
[InlineData("inward", 0.5366564f, 0f, -0.2683282f, 0xC18464EAu, 0x00000000u, 0xC202003Eu, 0x3F096250u, 0x00000000u, 0xC1ED58AEu)]
[InlineData("tangent", 0f, 0.30f, 0f, 0xC18415BEu, 0x3F63F3DBu, 0xC201B112u, 0x00000000u, 0x3E99999Au, 0xC1EB3325u)]
[InlineData("downhill", -0.2683282f, 0f, -0.5366564f, 0xC18A74DBu, 0x00000000u, 0xC208102Fu, 0xBE896250u, 0x00000000u, 0xC1EF7E37u)]
public void RoofDirections_ProductionQuantum_GraphFlatExactAndNeverWedge(
string direction,
float velocityX,
float velocityY,
float velocityZ,
uint terminalPositionXBits,
uint terminalPositionYBits,
uint terminalPositionZBits,
uint terminalVelocityXBits,
uint terminalVelocityYBits,
uint terminalVelocityZBits)
{
var initialPosition = new Vector3(0.5f, 0f, 3f);
var initialVelocity = new Vector3(velocityX, velocityY, velocityZ);
QuantumTrace graph = RunTrace(
preparedFlat: false,
initialPosition,
initialVelocity,
TickCount);
QuantumTrace flat = RunTrace(
preparedFlat: true,
initialPosition,
initialVelocity,
TickCount);
AssertTraceExact(graph, flat);
AssertNoSteepSurfaceFixedPoint(graph, direction);
AssertNoSlopePenetration(graph, direction);
Assert.True(graph.FirstContactTick >= 0,
$"{direction} never contacted the authored steep roof.");
AssertCollisionResponseExact(graph.Frames[graph.FirstContactTick]);
AssertTerminalStateExact(
graph,
terminalPositionXBits,
terminalPositionYBits,
terminalPositionZBits,
terminalVelocityXBits,
terminalVelocityYBits,
terminalVelocityZBits);
_output.WriteLine(
$"{direction}: peak={graph.PeakZ:R}, contactTick={graph.FirstContactTick}, " +
$"terminalPos={graph.Frames[^1].Position}, " +
$"terminalVelocity={graph.Frames[^1].Velocity}, " +
$"terminalFlags={graph.Frames[^1].TransientState}, " +
$"terminalSliding={graph.Frames[^1].SlidingNormal}");
}
[Fact]
public void UphillPositiveZJump_ProductionQuantum_GraphFlatExactWithoutLaunch()
{
// Positive-Z jump whose +X component points into/up the authored roof.
// Its authored separation keeps the real apex before Path-6 contact,
// then exercises SetCollide -> SetPositionInternal ->
// handle_all_collisions without manufacturing a second launch.
Vector3 initialPosition = new(-0.20f, 0f, 0.422f);
Vector3 initialVelocity = new(0.4472136f, 0f, 0.8944272f);
QuantumTrace graph = RunTrace(
preparedFlat: false,
initialPosition,
initialVelocity,
TickCount);
QuantumTrace flat = RunTrace(
preparedFlat: true,
initialPosition,
initialVelocity,
TickCount);
AssertTraceExact(graph, flat);
Assert.True(graph.FirstContactTick >= 0,
$"The uphill jump never hit the roof; peak={graph.PeakZ:R}, " +
$"terminal={graph.Frames[^1].Position}, " +
$"collisionTick={graph.Frames.FindIndex(frame => frame.CollisionNormalValid)}.");
AssertNoSteepSurfaceFixedPoint(graph, "uphill-jump");
AssertNoSlopePenetration(graph, "uphill-jump");
QuantumFrame hit = graph.Frames[graph.FirstContactTick];
int peakFrame = graph.Frames.FindIndex(frame => frame.Position.Z == graph.PeakZ);
Assert.InRange(peakFrame, 1, graph.FirstContactTick - 1);
Assert.True(hit.CandidateVelocity.Z < 0f,
$"The jump must contact after its real apex: {hit.CandidateVelocity}.");
Assert.All(
graph.Frames.GetRange(
graph.FirstContactTick,
graph.Frames.Count - graph.FirstContactTick),
frame => Assert.True(frame.CandidateVelocity.Z <= 0f,
$"The foot-contact path created an upward relaunch at frame " +
$"{frame.Tick}: {frame.CandidateVelocity}."));
float postHitPeak = graph.Frames
.GetRange(graph.FirstContactTick, graph.Frames.Count - graph.FirstContactTick)
.Max(frame => frame.Position.Z);
Assert.True(postHitPeak <= graph.PeakZ + 0.001f,
$"The collision created a second launch: firstPeak={graph.PeakZ:R}, " +
$"postHitPeak={postHitPeak:R}.");
AssertCollisionResponseExact(hit);
Assert.Equal(0x3EECC54Bu, BitConverter.SingleToUInt32Bits(graph.PeakZ));
Assert.Equal(6, graph.FirstContactTick);
AssertTerminalStateExact(
graph,
0xC18334B2u,
0x00000000u,
0xC200D006u,
0x3EE4F92Eu,
0x00000000u,
0xC1E40B5Du);
_output.WriteLine(
$"uphill-jump: peak={graph.PeakZ:R}, contactTick={graph.FirstContactTick}, " +
$"hitCandidateVelocity={hit.CandidateVelocity}, " +
$"hitPreResponseVelocity={hit.PreResponseVelocity}, " +
$"hitVelocity={hit.Velocity}, " +
$"terminalPos={graph.Frames[^1].Position}, " +
$"terminalVelocity={graph.Frames[^1].Velocity}, " +
$"terminalFlags={graph.Frames[^1].TransientState}, " +
$"terminalSliding={graph.Frames[^1].SlidingNormal}");
}
[Fact]
public void PositiveZJump_HeadOnlyCollision_UsesExactRetailElasticReflection()
{
var fixture = ElevatedHeadWall();
Vector3 initialPosition = new(-0.60f, 0f, 0f);
Vector3 initialVelocity = new(2f, 0f, 2f);
QuantumTrace graph = RunTrace(
preparedFlat: false,
initialPosition,
initialVelocity,
ticks: 12,
fixture);
QuantumTrace flat = RunTrace(
preparedFlat: true,
initialPosition,
initialVelocity,
ticks: 12,
fixture);
AssertTraceExact(graph, flat);
int hitIndex = graph.Frames.FindIndex(frame => frame.CollisionNormalValid);
Assert.InRange(hitIndex, 0, graph.Frames.Count - 1);
QuantumFrame hit = graph.Frames[hitIndex];
Assert.True(hit.CandidateVelocity.Z > 0f,
$"The head-only collision did not occur during positive-Z travel: {hit.CandidateVelocity}.");
Assert.True(Vector3.Dot(hit.PreResponseVelocity, hit.CollisionNormal) < 0f,
$"The fixture did not exercise the inward reflection branch: " +
$"v={hit.PreResponseVelocity}, n={hit.CollisionNormal}.");
AssertCollisionResponseExact(hit);
AssertFloatBits(hit.PreResponseVelocity.Z, hit.Velocity.Z);
Assert.True(hit.Velocity.X < 0f,
$"The 5%-elastic retail reflection did not reverse the inward component: {hit.Velocity}.");
for (int i = hitIndex + 1; i < graph.Frames.Count; i++)
{
Assert.True(graph.Frames[i].CandidateVelocity.Z
< graph.Frames[i - 1].CandidateVelocity.Z,
$"The head collision manufactured a later vertical launch at frame {i}.");
}
}
private static QuantumTrace RunTrace(
bool preparedFlat,
Vector3 initialPosition,
Vector3 initialVelocity,
int ticks) =>
RunTrace(preparedFlat, initialPosition, initialVelocity, ticks, WideSteepRoof());
private static QuantumTrace RunTrace(
bool preparedFlat,
Vector3 initialPosition,
Vector3 initialVelocity,
int ticks,
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture)
{
PhysicsEngine engine = BuildEngine(fixture, preparedFlat);
var body = new PhysicsBody
{
Position = initialPosition,
Orientation = Quaternion.Identity,
Velocity = initialVelocity,
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(Cell, initialPosition, initialPosition);
var frames = new List<QuantumFrame>(ticks);
float peakZ = body.Position.Z;
int firstContactTick = -1;
uint cell = Cell;
for (int tick = 0; tick < ticks; tick++)
{
Vector3 preIntegratePosition = body.Position;
body.calc_acceleration();
body.UpdatePhysicsInternal(Dt);
Vector3 candidatePosition = body.Position;
Vector3 candidateVelocity = body.Velocity;
bool candidateMoved = candidatePosition != preIntegratePosition;
bool onGroundBeforeResolve = body.OnWalkable;
ResolveResult result = engine.ResolveWithTransition(
preIntegratePosition,
candidatePosition,
cell,
Radius,
sphereHeight: 1.835f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
isOnGround: onGroundBeforeResolve,
body,
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x01000000u,
sphereList: HumanSpheres,
sphereScale: 1f);
Vector3 preResponseVelocity = body.Velocity;
int preResponseStationaryFall = body.FramesStationaryFall;
// Production captures these after ResolveWithTransition has
// published plane/sliding/fsf state but before SetPositionInternal
// replaces Contact/OnWalkable. Resolve never mutates these two bits.
bool previousContact = body.InContact;
bool previousOnWalkable = body.OnWalkable;
body.CachedVelocity = candidateMoved
? (result.Position - preIntegratePosition) / Dt
: Vector3.Zero;
body.CommitTransitionPosition(result.CellId, result.Position);
cell = result.CellId;
bool commitApplied = result.Ok && candidateMoved;
if (commitApplied)
{
PhysicsObjUpdate.CommitSetPositionTransition(
body,
result.InContact,
result.OnWalkable,
result.CollisionNormalValid,
result.CollisionNormal,
previousContact,
previousOnWalkable);
}
peakZ = MathF.Max(peakZ, body.Position.Z);
if (firstContactTick < 0 && result.InContact)
firstContactTick = tick;
frames.Add(CaptureFrame(
tick,
candidatePosition,
candidateVelocity,
preResponseVelocity,
preResponseStationaryFall,
previousOnWalkable,
candidateMoved,
commitApplied,
result,
body));
}
return new QuantumTrace(frames, peakZ, firstContactTick);
}
private static (
PhysicsBSPNode Root,
Dictionary<ushort, ResolvedPolygon> Resolved) WideSteepRoof()
{
Vector3[] vertices =
[
new(-64f, -64f, -128f),
new( 64f, -64f, 128f),
new( 64f, 64f, 128f),
new(-64f, 64f, -128f),
];
Vector3 normal = Vector3.Normalize(
Vector3.Cross(vertices[1] - vertices[0], vertices[3] - vertices[0]));
if (normal.X > 0f)
normal = -normal;
float d = -Vector3.Dot(normal, vertices[0]);
var root = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere { Origin = Vector3.Zero, Radius = 192f },
};
root.Polygons.Add(RoofPolygonId);
return (root, new Dictionary<ushort, ResolvedPolygon>
{
[RoofPolygonId] = new ResolvedPolygon
{
Id = RoofPolygonId,
Vertices = vertices,
Plane = new Plane(normal, d),
NumPoints = vertices.Length,
SidesType = CullMode.None,
},
});
}
private static (
PhysicsBSPNode Root,
Dictionary<ushort, ResolvedPolygon> Resolved) ElevatedHeadWall()
{
Vector3[] vertices =
[
new(0f, 64f, 1.10f),
new(0f, -64f, 1.10f),
new(0f, -64f, 64.00f),
new(0f, 64f, 64.00f),
];
var plane = new Plane(-Vector3.UnitX, 0f);
var root = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere
{
Origin = new Vector3(0f, 0f, 32f),
Radius = 96f,
},
};
root.Polygons.Add(RoofPolygonId);
return (root, new Dictionary<ushort, ResolvedPolygon>
{
[RoofPolygonId] = new ResolvedPolygon
{
Id = RoofPolygonId,
Vertices = vertices,
Plane = plane,
NumPoints = vertices.Length,
SidesType = CullMode.None,
},
});
}
private static PhysicsEngine BuildEngine(
(PhysicsBSPNode Root, Dictionary<ushort, ResolvedPolygon> Resolved) fixture,
bool preparedFlat)
{
var normalized = new Dictionary<ushort, ResolvedPolygon>(fixture.Resolved.Count);
foreach ((ushort id, ResolvedPolygon polygon) in fixture.Resolved)
{
normalized.Add(id, new ResolvedPolygon
{
Id = id,
Vertices = polygon.Vertices,
Plane = polygon.Plane,
NumPoints = polygon.NumPoints,
SidesType = polygon.SidesType,
});
}
var physics = new GfxObjPhysics
{
SourceId = GfxId,
BSP = new PhysicsBSPTree { Root = fixture.Root },
Resolved = normalized,
BoundingSphere = fixture.Root.BoundingSphere,
};
var cache = new PhysicsDataCache();
if (preparedFlat)
{
cache.CollisionTraversalMode = CollisionTraversalMode.Flat;
cache.CacheGfxObj(GfxId, FlatCollisionAssetBuilder.FlattenGfxObj(physics));
}
else
{
cache.RegisterGfxObjForTest(GfxId, physics);
}
var heights = new byte[81];
var heightTable = new float[256];
Array.Fill(heightTable, -1000f);
var engine = new PhysicsEngine { DataCache = cache };
engine.AddLandblock(
0xA9B40000u,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
engine.ShadowObjects.Register(
GfxId,
GfxId,
Vector3.Zero,
Quaternion.Identity,
fixture.Root.BoundingSphere.Radius,
0f,
0f,
0xA9B4FFFFu,
ShadowCollisionType.BSP,
1f);
return engine;
}
private static void AssertTraceExact(QuantumTrace graph, QuantumTrace flat)
{
Assert.Equal(graph.PeakZ, flat.PeakZ);
Assert.Equal(graph.FirstContactTick, flat.FirstContactTick);
Assert.Equal(graph.Frames.Count, flat.Frames.Count);
for (int i = 0; i < graph.Frames.Count; i++)
Assert.Equal(graph.Frames[i].Bits, flat.Frames[i].Bits);
}
private static void AssertNoSteepSurfaceFixedPoint(
QuantumTrace trace,
string direction)
{
int frozenStreak = 0;
for (int i = 1; i < trace.Frames.Count; i++)
{
QuantumFrame previous = trace.Frames[i - 1];
QuantumFrame current = trace.Frames[i];
bool onSlope = IsOverSlope(current.Position)
&& current.ContactPlaneValid
&& current.ContactPlane.Normal.Z < PhysicsGlobals.FloorZ;
frozenStreak = onSlope
&& Vector3.Distance(previous.Position, current.Position) < 0.001f
? frozenStreak + 1
: 0;
Assert.True(frozenStreak <= 15,
$"{direction} fixed on the steep roof for {frozenStreak} ticks at " +
$"frame {i}, position={current.Position}.");
}
}
private static void AssertNoSlopePenetration(QuantumTrace trace, string direction)
{
Plane slope = WideSteepRoof().Resolved[RoofPolygonId].Plane;
for (int i = 0; i < trace.Frames.Count; i++)
{
QuantumFrame frame = trace.Frames[i];
Assert.True(float.IsFinite(frame.Position.X)
&& float.IsFinite(frame.Position.Y)
&& float.IsFinite(frame.Position.Z),
$"{direction} produced a non-finite position at frame {i}: {frame.Position}.");
if (!IsOverSlope(frame.Position))
continue;
Vector3 footCenter = frame.Position + HumanSpheres[0].Origin;
float signedDistance = Vector3.Dot(slope.Normal, footCenter) + slope.D;
Assert.True(signedDistance >= Radius - 0.015f,
$"{direction} penetrated the roof at frame {i}: " +
$"distance={signedDistance:R}, position={frame.Position}.");
}
}
private static bool IsOverSlope(Vector3 position) =>
position.X is >= -64f and <= 64f && MathF.Abs(position.Y) <= 64f;
private static void AssertCollisionResponseExact(QuantumFrame hit)
{
Assert.True(hit.ResultOk,
$"Frame {hit.Tick} reported contact without an accepted transition.");
Assert.True(hit.CandidateMoved,
$"Frame {hit.Tick} reported contact without a moving candidate.");
Assert.True(hit.CommitApplied,
$"Frame {hit.Tick} did not execute the production commit/response gate.");
Vector3 expected = hit.PreResponseVelocity;
bool shouldReflect = !hit.PreviousOnWalkable || !hit.BodyOnWalkable;
if (hit.PreResponseStationaryFall > 1)
{
expected = Vector3.Zero;
}
else if (shouldReflect && hit.CollisionNormalValid)
{
float dot = Vector3.Dot(expected, hit.CollisionNormal);
if (dot < 0f)
expected += hit.CollisionNormal * (-(dot * 1.05f));
}
AssertVectorBits(expected, hit.Velocity);
}
private static void AssertTerminalStateExact(
QuantumTrace trace,
uint positionXBits,
uint positionYBits,
uint positionZBits,
uint velocityXBits,
uint velocityYBits,
uint velocityZBits)
{
QuantumFrame terminal = trace.Frames[^1];
Assert.Equal(positionXBits, BitConverter.SingleToUInt32Bits(terminal.Position.X));
Assert.Equal(positionYBits, BitConverter.SingleToUInt32Bits(terminal.Position.Y));
Assert.Equal(positionZBits, BitConverter.SingleToUInt32Bits(terminal.Position.Z));
Assert.Equal(velocityXBits, BitConverter.SingleToUInt32Bits(terminal.Velocity.X));
Assert.Equal(velocityYBits, BitConverter.SingleToUInt32Bits(terminal.Velocity.Y));
Assert.Equal(velocityZBits, BitConverter.SingleToUInt32Bits(terminal.Velocity.Z));
Assert.Equal(TransientStateFlags.Active | TransientStateFlags.Contact,
terminal.TransientState);
Assert.True(terminal.BodyInContact);
Assert.False(terminal.BodyOnWalkable);
Assert.False(terminal.BodySliding);
Assert.Equal(Vector3.Zero, terminal.SlidingNormal);
Assert.False(terminal.CollisionNormalValid);
Assert.True(terminal.ContactPlaneValid);
Assert.Equal(0xBF64F92Fu,
BitConverter.SingleToUInt32Bits(terminal.ContactPlane.Normal.X));
Assert.Equal(0x00000000u,
BitConverter.SingleToUInt32Bits(terminal.ContactPlane.Normal.Y));
Assert.Equal(0x3EE4F92Fu,
BitConverter.SingleToUInt32Bits(terminal.ContactPlane.Normal.Z));
Assert.Equal(0x80000000u,
BitConverter.SingleToUInt32Bits(terminal.ContactPlane.D));
}
private static QuantumFrame CaptureFrame(
int tick,
Vector3 candidatePosition,
Vector3 candidateVelocity,
Vector3 preResponseVelocity,
int preResponseStationaryFall,
bool previousOnWalkable,
bool candidateMoved,
bool commitApplied,
ResolveResult result,
PhysicsBody body)
{
var bits = new StringBuilder(768);
Append(bits, tick);
Append(bits, candidatePosition);
Append(bits, candidateVelocity);
Append(bits, preResponseVelocity);
Append(bits, preResponseStationaryFall);
Append(bits, candidateMoved);
Append(bits, commitApplied);
Append(bits, result.Position);
Append(bits, result.CellId);
Append(bits, result.IsOnGround);
Append(bits, result.CollisionNormalValid);
Append(bits, result.CollisionNormal);
Append(bits, result.Ok);
Append(bits, result.Orientation);
Append(bits, result.InContact);
Append(bits, result.OnWalkable);
Append(bits, body.Position);
Append(bits, body.CellPosition.ObjCellId);
Append(bits, body.CellPosition.Frame.Origin);
Append(bits, body.CellPosition.Frame.Orientation);
Append(bits, body.Velocity);
Append(bits, body.CachedVelocity);
Append(bits, body.Acceleration);
Append(bits, body.GroundNormal);
Append(bits, body.SlidingNormal);
Append(bits, body.ContactPlaneValid);
Append(bits, body.ContactPlane);
Append(bits, body.ContactPlaneCellId);
Append(bits, body.ContactPlaneIsWater);
Append(bits, body.WalkablePolygonValid);
Append(bits, body.WalkablePlane);
Append(bits, body.WalkableUp);
Append(bits, body.FramesStationaryFall);
Append(bits, (uint)body.State);
Append(bits, (uint)body.TransientState);
return new QuantumFrame(
tick,
candidatePosition,
candidateVelocity,
preResponseVelocity,
preResponseStationaryFall,
candidateMoved,
result.Ok,
commitApplied,
body.Position,
body.Velocity,
body.TransientState,
body.InContact,
body.OnWalkable,
(body.TransientState & TransientStateFlags.Sliding) != 0,
body.SlidingNormal,
result.CollisionNormalValid,
result.CollisionNormal,
previousOnWalkable,
body.ContactPlaneValid,
body.ContactPlane,
bits.ToString());
}
private static void Append(StringBuilder target, bool value) =>
target.Append(value ? "1|" : "0|");
private static void Append(StringBuilder target, int value) =>
target.Append(value).Append('|');
private static void Append(StringBuilder target, uint value) =>
target.Append(value.ToString("X8")).Append('|');
private static void Append(StringBuilder target, float value) =>
Append(target, BitConverter.SingleToUInt32Bits(value));
private static void Append(StringBuilder target, Vector3 value)
{
Append(target, value.X);
Append(target, value.Y);
Append(target, value.Z);
}
private static void Append(StringBuilder target, Quaternion value)
{
Append(target, value.X);
Append(target, value.Y);
Append(target, value.Z);
Append(target, value.W);
}
private static void Append(StringBuilder target, Plane value)
{
Append(target, value.Normal);
Append(target, value.D);
}
private static void AssertFloatBits(float expected, float actual) =>
Assert.Equal(
BitConverter.SingleToUInt32Bits(expected),
BitConverter.SingleToUInt32Bits(actual));
private static void AssertVectorBits(Vector3 expected, Vector3 actual)
{
AssertFloatBits(expected.X, actual.X);
AssertFloatBits(expected.Y, actual.Y);
AssertFloatBits(expected.Z, actual.Z);
}
private sealed record QuantumTrace(
List<QuantumFrame> Frames,
float PeakZ,
int FirstContactTick);
private sealed record QuantumFrame(
int Tick,
Vector3 CandidatePosition,
Vector3 CandidateVelocity,
Vector3 PreResponseVelocity,
int PreResponseStationaryFall,
bool CandidateMoved,
bool ResultOk,
bool CommitApplied,
Vector3 Position,
Vector3 Velocity,
TransientStateFlags TransientState,
bool BodyInContact,
bool BodyOnWalkable,
bool BodySliding,
Vector3 SlidingNormal,
bool CollisionNormalValid,
Vector3 CollisionNormal,
bool PreviousOnWalkable,
bool ContactPlaneValid,
Plane ContactPlane,
string Bits);
}

View file

@ -6,13 +6,10 @@ using Xunit.Abstractions;
namespace AcDream.Core.Tests.Physics; namespace AcDream.Core.Tests.Physics;
/// <summary> /// <summary>
/// Campaign P Slice 2B's production-shaped steep-roof control. It carries /// Historical resolver-only steep-roof control retained for the original
/// contact state between 30 Hz resolves exactly as the live PhysicsBody path /// half-second wedge signature. The authoritative production chronology and
/// does, so the nested edge/StepDown dispatcher can turn a vertical landing /// graph/flat direction matrix live in
/// into retail's downhill response instead of the old under-modeled fixed /// <see cref="Ts4ProductionQuantumConformanceTests"/>.
/// point. The paired graph/flat direction matrix in
/// <see cref="RetailEdgeResponseOrderingTests"/> covers vertical, inward,
/// tangential, uphill, downhill, wall, roof, and ledge histories.
/// </summary> /// </summary>
public class Ts4SteepRoofWedgeCaptureTests public class Ts4SteepRoofWedgeCaptureTests
{ {
@ -21,7 +18,7 @@ public class Ts4SteepRoofWedgeCaptureTests
private const uint CellId = 0xA9B40001u; private const uint CellId = 0xA9B40001u;
private const int TicksPerSecond = 30; // #32 L.5 retail physics tick rate private const int TicksPerSecond = 30; // #32 L.5 retail physics tick rate
private const int MaxTicks = 6 * TicksPerSecond; private const int MaxTicks = 3 * TicksPerSecond;
private const int WedgeTickThreshold = 15; // 0.5 s of zero motion == wedged private const int WedgeTickThreshold = 15; // 0.5 s of zero motion == wedged
private const float WedgeEpsilon = 0.001f; // 1 mm private const float WedgeEpsilon = 0.001f; // 1 mm
@ -74,12 +71,12 @@ public class Ts4SteepRoofWedgeCaptureTests
/// <summary> /// <summary>
/// Falls a player-flagged mover from directly above the slope and carries /// Falls a player-flagged mover from directly above the slope and carries
/// each frame's contact state into the next frame, matching the production /// resolver contact bits between frames. Within the original three-second
/// PhysicsBody path. The exact edge/step-down chain must move the body /// capture window it must keep making downhill progress and never enter
/// downhill and onto the reference floor without a half-second wedge. /// the reported half-second fixed point.
/// </summary> /// </summary>
[Fact] [Fact]
public void FallOntoSteepSlope_PureVertical_NeverWedgesAndReachesFloor() public void FallOntoSteepSlope_PureVertical_NeverWedgesWithinThreeSeconds()
{ {
var engine = MakeSlopeEngine(); var engine = MakeSlopeEngine();
float r = BSPStepUpFixtures.SphereRadius; float r = BSPStepUpFixtures.SphereRadius;
@ -97,8 +94,8 @@ public class Ts4SteepRoofWedgeCaptureTests
float fallVelocityZ = 0f; float fallVelocityZ = 0f;
uint cell = CellId; uint cell = CellId;
Vector3 start = pos;
int frozenStreak = 0; int frozenStreak = 0;
bool reachedFloor = false;
for (int tick = 0; tick < MaxTicks; tick++) for (int tick = 0; tick < MaxTicks; tick++)
{ {
@ -148,15 +145,10 @@ public class Ts4SteepRoofWedgeCaptureTests
Assert.True(frozenStreak <= WedgeTickThreshold, Assert.True(frozenStreak <= WedgeTickThreshold,
$"Body froze for {frozenStreak} ticks at {pos}."); $"Body froze for {frozenStreak} ticks at {pos}.");
if (pos.X < 0f && pos.Z <= r + 0.05f)
{
reachedFloor = true;
break;
}
} }
Assert.True(reachedFloor, Assert.True(pos.X < start.X - 0.10f,
$"The production-shaped vertical trace did not reach the floor within " + $"The resolver-only trace made no downhill progress within " +
$"{MaxTicks} ticks; final=({pos.X:F3},{pos.Y:F3},{pos.Z:F3})."); $"{MaxTicks} ticks; start={start}, final={pos}.");
} }
} }