test(physics): harden TS-4 production chronology
This commit is contained in:
parent
75b6f6b6c9
commit
d3c0d9ec0e
5 changed files with 807 additions and 101 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue