feat(physics): add differential flat BSP traversal
Port every current containment, overlap, walkable, and six-path moving-collision query to immutable integer-indexed assets behind a graph-authoritative referee. Exact synthetic, installed-DAT, complete-resolver, and zero-allocation gates prove bit-identical behavior before connected dual publication. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
9cd42417a8
commit
f7ff9f4eea
13 changed files with 3907 additions and 46 deletions
12
AGENTS.md
12
AGENTS.md
|
|
@ -169,9 +169,16 @@ interface shares the render package's single mmap. Two complete
|
||||||
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
||||||
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
||||||
package, Release build, and 8,371-test / 5-skip gates pass.
|
package, Release build, and 8,371-test / 5-skip gates pass.
|
||||||
|
Slice I4 is complete: every current containment, overlap, walkable, and
|
||||||
|
six-path moving-collision entry point has an integer-indexed flat shadow port.
|
||||||
|
The exact referee passes 13 focused tests, 7,500 installed-DAT comparisons,
|
||||||
|
1,200 complete resolver frames, and 10,000 warmed iterations at zero managed
|
||||||
|
bytes with no mismatch. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
|
Production remains graph-authoritative until the connected shadow and cutover
|
||||||
|
gates.
|
||||||
Slice H's retained UI/frame, bounded-light, and pooled/borrowed/direct network
|
Slice H's retained UI/frame, bounded-light, and pooled/borrowed/direct network
|
||||||
work is complete; the exact seven-checkpoint connected lifecycle/reconnect gate
|
work is complete; the exact seven-checkpoint connected lifecycle/reconnect gate
|
||||||
passes. Slice I is active at I4 from
|
passes. Slice I is active at I5 from
|
||||||
`docs/plans/2026-07-25-modern-runtime-slice-i.md`. The exact G4 visual rollback
|
`docs/plans/2026-07-25-modern-runtime-slice-i.md`. The exact G4 visual rollback
|
||||||
is `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`; do not revert G3 or
|
is `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`; do not revert G3 or
|
||||||
the portal-warmup corrections. Evidence:
|
the portal-warmup corrections. Evidence:
|
||||||
|
|
@ -179,7 +186,8 @@ the portal-warmup corrections. Evidence:
|
||||||
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
||||||
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
||||||
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
||||||
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md`.
|
||||||
|
|
||||||
**Structural prerequisite before new M4 subsystem work:** all eight
|
**Structural prerequisite before new M4 subsystem work:** all eight
|
||||||
behavior-preserving `GameWindow` decomposition slices and the automated
|
behavior-preserving `GameWindow` decomposition slices and the automated
|
||||||
|
|
|
||||||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -167,9 +167,16 @@ interface shares the render package's single mmap. Two complete
|
||||||
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
||||||
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
||||||
package, Release build, and 8,371-test / 5-skip gates pass.
|
package, Release build, and 8,371-test / 5-skip gates pass.
|
||||||
|
Slice I4 is complete: every current containment, overlap, walkable, and
|
||||||
|
six-path moving-collision entry point has an integer-indexed flat shadow port.
|
||||||
|
The exact referee passes 13 focused tests, 7,500 installed-DAT comparisons,
|
||||||
|
1,200 complete resolver frames, and 10,000 warmed iterations at zero managed
|
||||||
|
bytes with no mismatch. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
|
Production remains graph-authoritative until the connected shadow and cutover
|
||||||
|
gates.
|
||||||
Slice H's retained UI/frame, bounded-light, and pooled/borrowed/direct network
|
Slice H's retained UI/frame, bounded-light, and pooled/borrowed/direct network
|
||||||
work is complete; the exact seven-checkpoint connected lifecycle/reconnect gate
|
work is complete; the exact seven-checkpoint connected lifecycle/reconnect gate
|
||||||
passes. Slice I is active at I4 from
|
passes. Slice I is active at I5 from
|
||||||
`docs/plans/2026-07-25-modern-runtime-slice-i.md`. The exact G4 visual rollback
|
`docs/plans/2026-07-25-modern-runtime-slice-i.md`. The exact G4 visual rollback
|
||||||
is `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`; do not revert G3 or
|
is `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`; do not revert G3 or
|
||||||
the portal-warmup corrections. Evidence:
|
the portal-warmup corrections. Evidence:
|
||||||
|
|
@ -177,7 +184,8 @@ the portal-warmup corrections. Evidence:
|
||||||
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
||||||
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
||||||
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
||||||
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md`.
|
||||||
|
|
||||||
**Structural prerequisite before new M4 subsystem work:** all eight
|
**Structural prerequisite before new M4 subsystem work:** all eight
|
||||||
behavior-preserving `GameWindow` decomposition slices and the automated
|
behavior-preserving `GameWindow` decomposition slices and the automated
|
||||||
|
|
|
||||||
|
|
@ -1654,13 +1654,20 @@ port in any phase — no separate listing here.
|
||||||
> defines deterministic immutable collision records and validates exact
|
> defines deterministic immutable collision records and validates exact
|
||||||
> source identity over synthetic corruption cases and representative installed
|
> source identity over synthetic corruption cases and representative installed
|
||||||
> DATs; production traversal remains on the graph oracle until the later
|
> DATs; production traversal remains on the graph oracle until the later
|
||||||
> differential gate.
|
> differential gate. Slice I3 appends the complete prepared collision package
|
||||||
|
> to the existing single mmap. Slice I4 now ports every current containment,
|
||||||
|
> overlap, walkable, and six-path moving-collision entry point to the immutable
|
||||||
|
> integer-indexed assets. Its exact referee passes 13 focused tests, 7,500
|
||||||
|
> installed-DAT comparisons, 1,200 complete resolver frames, and 10,000 warmed
|
||||||
|
> iterations at zero managed bytes with no mismatch. Production remains
|
||||||
|
> graph-authoritative while I5 dual publication and the connected shadow gate
|
||||||
|
> are active. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
> The historical exact cutover
|
> The historical exact cutover
|
||||||
> rollback remains
|
> rollback remains
|
||||||
> `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`. Slice H is complete:
|
> `git revert ef1d263337997bb030eadb7b8e71d73dc659907a`. Slice H is complete:
|
||||||
> retained UI/frame work, exact bounded light selection, and
|
> retained UI/frame work, exact bounded light selection, and
|
||||||
> pooled/borrowed/direct network I/O pass 8,292 Release tests / 5 skips and the
|
> pooled/borrowed/direct network I/O pass 8,292 Release tests / 5 skips and the
|
||||||
> connected lifecycle/reconnect gate. Slice I is active at I4 from
|
> connected lifecycle/reconnect gate. Slice I is active at I5 from
|
||||||
> [its detailed plan](2026-07-25-modern-runtime-slice-i.md).
|
> [its detailed plan](2026-07-25-modern-runtime-slice-i.md).
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||||
|
|
@ -1685,11 +1692,11 @@ hitch and follows.
|
||||||
| MP1a | `AcDream.Content` extraction (GL-free MeshExtractor + boundary records out of App) | ✅ SHIPPED 2026-07-05 — user-gated (renders identical, zero tripwires, perf-neutral); 8 commits `651d041e`..`b0758d77` |
|
| MP1a | `AcDream.Content` extraction (GL-free MeshExtractor + boundary records out of App) | ✅ SHIPPED 2026-07-05 — user-gated (renders identical, zero tripwires, perf-neutral); 8 commits `651d041e`..`b0758d77` |
|
||||||
| MP1b | Pak format + `acdream-bake` CLI + mmap `PakReader` + equivalence/full-scale gate | ✅ **SHIPPED 2026-07-24** — 729,888 EnvCell keys → 17,117 unique geometries + 712,771 aliases (42.6×), 751,141 total keys / 38,370 physical blobs, zero failures, 28,192.4 MiB, 81.4 s validated atomic publish; full-DAT gate also corrected a real collision in WB's legacy geometry hash. [Report](../research/2026-07-24-slice-b-full-bake-report.md) |
|
| MP1b | Pak format + `acdream-bake` CLI + mmap `PakReader` + equivalence/full-scale gate | ✅ **SHIPPED 2026-07-24** — 729,888 EnvCell keys → 17,117 unique geometries + 712,771 aliases (42.6×), 751,141 total keys / 38,370 physical blobs, zero failures, 28,192.4 MiB, 81.4 s validated atomic publish; full-DAT gate also corrected a real collision in WB's legacy geometry hash. [Report](../research/2026-07-24-slice-b-full-bake-report.md) |
|
||||||
| **MP-Alloc (safe batch)** | Reuse per-frame buffers: anim pose, particle draw-list, interior partition, animatedIds/drawableCells sets | ✅ SHIPPED + USER-GATED 2026-07-05 — dense-town frame-time spikes 20–87ms → 6–10ms, alloc spikes (30–75MB single-frame) eliminated, gen2 GC 5–11/window → ~0; user confirms FPS steady. 4 commits `b8c05e2b`..`91afea24` |
|
| **MP-Alloc (safe batch)** | Reuse per-frame buffers: anim pose, particle draw-list, interior partition, animatedIds/drawableCells sets | ✅ SHIPPED + USER-GATED 2026-07-05 — dense-town frame-time spikes 20–87ms → 6–10ms, alloc spikes (30–75MB single-frame) eliminated, gen2 GC 5–11/window → ~0; user confirms FPS steady. 4 commits `b8c05e2b`..`91afea24` |
|
||||||
| MP-Alloc (hard sites) | EnvCell settled-camera rebuild gate + physics `Transition` reuse | 🟡 Physics half SHIPPED 2026-07-25 — retail-shaped reset-complete transition/query scratch is bit-identical and 0 B/resolve across five profiles; immutable flat collision storage and its complete deterministic prepared package are shipped; traversal differential work continues in Slice I4. |
|
| MP-Alloc (hard sites) | EnvCell settled-camera rebuild gate + physics `Transition` reuse | 🟡 Physics half SHIPPED 2026-07-25 — retail-shaped reset-complete transition/query scratch is bit-identical and 0 B/resolve across five profiles; immutable flat collision storage, its deterministic prepared package, and the zero-mismatch/zero-allocation flat traversal referee are shipped; connected dual publication continues in Slice I5. |
|
||||||
| MP1c | Streaming cutover to pak + hitch gate | ✅ **SHIPPED 2026-07-24** — typed package-only production source, explicit Setup probes, non-allocating Portal-first lookup, exact translucency metadata, and ordered teardown; capped/uncapped/dense physical routes + user visual gate pass. Uncapped CPU p99 7.825→6.496 ms, GPU p99 3.406→2.706 ms, portal frame max 202.9→39.9 MiB, process allocation −41.8%, zero invalid Setup probes. [Report](../research/2026-07-24-slice-c-prepared-asset-cutover-report.md) |
|
| MP1c | Streaming cutover to pak + hitch gate | ✅ **SHIPPED 2026-07-24** — typed package-only production source, explicit Setup probes, non-allocating Portal-first lookup, exact translucency metadata, and ordered teardown; capped/uncapped/dense physical routes + user visual gate pass. Uncapped CPU p99 7.825→6.496 ms, GPU p99 3.406→2.706 ms, portal frame max 202.9→39.9 MiB, process allocation −41.8%, zero invalid Setup probes. [Report](../research/2026-07-24-slice-c-prepared-asset-cutover-report.md) |
|
||||||
| MP2 | Retail particle distance/cell-view degradation: DAT-authored range, exact infinite/finite off-view update branches, emitter-first render scan, user-requested 2× default range | ✅ SHIPPED 2026-07-17 — Aerlinthe hotspot `0x32000223` resolves to retail 64m and defaults to 128m; landscape/entity streaming distance unchanged |
|
| MP2 | Retail particle distance/cell-view degradation: DAT-authored range, exact infinite/finite off-view update branches, emitter-first render scan, user-requested 2× default range | ✅ SHIPPED 2026-07-17 — Aerlinthe hotspot `0x32000223` resolves to retail 64m and defaults to 128m; landscape/entity streaming distance unchanged |
|
||||||
| MP3 | Arch ECS render world + delta submission (the 300-FPS lever) | ⚪ — note: does NOT fix the steady-state GC churn (that's MP-Alloc); MP3 is the draw-submission throughput lever |
|
| MP3 | Arch ECS render world + delta submission (the 300-FPS lever) | ⚪ — note: does NOT fix the steady-state GC churn (that's MP-Alloc); MP3 is the draw-submission throughput lever |
|
||||||
| MP4 | Zero-alloc frame loop + flat physics data (residual, post-MP-Alloc) | 🟡 ACTIVE — Slice I1 zero-allocation transition scratch, I2 immutable schema, and I3 complete deterministic collision package shipped; I4 flat traversal shadow implementation is active. |
|
| MP4 | Zero-alloc frame loop + flat physics data (residual, post-MP-Alloc) | 🟡 ACTIVE — Slice I1 zero-allocation transition scratch, I2 immutable schema, I3 deterministic collision package, and I4 exact flat traversal referee shipped; I5 dual publication and connected shadow verification are active. |
|
||||||
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,14 @@ prepared-collision interface over the existing single mmap. Two complete
|
||||||
29,908,271,024-byte bakes contain 2,232,170 keys with zero failures and the
|
29,908,271,024-byte bakes contain 2,232,170 keys with zero failures and the
|
||||||
same SHA-256 across different worker counts; cancellation/corruption gates and
|
same SHA-256 across different worker counts; cancellation/corruption gates and
|
||||||
8,371 solution tests / 5 skips pass.
|
8,371 solution tests / 5 skips pass.
|
||||||
|
Slice I4 ports every current collision entry point to integer-indexed flat
|
||||||
|
assets behind a graph-authoritative referee. Thirteen focused tests, 10,000
|
||||||
|
randomized static/swept comparisons, 5,000 walkable comparisons, 7,500
|
||||||
|
installed-DAT comparisons, and 1,200 complete resolver frames have exact
|
||||||
|
state/float-bit equality; 10,000 warmed flat iterations allocate zero managed
|
||||||
|
bytes. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
Slice H is complete: retained UI/frame work, exact bounded light selection,
|
Slice H is complete: retained UI/frame work, exact bounded light selection,
|
||||||
and pooled/borrowed/direct network I/O pass. Slice I is active at I4 under
|
and pooled/borrowed/direct network I/O pass. Slice I is active at I5 under
|
||||||
[`2026-07-25-modern-runtime-slice-i.md`](2026-07-25-modern-runtime-slice-i.md).
|
[`2026-07-25-modern-runtime-slice-i.md`](2026-07-25-modern-runtime-slice-i.md).
|
||||||
The historical exact G4 visual rollback remains
|
The historical exact G4 visual rollback remains
|
||||||
`git revert ef1d263337997bb030eadb7b8e71d73dc659907a`.
|
`git revert ef1d263337997bb030eadb7b8e71d73dc659907a`.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
||||||
|
|
||||||
**Status:** I0–I3 COMPLETE — retail oracle, zero-allocation transition
|
**Status:** I0-I4 COMPLETE - retail oracle, zero-allocation transition
|
||||||
scratch, deterministic immutable collision records, and complete prepared
|
scratch, deterministic immutable collision records, complete prepared
|
||||||
package fixed; I4 active
|
package, and exact flat-traversal differential gate fixed; I5 active
|
||||||
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice I
|
**Parent:** `2026-07-24-modern-runtime-architecture.md`, Slice I
|
||||||
**Purpose:** remove parsed DAT object graphs and steady collision allocations
|
**Purpose:** remove parsed DAT object graphs and steady collision allocations
|
||||||
without changing one retail collision decision, float, comparison, or traversal
|
without changing one retail collision decision, float, comparison, or traversal
|
||||||
|
|
@ -213,6 +213,16 @@ tests / 5 skips pass. Production traversal remains graph-only. Evidence:
|
||||||
Gate: zero differential mismatch. Any mismatch blocks cutover; no tolerance
|
Gate: zero differential mismatch. Any mismatch blocks cutover; no tolerance
|
||||||
band is permitted for deterministic scalar results.
|
band is permitted for deterministic scalar results.
|
||||||
|
|
||||||
|
**PASSED 2026-07-25:** the integer-indexed shadow ports every current
|
||||||
|
containment, overlap, walkable, and six-path moving-collision entry point.
|
||||||
|
The 13-test referee completed 10,000 randomized static/swept comparisons,
|
||||||
|
5,000 walkable comparisons, 7,500 installed-DAT comparisons across three
|
||||||
|
representative cells, all six dispatcher paths, and 1,200 complete resolver
|
||||||
|
frames with exact state and float-bit equality. Ten thousand warmed flat
|
||||||
|
iterations allocated zero managed bytes. Production remains graph-authoritative
|
||||||
|
until I5/I6. Release build and 8,384 solution tests / 5 skips pass. Evidence:
|
||||||
|
[`../research/2026-07-25-slice-i4-flat-bsp-differential.md`](../research/2026-07-25-slice-i4-flat-bsp-differential.md).
|
||||||
|
|
||||||
### I5 — dual publication and connected shadow gate
|
### I5 — dual publication and connected shadow gate
|
||||||
|
|
||||||
1. Extend collision preparation so a `LandblockBuild` carries immutable flat
|
1. Extend collision preparation so a `LandblockBuild` carries immutable flat
|
||||||
|
|
|
||||||
87
docs/research/2026-07-25-slice-i4-flat-bsp-differential.md
Normal file
87
docs/research/2026-07-25-slice-i4-flat-bsp-differential.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Slice I4 - differential flat BSP traversal
|
||||||
|
|
||||||
|
**Date:** 2026-07-25
|
||||||
|
|
||||||
|
**Scope:** Modern Runtime Slice I4 only
|
||||||
|
|
||||||
|
**Behavioral boundary:** the new integer-indexed traversal is a shadow
|
||||||
|
implementation. The parsed-DAT graph remains production-authoritative until
|
||||||
|
the Slice I5 connected shadow gate and Slice I6 cutover.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
`FlatBspQuery` now ports every collision entry point used by the current
|
||||||
|
runtime over immutable, integer-indexed nodes and polygon ranges:
|
||||||
|
|
||||||
|
- point-in-cell;
|
||||||
|
- sphere/cell overlap;
|
||||||
|
- static sphere/polygon overlap;
|
||||||
|
- swept sphere/polygon time-of-impact;
|
||||||
|
- walkable polygon selection;
|
||||||
|
- solid and solid-polygon traversal;
|
||||||
|
- the complete six-path moving-collision dispatcher.
|
||||||
|
|
||||||
|
The port preserves retail's traversal order, strict comparisons, early
|
||||||
|
returns, and mutable query state. The positive child remains first where
|
||||||
|
retail visits it first; the negative child observes every valid-sphere,
|
||||||
|
contact, path, slide, and candidate mutation made by the positive child.
|
||||||
|
Only `BSPQuery.PolygonHitsSpherePrecise`, which is independent of graph
|
||||||
|
storage, is shared with the existing graph oracle.
|
||||||
|
|
||||||
|
`CollisionTraversal` is the narrow temporary referee seam. Production starts
|
||||||
|
in `Graph` mode. Tests can run a cloned complete resolver in `Flat` mode, and
|
||||||
|
a missing flat asset fails explicitly instead of falling back to the graph.
|
||||||
|
Slice I6 removes this representation switch after connected acceptance.
|
||||||
|
|
||||||
|
## Retail oracle
|
||||||
|
|
||||||
|
The implementation follows the consolidated pseudocode and boundary table in
|
||||||
|
[`2026-07-25-slice-i-collision-layout-oracle.md`](2026-07-25-slice-i-collision-layout-oracle.md),
|
||||||
|
including:
|
||||||
|
|
||||||
|
- `BSPTREE::find_collisions` at `0x0053A440`;
|
||||||
|
- `BSPNODE::point_inside_cell_bsp` at `0x0053C1F0`;
|
||||||
|
- `BSPNODE::sphere_intersects_cell_bsp` at `0x0053C260`;
|
||||||
|
- `BSPNODE::sphere_intersects_poly` at `0x0053CA30`;
|
||||||
|
- `BSPNODE::sphere_intersects_solid` at `0x0053CAF0`;
|
||||||
|
- `BSPNODE::find_walkable` at `0x0053CC80`;
|
||||||
|
- `BSPNODE::sphere_intersects_solid_poly` at `0x0053CD50`;
|
||||||
|
- `BSPLEAF::find_walkable` at `0x0053D6F0`.
|
||||||
|
|
||||||
|
The same oracle records the cross-checks against ACE master
|
||||||
|
`65f092...` and Chorizite DatReaderWriter `c535987...`. Retail remains the
|
||||||
|
authority where those references differ.
|
||||||
|
|
||||||
|
## Differential evidence
|
||||||
|
|
||||||
|
The referee compares scalar results and all stateful outputs by exact float
|
||||||
|
bits. It covers selected polygons, vectors, planes, path state, collision and
|
||||||
|
object flags, contact-plane writes, final `ResolveResult`, persistent body
|
||||||
|
state, and full cell membership.
|
||||||
|
|
||||||
|
| Gate | Executions | Result |
|
||||||
|
|---|---:|---:|
|
||||||
|
| Null, on-plane, radius-equality, equal-candidate, multi-polygon, and deep-tree fixtures | 4 focused fixtures | 0 mismatches |
|
||||||
|
| Static and swept overlap randomized sweep | 10,000 paired cases | 0 mismatches |
|
||||||
|
| Walkable randomized sweep | 5,000 paired cases | 0 mismatches |
|
||||||
|
| Six dispatcher paths | 6 complete transition fixtures | 0 mismatches |
|
||||||
|
| Installed-DAT sweep across `0x8A02016E`, `0x8A02017A`, and `0xA9B4013F` | 7,500 randomized cases | 0 mismatches |
|
||||||
|
| Complete graph/flat resolver sequences | 1,200 frames | 0 mismatches |
|
||||||
|
| Warmed flat traversal | 10,000 iterations | 0 managed bytes |
|
||||||
|
|
||||||
|
The installed-DAT gate randomizes points, spheres, movement, insert paths,
|
||||||
|
orientations, scale, and boundary values. No tolerance band is used.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- focused differential tests: 13 passed;
|
||||||
|
- complete Core suite: 3,276 passed / 2 skipped;
|
||||||
|
- `git diff --check`: no whitespace errors;
|
||||||
|
- full Release build: passed;
|
||||||
|
- complete Release suite: 8,384 passed / 5 skipped.
|
||||||
|
|
||||||
|
No divergence-register row changes are required. This slice changes the
|
||||||
|
storage representation exercised by the referee, not production gameplay
|
||||||
|
behavior. Slice I5 next publishes the prepared flat assets alongside the
|
||||||
|
current graph under one landblock generation and runs the connected shadow
|
||||||
|
gate.
|
||||||
|
|
@ -96,7 +96,7 @@ public static class BSPQuery
|
||||||
///
|
///
|
||||||
/// <para>ACE: Polygon.cs polygon_hits_sphere_precise.</para>
|
/// <para>ACE: Polygon.cs polygon_hits_sphere_precise.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool PolygonHitsSpherePrecise(
|
internal static bool PolygonHitsSpherePrecise(
|
||||||
in Plane polyPlane,
|
in Plane polyPlane,
|
||||||
ReadOnlySpan<Vector3> verts,
|
ReadOnlySpan<Vector3> verts,
|
||||||
Vector3 sphereCenter,
|
Vector3 sphereCenter,
|
||||||
|
|
@ -2339,6 +2339,31 @@ public static class BSPQuery
|
||||||
ref hitPolyId, ref hitNormal);
|
ref hitPolyId, ref hitNormal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocation-free graph-oracle seam used by the Slice I flat traversal
|
||||||
|
/// differential harness.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool SphereIntersectsPoly(
|
||||||
|
PhysicsBSPNode? node,
|
||||||
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||||
|
Vector3 sphereCenter,
|
||||||
|
float sphereRadius,
|
||||||
|
out ushort hitPolyId,
|
||||||
|
out Vector3 hitNormal)
|
||||||
|
{
|
||||||
|
hitPolyId = 0;
|
||||||
|
hitNormal = Vector3.Zero;
|
||||||
|
if (node is null) return false;
|
||||||
|
|
||||||
|
return SphereIntersectsPolyStaticRecurse(
|
||||||
|
node,
|
||||||
|
resolved,
|
||||||
|
sphereCenter,
|
||||||
|
sphereRadius,
|
||||||
|
ref hitPolyId,
|
||||||
|
ref hitNormal);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool SphereIntersectsPolyStaticRecurse(
|
private static bool SphereIntersectsPolyStaticRecurse(
|
||||||
PhysicsBSPNode? node,
|
PhysicsBSPNode? node,
|
||||||
Dictionary<ushort, ResolvedPolygon> resolved,
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||||
|
|
@ -2431,6 +2456,38 @@ public static class BSPQuery
|
||||||
return hitTime < float.MaxValue;
|
return hitTime < float.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allocation-free graph-oracle seam used by the Slice I flat traversal
|
||||||
|
/// differential harness.
|
||||||
|
/// </summary>
|
||||||
|
internal static bool SphereIntersectsPolyWithTime(
|
||||||
|
PhysicsBSPNode? node,
|
||||||
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||||
|
Vector3 sphereCenter,
|
||||||
|
float sphereRadius,
|
||||||
|
Vector3 movement,
|
||||||
|
out ushort hitPolyId,
|
||||||
|
out Vector3 hitNormal,
|
||||||
|
out float hitTime)
|
||||||
|
{
|
||||||
|
hitPolyId = 0;
|
||||||
|
hitNormal = Vector3.Zero;
|
||||||
|
hitTime = float.MaxValue;
|
||||||
|
if (node is null) return false;
|
||||||
|
|
||||||
|
SphereIntersectsPolyWithTimeRecurse(
|
||||||
|
node,
|
||||||
|
resolved,
|
||||||
|
sphereCenter,
|
||||||
|
sphereRadius,
|
||||||
|
movement,
|
||||||
|
ref hitPolyId,
|
||||||
|
ref hitNormal,
|
||||||
|
ref hitTime);
|
||||||
|
|
||||||
|
return hitTime < float.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
private static void SphereIntersectsPolyWithTimeRecurse(
|
private static void SphereIntersectsPolyWithTimeRecurse(
|
||||||
PhysicsBSPNode? node,
|
PhysicsBSPNode? node,
|
||||||
Dictionary<ushort, ResolvedPolygon> resolved,
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
||||||
|
|
|
||||||
|
|
@ -159,15 +159,19 @@ public static class CellTransit
|
||||||
// neighbour cell whether the sphere intersects its CellBSP.
|
// neighbour cell whether the sphere intersects its CellBSP.
|
||||||
// The portal-plane side test is only the unloaded-cell load hint.
|
// The portal-plane side test is only the unloaded-cell load hint.
|
||||||
var otherCell = cache.GetCellStruct(otherId);
|
var otherCell = cache.GetCellStruct(otherId);
|
||||||
if (otherCell?.CellBSP?.Root is not null)
|
if (otherCell is not null &&
|
||||||
|
CollisionTraversal.HasCellContainment(cache, otherCell))
|
||||||
{
|
{
|
||||||
for (int i = 0; i < sphereCount; i++)
|
for (int i = 0; i < sphereCount; i++)
|
||||||
{
|
{
|
||||||
var sphere = worldSpheres[i];
|
var sphere = worldSpheres[i];
|
||||||
var otherLocalCenter = Vector3.Transform(
|
var otherLocalCenter = Vector3.Transform(
|
||||||
sphere.Origin, otherCell.InverseWorldTransform);
|
sphere.Origin, otherCell.InverseWorldTransform);
|
||||||
bool hit = BSPQuery.SphereIntersectsCellBsp(
|
bool hit = CollisionTraversal.SphereIntersectsCell(
|
||||||
otherCell.CellBSP.Root, otherLocalCenter, sphere.Radius);
|
cache,
|
||||||
|
otherCell,
|
||||||
|
otherLocalCenter,
|
||||||
|
sphere.Radius);
|
||||||
if (hit)
|
if (hit)
|
||||||
{
|
{
|
||||||
candidates.Add(otherId);
|
candidates.Add(otherId);
|
||||||
|
|
@ -372,7 +376,8 @@ public static class CellTransit
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var otherCell = cache.GetCellStruct(portal.OtherCellId);
|
var otherCell = cache.GetCellStruct(portal.OtherCellId);
|
||||||
if (otherCell?.CellBSP?.Root is null)
|
if (otherCell is null ||
|
||||||
|
!CollisionTraversal.HasCellContainment(cache, otherCell))
|
||||||
{
|
{
|
||||||
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
{
|
{
|
||||||
|
|
@ -398,8 +403,11 @@ public static class CellTransit
|
||||||
{
|
{
|
||||||
var sphere = worldSpheres[i];
|
var sphere = worldSpheres[i];
|
||||||
var localCenter = Vector3.Transform(sphere.Origin, otherCell.InverseWorldTransform);
|
var localCenter = Vector3.Transform(sphere.Origin, otherCell.InverseWorldTransform);
|
||||||
inside = BSPQuery.SphereIntersectsCellBsp(
|
inside = CollisionTraversal.SphereIntersectsCell(
|
||||||
otherCell.CellBSP.Root, localCenter, sphere.Radius);
|
cache,
|
||||||
|
otherCell,
|
||||||
|
localCenter,
|
||||||
|
sphere.Radius);
|
||||||
|
|
||||||
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
{
|
{
|
||||||
|
|
@ -599,19 +607,25 @@ public static class CellTransit
|
||||||
if (start is null) return 0u;
|
if (start is null) return 0u;
|
||||||
|
|
||||||
// this->point_in_cell(point) → return this (:311402-311405)
|
// this->point_in_cell(point) → return this (:311402-311405)
|
||||||
if (PointInCell(start, worldPoint)) return startCellId;
|
if (PointInCell(cache, start, worldPoint)) return startCellId;
|
||||||
|
|
||||||
if (useStabList)
|
if (useStabList)
|
||||||
{
|
{
|
||||||
// arg3 != 0 → iterate stab_list, GetVisible + point_in_cell (:311444-311465)
|
// arg3 != 0 → iterate stab_list, GetVisible + point_in_cell (:311444-311465)
|
||||||
foreach (uint id in start.VisibleCellIds)
|
foreach (uint id in start.VisibleCellIds)
|
||||||
if (PointInCell(cache.GetCellStruct(id), worldPoint)) return id;
|
if (PointInCell(cache, cache.GetCellStruct(id), worldPoint)) return id;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
|
// arg3 == 0 → iterate direct portals, GetOtherCell + point_in_cell (:311411-311434)
|
||||||
foreach (var portal in start.Portals)
|
foreach (var portal in start.Portals)
|
||||||
if (PointInCell(cache.GetCellStruct(portal.OtherCellId), worldPoint)) return portal.OtherCellId;
|
if (PointInCell(
|
||||||
|
cache,
|
||||||
|
cache.GetCellStruct(portal.OtherCellId),
|
||||||
|
worldPoint))
|
||||||
|
{
|
||||||
|
return portal.OtherCellId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0u;
|
return 0u;
|
||||||
|
|
@ -623,11 +637,19 @@ public static class CellTransit
|
||||||
/// A cell with no hydrated <see cref="CellPhysics.CellBSP"/> returns false (see
|
/// A cell with no hydrated <see cref="CellPhysics.CellBSP"/> returns false (see
|
||||||
/// <see cref="FindVisibleChildCell"/>'s adaptation note).
|
/// <see cref="FindVisibleChildCell"/>'s adaptation note).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static bool PointInCell(CellPhysics? cell, Vector3 worldPoint)
|
private static bool PointInCell(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics? cell,
|
||||||
|
Vector3 worldPoint)
|
||||||
{
|
{
|
||||||
if (cell?.CellBSP?.Root is null) return false;
|
if (cell is null ||
|
||||||
|
!CollisionTraversal.HasCellContainment(cache, cell))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
|
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
|
||||||
return BSPQuery.PointInsideCellBsp(cell.CellBSP.Root, local);
|
return CollisionTraversal.PointInsideCell(cache, cell, local);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -901,9 +923,14 @@ public static class CellTransit
|
||||||
{
|
{
|
||||||
// Interior candidate — point_in_cell via the cell BSP (vtable[0x84]).
|
// Interior candidate — point_in_cell via the cell BSP (vtable[0x84]).
|
||||||
var cand = cache.GetCellStruct(candId);
|
var cand = cache.GetCellStruct(candId);
|
||||||
if (cand?.CellBSP?.Root is null) continue;
|
if (cand is null ||
|
||||||
|
!CollisionTraversal.HasCellContainment(cache, cand))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var local = Vector3.Transform(worldSphereCenter, cand.InverseWorldTransform);
|
var local = Vector3.Transform(worldSphereCenter, cand.InverseWorldTransform);
|
||||||
if (BSPQuery.PointInsideCellBsp(cand.CellBSP.Root, local))
|
if (CollisionTraversal.PointInsideCell(cache, cand, local))
|
||||||
return candId; // interior-wins, stop (pseudo_c:308819)
|
return candId; // interior-wins, stop (pseudo_c:308819)
|
||||||
}
|
}
|
||||||
else if (outdoorResult == 0u && containingOutdoorId != 0u && outdoorPickAllowed)
|
else if (outdoorResult == 0u && containingOutdoorId != 0u && outdoorPickAllowed)
|
||||||
|
|
@ -950,10 +977,15 @@ public static class CellTransit
|
||||||
if (currentLow >= 0x0100u)
|
if (currentLow >= 0x0100u)
|
||||||
{
|
{
|
||||||
var cur = cache.GetCellStruct(currentCellId);
|
var cur = cache.GetCellStruct(currentCellId);
|
||||||
if (cur?.CellBSP?.Root is not null)
|
if (cur is not null &&
|
||||||
|
CollisionTraversal.HasCellContainment(cache, cur))
|
||||||
{
|
{
|
||||||
var curLocal = Vector3.Transform(worldSphereCenter, cur.InverseWorldTransform);
|
var curLocal = Vector3.Transform(worldSphereCenter, cur.InverseWorldTransform);
|
||||||
if (!BSPQuery.SphereIntersectsCellBsp(cur.CellBSP.Root, curLocal, sphereRadius))
|
if (!CollisionTraversal.SphereIntersectsCell(
|
||||||
|
cache,
|
||||||
|
cur,
|
||||||
|
curLocal,
|
||||||
|
sphereRadius))
|
||||||
{
|
{
|
||||||
uint recovered = FindVisibleChildCell(
|
uint recovered = FindVisibleChildCell(
|
||||||
cache, currentCellId, worldSphereCenter, useStabList: true);
|
cache, currentCellId, worldSphereCenter, useStabList: true);
|
||||||
|
|
|
||||||
226
src/AcDream.Core/Physics/CollisionTraversal.cs
Normal file
226
src/AcDream.Core/Physics/CollisionTraversal.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Temporary Slice I referee mode. Graph remains production-authoritative;
|
||||||
|
/// Flat is enabled only by differential tests until dual publication and the
|
||||||
|
/// connected shadow gate complete.
|
||||||
|
/// </summary>
|
||||||
|
internal enum CollisionTraversalMode
|
||||||
|
{
|
||||||
|
Graph,
|
||||||
|
Flat,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Narrow representation switch used while the graph and flat collision
|
||||||
|
/// worlds coexist. I6 removes the graph branch after acceptance.
|
||||||
|
/// </summary>
|
||||||
|
internal static class CollisionTraversal
|
||||||
|
{
|
||||||
|
internal static bool HasCellContainment(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment");
|
||||||
|
return flat.RootIndex >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell.CellBSP?.Root is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasPhysics(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics");
|
||||||
|
return flat.RootIndex >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell.BSP?.Root is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool HasPhysics(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
GfxObjPhysics gfxObject)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("GfxObj physics");
|
||||||
|
return flat.RootIndex >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gfxObject.BSP.Root is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static FlatCollisionSphere RootBoundingSphere(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics");
|
||||||
|
return flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||||
|
}
|
||||||
|
|
||||||
|
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
||||||
|
return new FlatCollisionSphere(sphere.Origin, sphere.Radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool PointInsideCell(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell,
|
||||||
|
Vector3 localPoint)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment");
|
||||||
|
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSPQuery.PointInsideCellBsp(cell.CellBSP?.Root, localPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool SphereIntersectsCell(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell,
|
||||||
|
Vector3 localCenter,
|
||||||
|
float radius)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment");
|
||||||
|
return FlatBspQuery.SphereIntersectsCellBsp(
|
||||||
|
flat,
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSPQuery.SphereIntersectsCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TransitionState FindCollisions(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
CellPhysics cell,
|
||||||
|
Transition transition,
|
||||||
|
Vector3 localSphereCenter,
|
||||||
|
float localSphereRadius,
|
||||||
|
bool hasLocalSphere1,
|
||||||
|
Vector3 localSphere1Center,
|
||||||
|
float localSphere1Radius,
|
||||||
|
Vector3 localCurrentCenter,
|
||||||
|
Vector3 localSpaceZ,
|
||||||
|
float scale,
|
||||||
|
Quaternion localToWorld,
|
||||||
|
PhysicsEngine? engine,
|
||||||
|
Vector3 worldOrigin)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics");
|
||||||
|
return FlatBspQuery.FindCollisions(
|
||||||
|
flat,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSPQuery.FindCollisions(
|
||||||
|
cell.BSP?.Root,
|
||||||
|
cell.Resolved,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TransitionState FindCollisions(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
GfxObjPhysics gfxObject,
|
||||||
|
Transition transition,
|
||||||
|
Vector3 localSphereCenter,
|
||||||
|
float localSphereRadius,
|
||||||
|
bool hasLocalSphere1,
|
||||||
|
Vector3 localSphere1Center,
|
||||||
|
float localSphere1Radius,
|
||||||
|
Vector3 localCurrentCenter,
|
||||||
|
Vector3 localSpaceZ,
|
||||||
|
float scale,
|
||||||
|
Quaternion localToWorld,
|
||||||
|
PhysicsEngine? engine,
|
||||||
|
Vector3 worldOrigin)
|
||||||
|
{
|
||||||
|
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
||||||
|
{
|
||||||
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("GfxObj physics");
|
||||||
|
return FlatBspQuery.FindCollisions(
|
||||||
|
flat,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BSPQuery.FindCollisions(
|
||||||
|
gfxObject.BSP.Root,
|
||||||
|
gfxObject.Resolved,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvalidOperationException MissingFlat(string kind) =>
|
||||||
|
new(
|
||||||
|
$"Flat collision traversal requires a prepared {kind} asset. " +
|
||||||
|
"Gameplay must not fall back silently.");
|
||||||
|
}
|
||||||
2132
src/AcDream.Core/Physics/FlatBspQuery.cs
Normal file
2132
src/AcDream.Core/Physics/FlatBspQuery.cs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,14 @@ public sealed class PhysicsDataCache
|
||||||
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
private readonly ConcurrentDictionary<uint, SetupPhysics> _setup = new();
|
||||||
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
private readonly ConcurrentDictionary<uint, CellPhysics> _cellStruct = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slice I graph/flat differential selector. Production remains on the
|
||||||
|
/// graph oracle until I6; tests use the flat value to run the same complete
|
||||||
|
/// resolver from cloned inputs.
|
||||||
|
/// </summary>
|
||||||
|
internal CollisionTraversalMode CollisionTraversalMode { get; set; } =
|
||||||
|
CollisionTraversalMode.Graph;
|
||||||
|
|
||||||
// ── Phase 2: building portal cache for outdoor→indoor entry ───────────
|
// ── Phase 2: building portal cache for outdoor→indoor entry ───────────
|
||||||
private readonly ConcurrentDictionary<uint, BuildingPhysics> _buildings = new();
|
private readonly ConcurrentDictionary<uint, BuildingPhysics> _buildings = new();
|
||||||
|
|
||||||
|
|
@ -541,6 +549,12 @@ public sealed class GfxObjPhysics
|
||||||
/// Populated once at cache time so BSP queries don't pay per-test lookup cost.
|
/// Populated once at cache time so BSP queries don't pay per-test lookup cost.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared integer-indexed shadow representation. Optional until Slice I5
|
||||||
|
/// dual publication is complete.
|
||||||
|
/// </summary>
|
||||||
|
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
||||||
|
|
@ -578,6 +592,11 @@ public sealed class CellPhysics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared integer-indexed physics BSP shadow. Optional until Slice I5.
|
||||||
|
/// </summary>
|
||||||
|
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||||
|
|
||||||
// ── Indoor walking Phase 2 (2026-05-19): portal-graph fields ───────
|
// ── Indoor walking Phase 2 (2026-05-19): portal-graph fields ───────
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -590,6 +609,12 @@ public sealed class CellPhysics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared integer-indexed cell-containment BSP shadow. Optional until
|
||||||
|
/// Slice I5.
|
||||||
|
/// </summary>
|
||||||
|
public FlatCellContainmentBsp? FlatContainmentBsp { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Portal connections to neighbouring cells, in cell-local space.
|
/// Portal connections to neighbouring cells, in cell-local space.
|
||||||
/// Default: empty list. Source: <c>envCell.CellPortals</c>.
|
/// Default: empty list. Source: <c>envCell.CellPortals</c>.
|
||||||
|
|
|
||||||
|
|
@ -2298,7 +2298,11 @@ public sealed class Transition
|
||||||
|
|
||||||
var cell = engine.DataCache.GetCellStruct(cellId);
|
var cell = engine.DataCache.GetCellStruct(cellId);
|
||||||
// R2 guard: stale CellPhysics loaded for render but not physics.
|
// R2 guard: stale CellPhysics loaded for render but not physics.
|
||||||
if (cell?.BSP?.Root is null) continue;
|
if (cell is null ||
|
||||||
|
!CollisionTraversal.HasPhysics(engine.DataCache, cell))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Transform sphere into THIS cell's local space. Mirrors the
|
// Transform sphere into THIS cell's local space. Mirrors the
|
||||||
// primary-cell pattern at TransitionTypes.cs (FindEnvCollisions,
|
// primary-cell pattern at TransitionTypes.cs (FindEnvCollisions,
|
||||||
|
|
@ -2330,8 +2334,8 @@ public sealed class Transition
|
||||||
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
|
||||||
PhysicsDiagnostics.LastBspHitPoly = null;
|
PhysicsDiagnostics.LastBspHitPoly = null;
|
||||||
|
|
||||||
var result = BSPQuery.FindCollisions(
|
var result = CollisionTraversal.FindCollisions(
|
||||||
cell.BSP.Root, cell.Resolved, this,
|
engine.DataCache!, cell, this,
|
||||||
localCenter,
|
localCenter,
|
||||||
sphereRadius,
|
sphereRadius,
|
||||||
hasLocalSphere1,
|
hasLocalSphere1,
|
||||||
|
|
@ -2348,7 +2352,10 @@ public sealed class Transition
|
||||||
? "poly=n/a"
|
? "poly=n/a"
|
||||||
: System.FormattableString.Invariant(
|
: System.FormattableString.Invariant(
|
||||||
$"poly=0x{hit.Id:X4} n=({hit.Plane.Normal.X:F3},{hit.Plane.Normal.Y:F3},{hit.Plane.Normal.Z:F3}) d={hit.Plane.D:F3} sides={hit.SidesType}");
|
$"poly=0x{hit.Id:X4} n=({hit.Plane.Normal.X:F3},{hit.Plane.Normal.Y:F3},{hit.Plane.Normal.Z:F3}) d={hit.Plane.D:F3} sides={hit.SidesType}");
|
||||||
var bs = cell.BSP.Root.BoundingSphere;
|
FlatCollisionSphere bs =
|
||||||
|
CollisionTraversal.RootBoundingSphere(
|
||||||
|
engine.DataCache,
|
||||||
|
cell);
|
||||||
string bsDesc = System.FormattableString.Invariant(
|
string bsDesc = System.FormattableString.Invariant(
|
||||||
$"bs=({bs.Origin.X:F3},{bs.Origin.Y:F3},{bs.Origin.Z:F3}) br={bs.Radius:F3}");
|
$"bs=({bs.Origin.X:F3},{bs.Origin.Y:F3},{bs.Origin.Z:F3}) br={bs.Radius:F3}");
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
|
@ -2620,7 +2627,8 @@ public sealed class Transition
|
||||||
if (cellLow >= 0x0100 && engine.DataCache is not null)
|
if (cellLow >= 0x0100 && engine.DataCache is not null)
|
||||||
{
|
{
|
||||||
var cellPhysics = engine.DataCache.GetCellStruct(sp.CheckCellId);
|
var cellPhysics = engine.DataCache.GetCellStruct(sp.CheckCellId);
|
||||||
if (cellPhysics?.BSP?.Root is not null)
|
if (cellPhysics is not null &&
|
||||||
|
CollisionTraversal.HasPhysics(engine.DataCache!, cellPhysics))
|
||||||
{
|
{
|
||||||
// Transform player sphere to cell-local space.
|
// Transform player sphere to cell-local space.
|
||||||
var localCenter = Vector3.Transform(footCenter, cellPhysics.InverseWorldTransform);
|
var localCenter = Vector3.Transform(footCenter, cellPhysics.InverseWorldTransform);
|
||||||
|
|
@ -2677,9 +2685,9 @@ public sealed class Transition
|
||||||
cellOrigin = cellPhysics.WorldTransform.Translation;
|
cellOrigin = cellPhysics.WorldTransform.Translation;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cellState = BSPQuery.FindCollisions(
|
var cellState = CollisionTraversal.FindCollisions(
|
||||||
cellPhysics.BSP.Root,
|
engine.DataCache!,
|
||||||
cellPhysics.Resolved,
|
cellPhysics,
|
||||||
this,
|
this,
|
||||||
localCenter,
|
localCenter,
|
||||||
sphereRadius,
|
sphereRadius,
|
||||||
|
|
@ -3134,12 +3142,14 @@ public sealed class Transition
|
||||||
{
|
{
|
||||||
Vector3 dxy = obj.Position - sp.GlobalCurrCenter[0].Origin;
|
Vector3 dxy = obj.Position - sp.GlobalCurrCenter[0].Origin;
|
||||||
float distXY = MathF.Sqrt(dxy.X * dxy.X + dxy.Y * dxy.Y);
|
float distXY = MathF.Sqrt(dxy.X * dxy.X + dxy.Y * dxy.Y);
|
||||||
bool cacheHit = physics?.BSP?.Root is not null;
|
bool cacheHit = physics is not null &&
|
||||||
|
CollisionTraversal.HasPhysics(engine.DataCache!, physics);
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
$"[bsp-test] obj=0x{obj.EntityId:X8} gfx=0x{obj.GfxObjId:X8} state=0x{obj.State:X8} radius={obj.Radius:F3} pos=({obj.Position.X:F2},{obj.Position.Y:F2},{obj.Position.Z:F2}) distXY={distXY:F3} cacheHit={cacheHit}"));
|
$"[bsp-test] obj=0x{obj.EntityId:X8} gfx=0x{obj.GfxObjId:X8} state=0x{obj.State:X8} radius={obj.Radius:F3} pos=({obj.Position.X:F2},{obj.Position.Y:F2},{obj.Position.Z:F2}) distXY={distXY:F3} cacheHit={cacheHit}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (physics?.BSP?.Root is null)
|
if (physics is null ||
|
||||||
|
!CollisionTraversal.HasPhysics(engine.DataCache!, physics))
|
||||||
{
|
{
|
||||||
// Clear obstruction_ethereal before skipping — retail's per-object
|
// Clear obstruction_ethereal before skipping — retail's per-object
|
||||||
// clear (pc:276989) fires after shape tests; we clear early here to
|
// clear (pc:276989) fires after shape tests; we clear early here to
|
||||||
|
|
@ -3179,9 +3189,9 @@ public sealed class Transition
|
||||||
// Use the retail 6-path dispatcher with pre-resolved polygons.
|
// Use the retail 6-path dispatcher with pre-resolved polygons.
|
||||||
// Pass the object's scale so collision response offsets (in
|
// Pass the object's scale so collision response offsets (in
|
||||||
// unscaled local space) are multiplied back to world space.
|
// unscaled local space) are multiplied back to world space.
|
||||||
result = BSPQuery.FindCollisions(
|
result = CollisionTraversal.FindCollisions(
|
||||||
physics.BSP.Root,
|
engine.DataCache!,
|
||||||
physics.Resolved,
|
physics,
|
||||||
this,
|
this,
|
||||||
localSphere0Center,
|
localSphere0Center,
|
||||||
localSphere0Radius,
|
localSphere0Radius,
|
||||||
|
|
@ -3476,7 +3486,11 @@ public sealed class Transition
|
||||||
// CacheBuilding site resolves 0x02 Setup models to their first part
|
// CacheBuilding site resolves 0x02 Setup models to their first part
|
||||||
// — retail tests part_array->parts[0] only, 0x006b5320).
|
// — retail tests part_array->parts[0] only, 0x006b5320).
|
||||||
var physics = engine.DataCache.GetGfxObj(building.ModelId);
|
var physics = engine.DataCache.GetGfxObj(building.ModelId);
|
||||||
if (physics?.BSP?.Root is null) return TransitionState.OK;
|
if (physics is null ||
|
||||||
|
!CollisionTraversal.HasPhysics(engine.DataCache, physics))
|
||||||
|
{
|
||||||
|
return TransitionState.OK;
|
||||||
|
}
|
||||||
|
|
||||||
var sp = SpherePath;
|
var sp = SpherePath;
|
||||||
var ci = CollisionInfo;
|
var ci = CollisionInfo;
|
||||||
|
|
@ -3509,9 +3523,9 @@ public sealed class Transition
|
||||||
TransitionState result;
|
TransitionState result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result = BSPQuery.FindCollisions(
|
result = CollisionTraversal.FindCollisions(
|
||||||
physics.BSP.Root,
|
engine.DataCache!,
|
||||||
physics.Resolved,
|
physics,
|
||||||
this,
|
this,
|
||||||
localSphere0Center,
|
localSphere0Center,
|
||||||
localSphere0Radius,
|
localSphere0Radius,
|
||||||
|
|
|
||||||
1249
tests/AcDream.Core.Tests/Physics/FlatBspQueryDifferentialTests.cs
Normal file
1249
tests/AcDream.Core.Tests/Physics/FlatBspQueryDifferentialTests.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue