feat(streaming): shadow-publish flat collision assets
Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
f7ff9f4eea
commit
d9446030e6
32 changed files with 2777 additions and 92 deletions
15
AGENTS.md
15
AGENTS.md
|
|
@ -174,11 +174,17 @@ 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,
|
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
|
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.
|
bytes with no mismatch. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
Production remains graph-authoritative until the connected shadow and cutover
|
Production remains graph-authoritative until the Slice I6 cutover.
|
||||||
gates.
|
Slice I5 is complete: immutable near-build collision closures and strict live
|
||||||
|
collision publication install graph and prepared-flat views under canonical
|
||||||
|
ownership. Cancellation, demotion, rehydration, revisit, removal, and reset
|
||||||
|
gates pass. The lifecycle/reconnect and nine-stop routes sampled 14,064 exact
|
||||||
|
queries with zero mismatch, fault, or artifact; graph/flat cell residency
|
||||||
|
matched at every stable checkpoint and teardown converged. Release build and
|
||||||
|
8,396 solution tests / 5 skips pass.
|
||||||
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 I5 from
|
passes. Slice I is active at I6 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:
|
||||||
|
|
@ -187,7 +193,8 @@ the portal-warmup corrections. Evidence:
|
||||||
`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` and
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md` and
|
||||||
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md`.
|
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i5-dual-collision-shadow.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
|
||||||
|
|
|
||||||
15
CLAUDE.md
15
CLAUDE.md
|
|
@ -172,11 +172,17 @@ 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,
|
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
|
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.
|
bytes with no mismatch. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
Production remains graph-authoritative until the connected shadow and cutover
|
Production remains graph-authoritative until the Slice I6 cutover.
|
||||||
gates.
|
Slice I5 is complete: immutable near-build collision closures and strict live
|
||||||
|
collision publication install graph and prepared-flat views under canonical
|
||||||
|
ownership. Cancellation, demotion, rehydration, revisit, removal, and reset
|
||||||
|
gates pass. The lifecycle/reconnect and nine-stop routes sampled 14,064 exact
|
||||||
|
queries with zero mismatch, fault, or artifact; graph/flat cell residency
|
||||||
|
matched at every stable checkpoint and teardown converged. Release build and
|
||||||
|
8,396 solution tests / 5 skips pass.
|
||||||
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 I5 from
|
passes. Slice I is active at I6 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:
|
||||||
|
|
@ -185,7 +191,8 @@ the portal-warmup corrections. Evidence:
|
||||||
`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` and
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md` and
|
||||||
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md`.
|
`docs/research/2026-07-25-slice-i4-flat-bsp-differential.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i5-dual-collision-shadow.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
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,17 @@ EnvCell topology remains independently keyed. Production traversal is still
|
||||||
the parsed graph oracle until Slice I6. Full-catalog evidence:
|
the parsed graph oracle until Slice I6. Full-catalog evidence:
|
||||||
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
||||||
|
|
||||||
|
**Slice I5 dual-publication seam (2026-07-25).** Near-tier
|
||||||
|
`LandblockBuild` payloads now carry one immutable prepared-collision closure
|
||||||
|
outside the DAT lock. `LandblockPhysicsPublisher` installs the parsed oracle
|
||||||
|
and flat view under the same retained receipt; live objects use the strict
|
||||||
|
`LiveCollisionAssetPublisher`. Cell/topology ownership is landblock-scoped and
|
||||||
|
withdrawn on replacement, demotion, removal, and reset. The connected
|
||||||
|
graph-authoritative referee completed 14,064 exact samples with no mismatch or
|
||||||
|
fault. This is still one `DatCollection` and one prepared-package mmap, not a
|
||||||
|
second reader or a WorldBuilder runtime dependency. Evidence:
|
||||||
|
`docs/research/2026-07-25-slice-i5-dual-collision-shadow.md`.
|
||||||
|
|
||||||
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
||||||
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
||||||
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
||||||
|
|
|
||||||
|
|
@ -1661,13 +1661,18 @@ port in any phase — no separate listing here.
|
||||||
> installed-DAT comparisons, 1,200 complete resolver frames, and 10,000 warmed
|
> installed-DAT comparisons, 1,200 complete resolver frames, and 10,000 warmed
|
||||||
> iterations at zero managed bytes with no mismatch. Production remains
|
> iterations at zero managed bytes with no mismatch. Production remains
|
||||||
> graph-authoritative while I5 dual publication and the connected shadow gate
|
> graph-authoritative while I5 dual publication and the connected shadow gate
|
||||||
> are active. Release build and 8,384 solution tests / 5 skips pass.
|
> execute. I5 is now complete: immutable near-build collision closures and the
|
||||||
|
> strict live-object publisher install both representations under canonical
|
||||||
|
> ownership. Lifecycle/reconnect plus the nine-stop route sampled 14,064
|
||||||
|
> queries with exact state/float-bit equality, zero mismatch/fault/artifact,
|
||||||
|
> equal graph/flat cell residency, and graceful teardown. Release build and
|
||||||
|
> 8,396 solution tests / 5 skips pass. I6 production cutover is active.
|
||||||
> 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 I5 from
|
> connected lifecycle/reconnect gate. Slice I is active at I6 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
|
||||||
|
|
@ -1692,11 +1697,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, its deterministic prepared package, and the zero-mismatch/zero-allocation flat traversal referee are shipped; connected dual publication continues in Slice I5. |
|
| 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, deterministic package, zero-allocation traversal, strict dual publication, and the 14,064-sample connected shadow gate are shipped; production flat cutover continues in Slice I6. |
|
||||||
| 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, I3 deterministic collision package, and I4 exact flat traversal referee shipped; I5 dual publication and connected shadow verification are 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, I4 exact flat traversal, and I5 strict dual publication plus connected shadow verification shipped; I6 production cutover is active. |
|
||||||
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,14 @@ randomized static/swept comparisons, 5,000 walkable comparisons, 7,500
|
||||||
installed-DAT comparisons, and 1,200 complete resolver frames have exact
|
installed-DAT comparisons, and 1,200 complete resolver frames have exact
|
||||||
state/float-bit equality; 10,000 warmed flat iterations allocate zero managed
|
state/float-bit equality; 10,000 warmed flat iterations allocate zero managed
|
||||||
bytes. Release build and 8,384 solution tests / 5 skips pass.
|
bytes. Release build and 8,384 solution tests / 5 skips pass.
|
||||||
Slice H is complete: retained UI/frame work, exact bounded light selection,
|
Slice I5 is complete: immutable near-build collision closures and strict live
|
||||||
and pooled/borrowed/direct network I/O pass. Slice I is active at I5 under
|
collision publication install graph and prepared-flat views together. The
|
||||||
|
lifecycle/reconnect and nine-stop connected routes sampled 14,064 exact
|
||||||
|
queries with zero mismatch, fault, or artifact; graph/flat cell residency
|
||||||
|
matched at every stable checkpoint and teardown converged. Release build and
|
||||||
|
8,396 solution tests / 5 skips pass. 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 I6 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,9 @@
|
||||||
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
||||||
|
|
||||||
**Status:** I0-I4 COMPLETE - retail oracle, zero-allocation transition
|
**Status:** I0-I5 COMPLETE - retail oracle, zero-allocation transition
|
||||||
scratch, deterministic immutable collision records, complete prepared
|
scratch, deterministic immutable collision records, complete prepared
|
||||||
package, and exact flat-traversal differential gate fixed; I5 active
|
package, exact flat traversal, dual publication, and connected shadow gate;
|
||||||
|
I6 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
|
||||||
|
|
@ -241,6 +242,17 @@ until I5/I6. Release build and 8,384 solution tests / 5 skips pass. Evidence:
|
||||||
Gate: zero shadow mismatch and no retained collision owner after landblock or
|
Gate: zero shadow mismatch and no retained collision owner after landblock or
|
||||||
session teardown.
|
session teardown.
|
||||||
|
|
||||||
|
**Complete 2026-07-25.** Near-tier builds carry one immutable prepared
|
||||||
|
collision closure; streamed and live objects publish graph and flat assets
|
||||||
|
strictly, with no graph-only fallback. Cancellation, publication, demotion,
|
||||||
|
rehydration, revisit, adjacent-landblock isolation, removal, and session
|
||||||
|
teardown gates pass. The complete Release solution passes 8,396 tests / 5
|
||||||
|
skips. The connected lifecycle/reconnect and nine-stop routes sampled 14,064
|
||||||
|
queries with exact state/float-bit equality, zero mismatch, zero flat fault,
|
||||||
|
zero mismatch artifact, equal graph/flat cell residency at every stable
|
||||||
|
checkpoint, and graceful exits. Evidence:
|
||||||
|
[`../research/2026-07-25-slice-i5-dual-collision-shadow.md`](../research/2026-07-25-slice-i5-dual-collision-shadow.md).
|
||||||
|
|
||||||
### I6 — production cutover and graph removal
|
### I6 — production cutover and graph removal
|
||||||
|
|
||||||
1. Flip canonical `PhysicsDataCache` records and every `BSPQuery` production
|
1. Flip canonical `PhysicsDataCache` records and every `BSPQuery` production
|
||||||
|
|
|
||||||
91
docs/research/2026-07-25-slice-i5-dual-collision-shadow.md
Normal file
91
docs/research/2026-07-25-slice-i5-dual-collision-shadow.md
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Slice I5 — dual collision publication and connected shadow gate
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Slice I5 is complete. Production gameplay remains graph-authoritative for this
|
||||||
|
checkpoint, but every streamed near-tier generation and every live DAT object
|
||||||
|
now publishes the matching prepared flat collision representation. The sampled
|
||||||
|
flat path runs from a complete cloned transition, compares exact state and
|
||||||
|
float bits, and cannot alter the graph result used by gameplay.
|
||||||
|
|
||||||
|
No visual, collision, movement, or retail algorithm changed in this slice.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- `LandblockBuild` carries one immutable `LandblockCollisionBuild` containing
|
||||||
|
sorted source IDs, flat GfxObj/Setup collision, flat CellStruct geometry, and
|
||||||
|
per-EnvCell topology.
|
||||||
|
- `LandblockBuildFactory` reads that closure from the existing
|
||||||
|
`IPreparedCollisionSource` after the serialized DAT transaction. A missing or
|
||||||
|
corrupt prepared record rejects the complete near-tier generation; a
|
||||||
|
graph-only partial publication is forbidden.
|
||||||
|
- `LandblockPhysicsPublisher` commits graph and flat views through one retained
|
||||||
|
publication receipt. Cell and topology ownership is withdrawn on exact
|
||||||
|
replacement, near-to-far demotion, full removal, and session teardown.
|
||||||
|
- `LiveCollisionAssetPublisher` applies the same strict dual-publication rule
|
||||||
|
to CreateObject, appearance, and local-player Setup paths.
|
||||||
|
- `PhysicsDataCache` exposes typed graph/flat counts in canonical lifecycle
|
||||||
|
snapshots. Landblock-scoped cell and topology catalogs retire together.
|
||||||
|
|
||||||
|
## Shadow referee
|
||||||
|
|
||||||
|
`CollisionShadowVerifier` samples the production entry points while leaving the
|
||||||
|
graph result authoritative. The flat query receives one retained, reset-complete
|
||||||
|
clone of `ObjectInfo`, `SpherePath`, and `CollisionInfo`. Comparisons cover:
|
||||||
|
|
||||||
|
- containment availability, point containment, sphere containment;
|
||||||
|
- root bounding spheres and physics availability;
|
||||||
|
- cell and GfxObj moving-collision results;
|
||||||
|
- complete transition state, collision/contact state, sphere path, candidate
|
||||||
|
ordering, walkable vertices, GUID lists, and exact IEEE-754 float bits.
|
||||||
|
|
||||||
|
A mismatch or flat fault writes one deterministic JSON artifact. The disabled
|
||||||
|
production path allocates zero managed bytes over 10,000 warmed queries.
|
||||||
|
|
||||||
|
## Automated evidence
|
||||||
|
|
||||||
|
- Focused Core and App collision/publication suites pass.
|
||||||
|
- Cancellation publishes neither representation.
|
||||||
|
- Pending receipt to accepted publication, demotion, rehydration,
|
||||||
|
same-location replacement, adjacent-landblock isolation, full removal, and
|
||||||
|
session reset preserve exact ownership without duplication.
|
||||||
|
- Release build: zero warnings and zero errors.
|
||||||
|
- Complete solution: 8,396 passed / 5 skipped.
|
||||||
|
|
||||||
|
## Connected evidence
|
||||||
|
|
||||||
|
Lifecycle/reconnect gate:
|
||||||
|
|
||||||
|
- artifact:
|
||||||
|
`logs/connected-world-gate-20260725-162020/report.json`
|
||||||
|
- capped login, Aerlinthe, Rynthid, Facility Hub, Holtburg, Aerlinthe revisit,
|
||||||
|
then a fresh uncapped reconnect process;
|
||||||
|
- 12,730 sampled queries, 12,730 exact matches;
|
||||||
|
- zero mismatches, zero flat faults, zero mismatch artifacts;
|
||||||
|
- graph/flat cell counts equal at every stable checkpoint;
|
||||||
|
- all publication, retirement, upload, and warmup backlogs converged to zero;
|
||||||
|
- both processes disconnected gracefully with exit code 0.
|
||||||
|
|
||||||
|
Nine-stop R6 route:
|
||||||
|
|
||||||
|
- artifact:
|
||||||
|
`logs/connected-r6-soak-20260725-162614.report.json`
|
||||||
|
- Caul, Sawato, Rynthid, Aerlinthe, Sawato revisit, Holtburg, Caul revisit,
|
||||||
|
Sawato plateau, and Caul plateau;
|
||||||
|
- turn, forward/release, jump, combat-toggle, and movement-truth exercises;
|
||||||
|
- 42,719 eligible queries, 1,334 sampled queries, 1,334 exact matches;
|
||||||
|
- zero mismatches, zero flat faults, zero mismatch artifacts;
|
||||||
|
- graph/flat cell counts equal at all nine stable checkpoints;
|
||||||
|
- graceful client shutdown and zero gate failures.
|
||||||
|
|
||||||
|
The run occurred under RDP/lock-screen conditions. It is accepted for
|
||||||
|
deterministic correctness and lifetime evidence only; its absolute frame/GPU
|
||||||
|
measurements are not a physical-display performance gate.
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
Slice I6 flips canonical production traversal to the prepared flat
|
||||||
|
representation in one bisectable commit. The graph remains a temporary
|
||||||
|
automated referee for the initial cutover gate. The exact cutover commit and
|
||||||
|
`git revert <sha>` command must be recorded before correctness/visual
|
||||||
|
acceptance, after which production graph retention can be removed.
|
||||||
|
|
@ -30,6 +30,7 @@ internal sealed record ContentEffectsAudioResult(
|
||||||
MagicCatalog MagicCatalog,
|
MagicCatalog MagicCatalog,
|
||||||
IAnimationLoader AnimationLoader,
|
IAnimationLoader AnimationLoader,
|
||||||
LiveEntityCollisionBuilder CollisionBuilder,
|
LiveEntityCollisionBuilder CollisionBuilder,
|
||||||
|
LiveCollisionAssetPublisher CollisionAssets,
|
||||||
EmitterDescRegistry EmitterRegistry,
|
EmitterDescRegistry EmitterRegistry,
|
||||||
ParticleSystem ParticleSystem,
|
ParticleSystem ParticleSystem,
|
||||||
ParticleHookSink ParticleSink,
|
ParticleHookSink ParticleSink,
|
||||||
|
|
@ -95,6 +96,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
IAnimationLoader animationLoader,
|
IAnimationLoader animationLoader,
|
||||||
bool dumpMotionEnabled);
|
bool dumpMotionEnabled);
|
||||||
|
LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||||
|
PhysicsDataCache physicsData,
|
||||||
|
IPreparedAssetSource preparedAssets);
|
||||||
EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats);
|
EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats);
|
||||||
ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters);
|
ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters);
|
||||||
ParticleHookSink CreateParticleSink(
|
ParticleHookSink CreateParticleSink(
|
||||||
|
|
@ -165,6 +169,15 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
||||||
animationLoader,
|
animationLoader,
|
||||||
dumpMotionEnabled));
|
dumpMotionEnabled));
|
||||||
|
|
||||||
|
public LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||||
|
PhysicsDataCache physicsData,
|
||||||
|
IPreparedAssetSource preparedAssets) =>
|
||||||
|
new(
|
||||||
|
physicsData,
|
||||||
|
preparedAssets as IPreparedCollisionSource
|
||||||
|
?? throw new NotSupportedException(
|
||||||
|
"Production prepared assets must expose collision data."));
|
||||||
|
|
||||||
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) =>
|
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) =>
|
||||||
new(dats);
|
new(dats);
|
||||||
|
|
||||||
|
|
@ -332,6 +345,10 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
||||||
_dependencies.DumpMotionEnabled);
|
_dependencies.DumpMotionEnabled);
|
||||||
_publication.PublishLiveEntityCollisionBuilder(collision);
|
_publication.PublishLiveEntityCollisionBuilder(collision);
|
||||||
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
||||||
|
LiveCollisionAssetPublisher collisionAssets =
|
||||||
|
_factory.CreateLiveCollisionAssetPublisher(
|
||||||
|
_dependencies.PhysicsDataCache,
|
||||||
|
preparedAssets);
|
||||||
|
|
||||||
EmitterDescRegistry emitters = _factory.CreateEmitterRegistry(dats);
|
EmitterDescRegistry emitters = _factory.CreateEmitterRegistry(dats);
|
||||||
_publication.PublishEmitterRegistry(emitters);
|
_publication.PublishEmitterRegistry(emitters);
|
||||||
|
|
@ -403,6 +420,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
||||||
magic,
|
magic,
|
||||||
animations,
|
animations,
|
||||||
collision,
|
collision,
|
||||||
|
collisionAssets,
|
||||||
emitters,
|
emitters,
|
||||||
particles,
|
particles,
|
||||||
particleSink,
|
particleSink,
|
||||||
|
|
|
||||||
|
|
@ -441,6 +441,9 @@ internal sealed class FrameRootCompositionPhase
|
||||||
d.FrameProfiler,
|
d.FrameProfiler,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
foundation.Residency,
|
foundation.Residency,
|
||||||
|
d.PhysicsEngine.DataCache
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"Lifecycle automation requires the canonical physics cache."),
|
||||||
currentRenderSceneOracle,
|
currentRenderSceneOracle,
|
||||||
renderSceneShadowComparison,
|
renderSceneShadowComparison,
|
||||||
renderFrameProduct);
|
renderFrameProduct);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ using AcDream.App.Settings;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.Content;
|
||||||
using AcDream.Core.Combat;
|
using AcDream.Core.Combat;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
using AcDream.Core.Chat;
|
using AcDream.Core.Chat;
|
||||||
|
|
@ -252,11 +253,16 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
|
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
|
||||||
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
|
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
|
||||||
|
|
||||||
|
IPreparedCollisionSource preparedCollisions =
|
||||||
|
content.PreparedAssets as IPreparedCollisionSource ??
|
||||||
|
throw new NotSupportedException(
|
||||||
|
"Production prepared assets must expose the matching " +
|
||||||
|
"prepared-collision catalog.");
|
||||||
var landblockBuildFactory = new LandblockBuildFactory(
|
var landblockBuildFactory = new LandblockBuildFactory(
|
||||||
content.Dats,
|
content.Dats,
|
||||||
|
preparedCollisions,
|
||||||
d.DatLock,
|
d.DatLock,
|
||||||
world.TerrainBuild.HeightTable,
|
world.TerrainBuild.HeightTable,
|
||||||
d.PhysicsDataCache,
|
|
||||||
d.Options.DumpSceneryZ);
|
d.Options.DumpSceneryZ);
|
||||||
var streamerLease = scope.Acquire(
|
var streamerLease = scope.Acquire(
|
||||||
"landblock streamer",
|
"landblock streamer",
|
||||||
|
|
@ -399,7 +405,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.Options,
|
d.Options,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
d.PhysicsDataCache,
|
content.CollisionAssets,
|
||||||
content.AnimationLoader,
|
content.AnimationLoader,
|
||||||
live.EntitySpawnAdapter,
|
live.EntitySpawnAdapter,
|
||||||
world.Foundation.TextureCache,
|
world.Foundation.TextureCache,
|
||||||
|
|
@ -689,7 +695,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
d.MotionBindings,
|
d.MotionBindings,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
d.DatLock,
|
d.DatLock,
|
||||||
d.PhysicsDataCache,
|
content.CollisionAssets,
|
||||||
d.AnimatedEntities,
|
d.AnimatedEntities,
|
||||||
localPlayerAnimation,
|
localPlayerAnimation,
|
||||||
localPlayerShadow,
|
localPlayerShadow,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
namespace AcDream.App.Diagnostics;
|
namespace AcDream.App.Diagnostics;
|
||||||
|
|
||||||
|
|
@ -81,6 +82,14 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
||||||
long DatObjectCacheHits,
|
long DatObjectCacheHits,
|
||||||
long DatObjectCacheMisses,
|
long DatObjectCacheMisses,
|
||||||
long DatObjectCacheEvictions,
|
long DatObjectCacheEvictions,
|
||||||
|
int PhysicsGraphGfxObjs,
|
||||||
|
int PhysicsGraphSetups,
|
||||||
|
int PhysicsGraphCells,
|
||||||
|
int PhysicsFlatGfxObjs,
|
||||||
|
int PhysicsFlatSetups,
|
||||||
|
int PhysicsFlatCells,
|
||||||
|
int PhysicsFlatEnvCells,
|
||||||
|
CollisionShadowStats CollisionShadow,
|
||||||
StreamingWorkDiagnostics StreamingWork,
|
StreamingWorkDiagnostics StreamingWork,
|
||||||
ResidencySnapshot Residency,
|
ResidencySnapshot Residency,
|
||||||
double Fps,
|
double Fps,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Rendering.Residency;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Vfx;
|
using AcDream.Core.Vfx;
|
||||||
using DatReaderWriter.Lib.IO;
|
using DatReaderWriter.Lib.IO;
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
private readonly FrameProfiler _frameProfiler;
|
private readonly FrameProfiler _frameProfiler;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly ResidencyManager _residency;
|
private readonly ResidencyManager _residency;
|
||||||
|
private readonly PhysicsDataCache _physics;
|
||||||
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||||
_renderSceneOracle;
|
_renderSceneOracle;
|
||||||
private readonly IRenderSceneShadowSnapshotSource?
|
private readonly IRenderSceneShadowSnapshotSource?
|
||||||
|
|
@ -63,6 +65,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
FrameProfiler frameProfiler,
|
FrameProfiler frameProfiler,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
ResidencyManager residency,
|
ResidencyManager residency,
|
||||||
|
PhysicsDataCache physics,
|
||||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
||||||
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
||||||
|
|
@ -92,6 +95,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
_residency = residency
|
_residency = residency
|
||||||
?? throw new ArgumentNullException(nameof(residency));
|
?? throw new ArgumentNullException(nameof(residency));
|
||||||
|
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||||
_renderSceneOracle = renderSceneOracle;
|
_renderSceneOracle = renderSceneOracle;
|
||||||
_renderSceneShadow = renderSceneShadow;
|
_renderSceneShadow = renderSceneShadow;
|
||||||
_renderFrameProduct = renderFrameProduct;
|
_renderFrameProduct = renderFrameProduct;
|
||||||
|
|
@ -184,6 +188,14 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||||
|
PhysicsGraphGfxObjs: _physics.GfxObjCount,
|
||||||
|
PhysicsGraphSetups: _physics.SetupCount,
|
||||||
|
PhysicsGraphCells: _physics.CellStructCount,
|
||||||
|
PhysicsFlatGfxObjs: _physics.FlatGfxObjCount,
|
||||||
|
PhysicsFlatSetups: _physics.FlatSetupCount,
|
||||||
|
PhysicsFlatCells: _physics.FlatCellStructCount,
|
||||||
|
PhysicsFlatEnvCells: _physics.FlatEnvCellCount,
|
||||||
|
CollisionShadow: _physics.CollisionShadowStats,
|
||||||
StreamingWork: _streaming.WorkDiagnostics,
|
StreamingWork: _streaming.WorkDiagnostics,
|
||||||
Residency: residency,
|
Residency: residency,
|
||||||
Fps: render.Fps,
|
Fps: render.Fps,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ internal sealed class PlayerModeController :
|
||||||
private readonly ILiveEntityMotionRuntimeBindings _motionBindings;
|
private readonly ILiveEntityMotionRuntimeBindings _motionBindings;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly object _datLock;
|
private readonly object _datLock;
|
||||||
private readonly PhysicsDataCache _physicsDataCache;
|
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
||||||
private readonly LocalPlayerAnimationController _animation;
|
private readonly LocalPlayerAnimationController _animation;
|
||||||
private readonly LocalPlayerShadowSynchronizer _shadow;
|
private readonly LocalPlayerShadowSynchronizer _shadow;
|
||||||
|
|
@ -59,7 +59,7 @@ internal sealed class PlayerModeController :
|
||||||
ILiveEntityMotionRuntimeBindings motionBindings,
|
ILiveEntityMotionRuntimeBindings motionBindings,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
object datLock,
|
object datLock,
|
||||||
PhysicsDataCache physicsDataCache,
|
LiveCollisionAssetPublisher collisionAssets,
|
||||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
||||||
LocalPlayerAnimationController animation,
|
LocalPlayerAnimationController animation,
|
||||||
LocalPlayerShadowSynchronizer shadow,
|
LocalPlayerShadowSynchronizer shadow,
|
||||||
|
|
@ -82,7 +82,8 @@ internal sealed class PlayerModeController :
|
||||||
_motionBindings = motionBindings ?? throw new ArgumentNullException(nameof(motionBindings));
|
_motionBindings = motionBindings ?? throw new ArgumentNullException(nameof(motionBindings));
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||||
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
|
_collisionAssets = collisionAssets ??
|
||||||
|
throw new ArgumentNullException(nameof(collisionAssets));
|
||||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||||
_animation = animation ?? throw new ArgumentNullException(nameof(animation));
|
_animation = animation ?? throw new ArgumentNullException(nameof(animation));
|
||||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||||
|
|
@ -536,7 +537,9 @@ internal sealed class PlayerModeController :
|
||||||
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(
|
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(
|
||||||
playerEntity.SourceGfxObjOrSetupId);
|
playerEntity.SourceGfxObjOrSetupId);
|
||||||
if (setup is not null)
|
if (setup is not null)
|
||||||
_physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, setup);
|
_collisionAssets.CacheSetup(
|
||||||
|
playerEntity.SourceGfxObjOrSetupId,
|
||||||
|
setup);
|
||||||
controller.StepUpHeight = setup is { StepUpHeight: > 0f }
|
controller.StepUpHeight = setup is { StepUpHeight: > 0f }
|
||||||
? setup.StepUpHeight
|
? setup.StepUpHeight
|
||||||
: 0.4f;
|
: 0.4f;
|
||||||
|
|
|
||||||
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes the graph and prepared collision views for DAT objects discovered
|
||||||
|
/// through the live-object path. Streamed statics carry their prepared closure
|
||||||
|
/// on <c>LandblockBuild</c>; live CreateObject/appearance data arrives outside
|
||||||
|
/// that transaction and therefore uses this matching strict publisher.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class LiveCollisionAssetPublisher
|
||||||
|
{
|
||||||
|
private readonly PhysicsDataCache _cache;
|
||||||
|
private readonly IPreparedCollisionSource _source;
|
||||||
|
|
||||||
|
public LiveCollisionAssetPublisher(
|
||||||
|
PhysicsDataCache cache,
|
||||||
|
IPreparedCollisionSource source)
|
||||||
|
{
|
||||||
|
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||||
|
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CacheGfxObj(uint id, GfxObj gfxObj)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(gfxObj);
|
||||||
|
FlatGfxObjCollisionAsset? prepared =
|
||||||
|
_cache.GetFlatGfxObj(id) is not null
|
||||||
|
? null
|
||||||
|
: Require(
|
||||||
|
_source.ReadGfxObjCollision(id),
|
||||||
|
"GfxObj collision",
|
||||||
|
id);
|
||||||
|
_cache.CacheGfxObj(id, gfxObj, prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CacheSetup(uint id, Setup setup)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(setup);
|
||||||
|
FlatSetupCollision? prepared =
|
||||||
|
_cache.GetFlatSetup(id) is not null
|
||||||
|
? null
|
||||||
|
: Require(
|
||||||
|
_source.ReadSetupCollision(id),
|
||||||
|
"Setup collision",
|
||||||
|
id);
|
||||||
|
_cache.CacheSetup(id, setup, prepared);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Require<T>(
|
||||||
|
PreparedCollisionReadResult<T> result,
|
||||||
|
string kind,
|
||||||
|
uint id)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (result.Status == PreparedAssetReadStatus.Loaded &&
|
||||||
|
result.Data is not null)
|
||||||
|
{
|
||||||
|
return result.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{kind} 0x{id:X8} is {result.Status}. " +
|
||||||
|
"Live collision cannot publish only one representation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
private readonly RuntimeOptions _options;
|
private readonly RuntimeOptions _options;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly PhysicsDataCache _physicsData;
|
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||||
private readonly IAnimationLoader _animationLoader;
|
private readonly IAnimationLoader _animationLoader;
|
||||||
private readonly EntitySpawnAdapter _spawnAdapter;
|
private readonly EntitySpawnAdapter _spawnAdapter;
|
||||||
private readonly TextureCache _textures;
|
private readonly TextureCache _textures;
|
||||||
|
|
@ -62,7 +62,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
RuntimeOptions options,
|
RuntimeOptions options,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
LiveEntityRuntime runtime,
|
LiveEntityRuntime runtime,
|
||||||
PhysicsDataCache physicsData,
|
LiveCollisionAssetPublisher collisionAssets,
|
||||||
IAnimationLoader animationLoader,
|
IAnimationLoader animationLoader,
|
||||||
EntitySpawnAdapter spawnAdapter,
|
EntitySpawnAdapter spawnAdapter,
|
||||||
TextureCache textures,
|
TextureCache textures,
|
||||||
|
|
@ -82,7 +82,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||||
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
|
_collisionAssets = collisionAssets ??
|
||||||
|
throw new ArgumentNullException(nameof(collisionAssets));
|
||||||
_animationLoader = animationLoader ?? throw new ArgumentNullException(nameof(animationLoader));
|
_animationLoader = animationLoader ?? throw new ArgumentNullException(nameof(animationLoader));
|
||||||
_spawnAdapter = spawnAdapter ?? throw new ArgumentNullException(nameof(spawnAdapter));
|
_spawnAdapter = spawnAdapter ?? throw new ArgumentNullException(nameof(spawnAdapter));
|
||||||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||||||
|
|
@ -172,7 +173,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
|
|
||||||
Setup? setup = _dats.Get<Setup>(canonicalSpawn.SetupTableId.Value);
|
Setup? setup = _dats.Get<Setup>(canonicalSpawn.SetupTableId.Value);
|
||||||
if (setup is not null)
|
if (setup is not null)
|
||||||
_physicsData.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
_collisionAssets.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
||||||
if (setup is null)
|
if (setup is null)
|
||||||
{
|
{
|
||||||
_missingSetup++;
|
_missingSetup++;
|
||||||
|
|
@ -282,7 +283,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_physicsData.CacheGfxObj(part.GfxObjId, gfx);
|
_collisionAssets.CacheGfxObj(part.GfxObjId, gfx);
|
||||||
if (dumpClothing)
|
if (dumpClothing)
|
||||||
{
|
{
|
||||||
var subMeshes = GfxObjMesh.Build(gfx, _dats);
|
var subMeshes = GfxObjMesh.Build(gfx, _dats);
|
||||||
|
|
@ -430,7 +431,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slotZeroGfx is not null)
|
if (slotZeroGfx is not null)
|
||||||
_physicsData.CacheGfxObj(slotZeroId, slotZeroGfx);
|
_collisionAssets.CacheGfxObj(slotZeroId, slotZeroGfx);
|
||||||
return slotZeroId;
|
return slotZeroId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -512,7 +513,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_physicsData.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
_collisionAssets.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
||||||
|
|
||||||
Dictionary<uint, uint>? resolved = null;
|
Dictionary<uint, uint>? resolved = null;
|
||||||
foreach (var surfaceQid in gfx.Surfaces)
|
foreach (var surfaceQid in gfx.Surfaces)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ namespace AcDream.App.Streaming;
|
||||||
public sealed record LandblockBuild(
|
public sealed record LandblockBuild(
|
||||||
LoadedLandblock Landblock,
|
LoadedLandblock Landblock,
|
||||||
EnvCellLandblockBuild? EnvCells = null,
|
EnvCellLandblockBuild? EnvCells = null,
|
||||||
LandblockBuildOrigin Origin = default)
|
LandblockBuildOrigin Origin = default,
|
||||||
|
LandblockCollisionBuild? Collisions = null)
|
||||||
{
|
{
|
||||||
public uint LandblockId => Landblock.LandblockId;
|
public uint LandblockId => Landblock.LandblockId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
||||||
|
|
@ -17,19 +18,21 @@ namespace AcDream.App.Streaming;
|
||||||
public sealed class LandblockBuildFactory
|
public sealed class LandblockBuildFactory
|
||||||
{
|
{
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
|
private readonly IPreparedCollisionSource _preparedCollisions;
|
||||||
private readonly object _datLock;
|
private readonly object _datLock;
|
||||||
private readonly float[] _heightTable;
|
private readonly float[] _heightTable;
|
||||||
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache;
|
|
||||||
private readonly bool _dumpSceneryZ;
|
private readonly bool _dumpSceneryZ;
|
||||||
|
|
||||||
public LandblockBuildFactory(
|
public LandblockBuildFactory(
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
|
IPreparedCollisionSource preparedCollisions,
|
||||||
object datLock,
|
object datLock,
|
||||||
float[] heightTable,
|
float[] heightTable,
|
||||||
AcDream.Core.Physics.PhysicsDataCache physicsDataCache,
|
|
||||||
bool dumpSceneryZ = false)
|
bool dumpSceneryZ = false)
|
||||||
{
|
{
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
|
_preparedCollisions = preparedCollisions ??
|
||||||
|
throw new ArgumentNullException(nameof(preparedCollisions));
|
||||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||||
ArgumentNullException.ThrowIfNull(heightTable);
|
ArgumentNullException.ThrowIfNull(heightTable);
|
||||||
if (heightTable.Length < 256)
|
if (heightTable.Length < 256)
|
||||||
|
|
@ -37,7 +40,6 @@ public sealed class LandblockBuildFactory
|
||||||
"The retail terrain height table must contain at least 256 entries.",
|
"The retail terrain height table must contain at least 256 entries.",
|
||||||
nameof(heightTable));
|
nameof(heightTable));
|
||||||
_heightTable = (float[])heightTable.Clone();
|
_heightTable = (float[])heightTable.Clone();
|
||||||
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
|
|
||||||
_dumpSceneryZ = dumpSceneryZ;
|
_dumpSceneryZ = dumpSceneryZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +75,7 @@ public sealed class LandblockBuildFactory
|
||||||
// lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
|
// lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
|
||||||
// cost). Identical work in both branches; the probe branch only adds the
|
// cost). Identical work in both branches; the probe branch only adds the
|
||||||
// stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
|
// stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
|
||||||
|
LandblockBuild? build;
|
||||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeTeleportEnabled)
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeTeleportEnabled)
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
@ -80,17 +83,28 @@ public sealed class LandblockBuildFactory
|
||||||
{
|
{
|
||||||
long waitedMs = sw.ElapsedMilliseconds;
|
long waitedMs = sw.ElapsedMilliseconds;
|
||||||
sw.Restart();
|
sw.Restart();
|
||||||
var built = BuildLocked(request);
|
build = BuildLocked(request);
|
||||||
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
|
||||||
"BUILD", request.LandblockId,
|
"BUILD", request.LandblockId,
|
||||||
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={request.Kind}");
|
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={request.Kind}");
|
||||||
return built;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock (_datLock)
|
else
|
||||||
{
|
{
|
||||||
return BuildLocked(request);
|
lock (_datLock)
|
||||||
|
build = BuildLocked(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (build is null ||
|
||||||
|
request.Kind == LandblockStreamJobKind.LoadFar)
|
||||||
|
{
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
|
||||||
|
return build with
|
||||||
|
{
|
||||||
|
Collisions = BuildPreparedCollisionClosure(build.Landblock),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private AcDream.App.Streaming.LandblockBuild? BuildLocked(
|
private AcDream.App.Streaming.LandblockBuild? BuildLocked(
|
||||||
|
|
@ -143,7 +157,6 @@ public sealed class LandblockBuildFactory
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
|
||||||
if (gfx is not null)
|
if (gfx is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheGfxObj(e.SourceGfxObjOrSetupId, gfx);
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||||
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
meshRefs.Add(new AcDream.Core.World.MeshRef(
|
||||||
|
|
@ -156,13 +169,11 @@ public sealed class LandblockBuildFactory
|
||||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
|
||||||
if (setup is not null)
|
if (setup is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheSetup(e.SourceGfxObjOrSetupId, setup);
|
|
||||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||||
foreach (var mr in flat)
|
foreach (var mr in flat)
|
||||||
{
|
{
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||||
if (gfx is null) continue;
|
if (gfx is null) continue;
|
||||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
|
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
|
||||||
meshRefs.Add(mr);
|
meshRefs.Add(mr);
|
||||||
|
|
@ -293,6 +304,103 @@ public sealed class LandblockBuildFactory
|
||||||
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the exact prepared collision closure after the serialized DAT
|
||||||
|
/// transaction completes. The immutable package is independently
|
||||||
|
/// thread-safe, so these reads never extend the shared DAT-reader lock.
|
||||||
|
/// Missing or corrupt prepared data fails the complete near-tier build;
|
||||||
|
/// production must not publish a graph-only partial generation.
|
||||||
|
/// </summary>
|
||||||
|
private AcDream.Core.World.LandblockCollisionBuild
|
||||||
|
BuildPreparedCollisionClosure(
|
||||||
|
AcDream.Core.World.LoadedLandblock landblock)
|
||||||
|
{
|
||||||
|
AcDream.Core.World.PhysicsDatBundle dats =
|
||||||
|
landblock.PhysicsDats ??
|
||||||
|
AcDream.Core.World.PhysicsDatBundle.Empty;
|
||||||
|
ImmutableArray<uint> gfxObjIds = [.. dats.GfxObjs.Keys.Order()];
|
||||||
|
ImmutableArray<uint> setupIds = [.. dats.Setups.Keys.Order()];
|
||||||
|
ImmutableArray<uint> envCellIds = [.. dats.EnvCells.Keys.Order()];
|
||||||
|
var gfxObjs =
|
||||||
|
ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
AcDream.Core.Physics.FlatGfxObjCollisionAsset>();
|
||||||
|
var setups =
|
||||||
|
ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
AcDream.Core.Physics.FlatSetupCollision>();
|
||||||
|
var cellStructures =
|
||||||
|
ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
AcDream.Core.Physics.FlatCellStructureCollisionAsset>();
|
||||||
|
var envCells =
|
||||||
|
ImmutableDictionary.CreateBuilder<
|
||||||
|
uint,
|
||||||
|
AcDream.Core.Physics.FlatEnvCellTopology>();
|
||||||
|
|
||||||
|
foreach (uint id in gfxObjIds)
|
||||||
|
{
|
||||||
|
gfxObjs.Add(
|
||||||
|
id,
|
||||||
|
RequirePrepared(
|
||||||
|
_preparedCollisions.ReadGfxObjCollision(id),
|
||||||
|
"GfxObj collision",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (uint id in setupIds)
|
||||||
|
{
|
||||||
|
setups.Add(
|
||||||
|
id,
|
||||||
|
RequirePrepared(
|
||||||
|
_preparedCollisions.ReadSetupCollision(id),
|
||||||
|
"Setup collision",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (uint id in envCellIds)
|
||||||
|
{
|
||||||
|
cellStructures.Add(
|
||||||
|
id,
|
||||||
|
RequirePrepared(
|
||||||
|
_preparedCollisions.ReadCellStructureCollision(id),
|
||||||
|
"CellStruct collision",
|
||||||
|
id));
|
||||||
|
envCells.Add(
|
||||||
|
id,
|
||||||
|
RequirePrepared(
|
||||||
|
_preparedCollisions.ReadEnvCellTopology(id),
|
||||||
|
"EnvCell topology",
|
||||||
|
id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AcDream.Core.World.LandblockCollisionBuild(
|
||||||
|
gfxObjs.ToImmutable(),
|
||||||
|
setups.ToImmutable(),
|
||||||
|
cellStructures.ToImmutable(),
|
||||||
|
envCells.ToImmutable(),
|
||||||
|
gfxObjIds,
|
||||||
|
setupIds,
|
||||||
|
envCellIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T RequirePrepared<T>(
|
||||||
|
PreparedCollisionReadResult<T> result,
|
||||||
|
string kind,
|
||||||
|
uint id)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
if (result.Status == PreparedAssetReadStatus.Loaded &&
|
||||||
|
result.Data is not null)
|
||||||
|
{
|
||||||
|
return result.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{kind} 0x{id:X8} is {result.Status}. " +
|
||||||
|
"The complete near-tier generation cannot be published.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
|
||||||
/// landblock on the worker thread. Pure CPU — no GL calls.
|
/// landblock on the worker thread. Pure CPU — no GL calls.
|
||||||
|
|
@ -363,7 +471,6 @@ public sealed class LandblockBuildFactory
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
|
||||||
if (gfx is not null)
|
if (gfx is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
|
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
|
||||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||||
|
|
@ -374,13 +481,11 @@ public sealed class LandblockBuildFactory
|
||||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
|
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
|
||||||
if (setup is not null)
|
if (setup is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheSetup(spawn.ObjectId, setup);
|
|
||||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||||
foreach (var mr in flat)
|
foreach (var mr in flat)
|
||||||
{
|
{
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||||
if (gfx is null) continue;
|
if (gfx is null) continue;
|
||||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
|
||||||
// Compose: part's own transform, then the spawn's scale.
|
// Compose: part's own transform, then the spawn's scale.
|
||||||
var partXf = mr.PartTransform * scaleMat;
|
var partXf = mr.PartTransform * scaleMat;
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
|
|
@ -687,7 +792,6 @@ public sealed class LandblockBuildFactory
|
||||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
|
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
|
||||||
if (gfx is not null)
|
if (gfx is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheGfxObj(stab.Id, gfx);
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||||
|
|
@ -702,7 +806,6 @@ public sealed class LandblockBuildFactory
|
||||||
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(stab.Id);
|
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(stab.Id);
|
||||||
if (setup is not null)
|
if (setup is not null)
|
||||||
{
|
{
|
||||||
_physicsDataCache.CacheSetup(stab.Id, setup);
|
|
||||||
stabLightCount = setup.Lights.Count;
|
stabLightCount = setup.Lights.Count;
|
||||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||||
if (dumpStab)
|
if (dumpStab)
|
||||||
|
|
@ -727,7 +830,6 @@ public sealed class LandblockBuildFactory
|
||||||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
|
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
|
||||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||||
if (pb is not null) interiorBounds.Add(mr.PartTransform, pb.Value);
|
if (pb is not null) interiorBounds.Add(mr.PartTransform, pb.Value);
|
||||||
meshRefs.Add(mr);
|
meshRefs.Add(mr);
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ public sealed class LandblockPhysicsPublication
|
||||||
internal int BuildingCursor { get; set; }
|
internal int BuildingCursor { get; set; }
|
||||||
internal bool BaseCommitted { get; set; }
|
internal bool BaseCommitted { get; set; }
|
||||||
internal int GfxCursor { get; set; }
|
internal int GfxCursor { get; set; }
|
||||||
|
internal uint[] SetupObjectIds { get; set; } = Array.Empty<uint>();
|
||||||
|
internal int SetupCursor { get; set; }
|
||||||
internal int PriorStaticCursor { get; set; }
|
internal int PriorStaticCursor { get; set; }
|
||||||
internal int StaticCursor { get; set; }
|
internal int StaticCursor { get; set; }
|
||||||
internal int BspOwnerCount { get; set; }
|
internal int BspOwnerCount { get; set; }
|
||||||
|
|
@ -199,7 +201,7 @@ public sealed class LandblockPhysicsPublisher
|
||||||
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||||
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||||
?? Array.Empty<BuildingInfo>();
|
?? Array.Empty<BuildingInfo>();
|
||||||
return new LandblockPhysicsPublication(
|
var publication = new LandblockPhysicsPublication(
|
||||||
_receiptOwner,
|
_receiptOwner,
|
||||||
build,
|
build,
|
||||||
origin,
|
origin,
|
||||||
|
|
@ -207,6 +209,10 @@ public sealed class LandblockPhysicsPublisher
|
||||||
buildings,
|
buildings,
|
||||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||||
build.Landblock.LandblockId));
|
build.Landblock.LandblockId));
|
||||||
|
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||||
|
? [.. collisions.SetupIds]
|
||||||
|
: datBundle.Setups.Keys.Order().ToArray();
|
||||||
|
return publication;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -223,6 +229,14 @@ public sealed class LandblockPhysicsPublisher
|
||||||
|
|
||||||
IReadOnlyList<WorldEntity> entities =
|
IReadOnlyList<WorldEntity> entities =
|
||||||
publication.Build.Landblock.Entities;
|
publication.Build.Landblock.Entities;
|
||||||
|
if (publication.Build.Collisions is { } collisions)
|
||||||
|
{
|
||||||
|
publication.GfxObjectIds = [.. collisions.GfxObjIds];
|
||||||
|
publication.PreparationCursor = entities.Count;
|
||||||
|
publication.PreparationCommitted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (publication.PreparationCursor < entities.Count)
|
if (publication.PreparationCursor < entities.Count)
|
||||||
{
|
{
|
||||||
WorldEntity entity = entities[publication.PreparationCursor];
|
WorldEntity entity = entities[publication.PreparationCursor];
|
||||||
|
|
@ -389,10 +403,33 @@ public sealed class LandblockPhysicsPublisher
|
||||||
long cacheStarted = Stopwatch.GetTimestamp();
|
long cacheStarted = Stopwatch.GetTimestamp();
|
||||||
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||||
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||||
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
|
{
|
||||||
|
FlatGfxObjCollisionAsset? prepared = null;
|
||||||
|
publication.Build.Collisions?.GfxObjs.TryGetValue(
|
||||||
|
gfxObjectId,
|
||||||
|
out prepared);
|
||||||
|
_physicsDataCache.CacheGfxObj(
|
||||||
|
gfxObjectId,
|
||||||
|
gfx,
|
||||||
|
prepared);
|
||||||
|
}
|
||||||
publication.GfxCursor++;
|
publication.GfxCursor++;
|
||||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||||
}
|
}
|
||||||
|
else if (publication.SetupCursor < publication.SetupObjectIds.Length)
|
||||||
|
{
|
||||||
|
uint setupId =
|
||||||
|
publication.SetupObjectIds[publication.SetupCursor];
|
||||||
|
if (datBundle.Setups.TryGetValue(setupId, out var setup))
|
||||||
|
{
|
||||||
|
FlatSetupCollision? prepared = null;
|
||||||
|
publication.Build.Collisions?.Setups.TryGetValue(
|
||||||
|
setupId,
|
||||||
|
out prepared);
|
||||||
|
_physicsDataCache.CacheSetup(setupId, setup, prepared);
|
||||||
|
}
|
||||||
|
publication.SetupCursor++;
|
||||||
|
}
|
||||||
else if (publication.PriorStaticCursor
|
else if (publication.PriorStaticCursor
|
||||||
< publication.PriorStaticOwnerIds.Length)
|
< publication.PriorStaticOwnerIds.Length)
|
||||||
{
|
{
|
||||||
|
|
@ -486,11 +523,21 @@ public sealed class LandblockPhysicsPublisher
|
||||||
Matrix4x4.CreateFromQuaternion(rotation)
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||||
|
|
||||||
|
FlatCellStructureCollisionAsset? preparedStructure = null;
|
||||||
|
FlatEnvCellTopology? preparedTopology = null;
|
||||||
|
publication.Build.Collisions?.CellStructures.TryGetValue(
|
||||||
|
envCellId,
|
||||||
|
out preparedStructure);
|
||||||
|
publication.Build.Collisions?.EnvCells.TryGetValue(
|
||||||
|
envCellId,
|
||||||
|
out preparedTopology);
|
||||||
_physicsDataCache.CacheCellStruct(
|
_physicsDataCache.CacheCellStruct(
|
||||||
envCellId,
|
envCellId,
|
||||||
envCell,
|
envCell,
|
||||||
cellStruct,
|
cellStruct,
|
||||||
physicsCellTransform);
|
physicsCellTransform,
|
||||||
|
preparedStructure,
|
||||||
|
preparedTopology);
|
||||||
|
|
||||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||||
cellStruct.VertexArray.Vertices.Count);
|
cellStruct.VertexArray.Vertices.Count);
|
||||||
|
|
|
||||||
555
src/AcDream.Core/Physics/CollisionShadowVerifier.cs
Normal file
555
src/AcDream.Core/Physics/CollisionShadowVerifier.cs
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
|
public readonly record struct CollisionShadowStats(
|
||||||
|
long Queries,
|
||||||
|
long Samples,
|
||||||
|
long Matches,
|
||||||
|
long Mismatches,
|
||||||
|
long Faults);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slice I5 sampled graph/flat referee. The graph result always remains
|
||||||
|
/// authoritative. One retained clone receives the flat query first; the
|
||||||
|
/// canonical transition is then evaluated by the graph path and compared
|
||||||
|
/// field-for-field with exact float bits.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CollisionShadowVerifier
|
||||||
|
{
|
||||||
|
private readonly int _sampleEvery;
|
||||||
|
private readonly string _artifactDirectory;
|
||||||
|
private readonly Transition _shadowTransition = new();
|
||||||
|
private long _queries;
|
||||||
|
private long _samples;
|
||||||
|
private long _matches;
|
||||||
|
private long _mismatches;
|
||||||
|
private long _faults;
|
||||||
|
private bool _flatPass;
|
||||||
|
|
||||||
|
public CollisionShadowVerifier(
|
||||||
|
int sampleEvery,
|
||||||
|
string artifactDirectory)
|
||||||
|
{
|
||||||
|
if (sampleEvery <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sampleEvery));
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(artifactDirectory);
|
||||||
|
_sampleEvery = sampleEvery;
|
||||||
|
_artifactDirectory = Path.GetFullPath(artifactDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool IsFlatPass => _flatPass;
|
||||||
|
|
||||||
|
internal CollisionShadowStats Stats => new(
|
||||||
|
_queries,
|
||||||
|
_samples,
|
||||||
|
_matches,
|
||||||
|
_mismatches,
|
||||||
|
_faults);
|
||||||
|
|
||||||
|
internal bool TrySample(out long sample)
|
||||||
|
{
|
||||||
|
if (_flatPass)
|
||||||
|
{
|
||||||
|
sample = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long query = ++_queries;
|
||||||
|
if (query % _sampleEvery != 0)
|
||||||
|
{
|
||||||
|
sample = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sample = ++_samples;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Transition PrepareShadow(Transition source)
|
||||||
|
{
|
||||||
|
_shadowTransition.CopyFrom(source);
|
||||||
|
return _shadowTransition;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void BeginFlatPass()
|
||||||
|
{
|
||||||
|
if (_flatPass)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Collision shadow flat passes cannot overlap.");
|
||||||
|
_flatPass = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void EndFlatPass()
|
||||||
|
{
|
||||||
|
if (!_flatPass)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"No collision shadow flat pass is active.");
|
||||||
|
_flatPass = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RecordBoolean(
|
||||||
|
long sample,
|
||||||
|
string kind,
|
||||||
|
uint sourceId,
|
||||||
|
bool graph,
|
||||||
|
bool flat,
|
||||||
|
string input)
|
||||||
|
{
|
||||||
|
if (graph == flat)
|
||||||
|
{
|
||||||
|
_matches++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordMismatch(
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
sourceId,
|
||||||
|
"Result",
|
||||||
|
graph.ToString(),
|
||||||
|
flat.ToString(),
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RecordSphere(
|
||||||
|
long sample,
|
||||||
|
string kind,
|
||||||
|
uint sourceId,
|
||||||
|
FlatCollisionSphere graph,
|
||||||
|
FlatCollisionSphere flat)
|
||||||
|
{
|
||||||
|
if (Exact(graph.Origin, flat.Origin) &&
|
||||||
|
Exact(graph.Radius, flat.Radius))
|
||||||
|
{
|
||||||
|
_matches++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordMismatch(
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
sourceId,
|
||||||
|
"RootBoundingSphere",
|
||||||
|
Format(graph),
|
||||||
|
Format(flat),
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RecordTransition(
|
||||||
|
long sample,
|
||||||
|
string kind,
|
||||||
|
uint sourceId,
|
||||||
|
TransitionState graphState,
|
||||||
|
TransitionState flatState,
|
||||||
|
Transition graph,
|
||||||
|
Transition flat,
|
||||||
|
string input)
|
||||||
|
{
|
||||||
|
if (graphState != flatState)
|
||||||
|
{
|
||||||
|
RecordMismatch(
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
sourceId,
|
||||||
|
"TransitionState",
|
||||||
|
graphState.ToString(),
|
||||||
|
flatState.ToString(),
|
||||||
|
input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TransitionExactComparer.TryFindDifference(
|
||||||
|
graph,
|
||||||
|
flat,
|
||||||
|
out string path,
|
||||||
|
out string graphValue,
|
||||||
|
out string flatValue))
|
||||||
|
{
|
||||||
|
RecordMismatch(
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
sourceId,
|
||||||
|
path,
|
||||||
|
graphValue,
|
||||||
|
flatValue,
|
||||||
|
input);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_matches++;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void RecordFault(
|
||||||
|
long sample,
|
||||||
|
string kind,
|
||||||
|
uint sourceId,
|
||||||
|
Exception fault,
|
||||||
|
string input)
|
||||||
|
{
|
||||||
|
_faults++;
|
||||||
|
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||||
|
1,
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
$"0x{sourceId:X8}",
|
||||||
|
"FlatFault",
|
||||||
|
"graph-authoritative",
|
||||||
|
$"{fault.GetType().FullName}: {fault.Message}",
|
||||||
|
input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordMismatch(
|
||||||
|
long sample,
|
||||||
|
string kind,
|
||||||
|
uint sourceId,
|
||||||
|
string difference,
|
||||||
|
string graph,
|
||||||
|
string flat,
|
||||||
|
string input)
|
||||||
|
{
|
||||||
|
_mismatches++;
|
||||||
|
WriteArtifact(new CollisionShadowMismatchArtifact(
|
||||||
|
1,
|
||||||
|
sample,
|
||||||
|
kind,
|
||||||
|
$"0x{sourceId:X8}",
|
||||||
|
difference,
|
||||||
|
graph,
|
||||||
|
flat,
|
||||||
|
input));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteArtifact(CollisionShadowMismatchArtifact artifact)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_artifactDirectory);
|
||||||
|
string path = Path.Combine(
|
||||||
|
_artifactDirectory,
|
||||||
|
FormattableString.Invariant(
|
||||||
|
$"collision-shadow-{artifact.Sample:D8}.json"));
|
||||||
|
string json = JsonSerializer.Serialize(
|
||||||
|
artifact,
|
||||||
|
CollisionShadowJsonContext.Default
|
||||||
|
.CollisionShadowMismatchArtifact);
|
||||||
|
File.WriteAllText(path, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string FormatInput(
|
||||||
|
Vector3 center,
|
||||||
|
float radius) =>
|
||||||
|
$"center={Format(center)};radius={Bits(radius)}";
|
||||||
|
|
||||||
|
internal static string FormatInput(
|
||||||
|
Vector3 center,
|
||||||
|
float radius,
|
||||||
|
bool hasSphere1,
|
||||||
|
Vector3 sphere1Center,
|
||||||
|
float sphere1Radius,
|
||||||
|
Vector3 currentCenter,
|
||||||
|
Vector3 localSpaceZ,
|
||||||
|
float scale,
|
||||||
|
Quaternion localToWorld,
|
||||||
|
Vector3 worldOrigin) =>
|
||||||
|
$"sphere0={Format(center)}/{Bits(radius)};" +
|
||||||
|
$"sphere1={(hasSphere1 ? Format(sphere1Center) : "none")}/" +
|
||||||
|
$"{Bits(sphere1Radius)};current={Format(currentCenter)};" +
|
||||||
|
$"localZ={Format(localSpaceZ)};scale={Bits(scale)};" +
|
||||||
|
$"rotation={Format(localToWorld)};origin={Format(worldOrigin)}";
|
||||||
|
|
||||||
|
private static string Format(FlatCollisionSphere sphere) =>
|
||||||
|
$"{Format(sphere.Origin)}/{Bits(sphere.Radius)}";
|
||||||
|
|
||||||
|
private static string Format(Vector3 value) =>
|
||||||
|
$"{Bits(value.X)},{Bits(value.Y)},{Bits(value.Z)}";
|
||||||
|
|
||||||
|
private static string Format(Quaternion value) =>
|
||||||
|
$"{Bits(value.X)},{Bits(value.Y)},{Bits(value.Z)},{Bits(value.W)}";
|
||||||
|
|
||||||
|
private static string Bits(float value) =>
|
||||||
|
$"0x{BitConverter.SingleToInt32Bits(value):X8}";
|
||||||
|
|
||||||
|
private static bool Exact(float left, float right) =>
|
||||||
|
BitConverter.SingleToInt32Bits(left) ==
|
||||||
|
BitConverter.SingleToInt32Bits(right);
|
||||||
|
|
||||||
|
private static bool Exact(Vector3 left, Vector3 right) =>
|
||||||
|
Exact(left.X, right.X) &&
|
||||||
|
Exact(left.Y, right.Y) &&
|
||||||
|
Exact(left.Z, right.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record CollisionShadowMismatchArtifact(
|
||||||
|
int SchemaVersion,
|
||||||
|
long Sample,
|
||||||
|
string Kind,
|
||||||
|
string SourceId,
|
||||||
|
string Difference,
|
||||||
|
string Graph,
|
||||||
|
string Flat,
|
||||||
|
string Input);
|
||||||
|
|
||||||
|
[System.Text.Json.Serialization.JsonSerializable(
|
||||||
|
typeof(CollisionShadowMismatchArtifact))]
|
||||||
|
internal sealed partial class CollisionShadowJsonContext :
|
||||||
|
System.Text.Json.Serialization.JsonSerializerContext;
|
||||||
|
|
||||||
|
internal static class TransitionExactComparer
|
||||||
|
{
|
||||||
|
internal static bool TryFindDifference(
|
||||||
|
Transition graph,
|
||||||
|
Transition flat,
|
||||||
|
out string path,
|
||||||
|
out string graphValue,
|
||||||
|
out string flatValue)
|
||||||
|
{
|
||||||
|
if (CompareObjectInfo(
|
||||||
|
graph.ObjectInfo,
|
||||||
|
flat.ObjectInfo,
|
||||||
|
out path,
|
||||||
|
out graphValue,
|
||||||
|
out flatValue))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CompareSpherePath(
|
||||||
|
graph.SpherePath,
|
||||||
|
flat.SpherePath,
|
||||||
|
out path,
|
||||||
|
out graphValue,
|
||||||
|
out flatValue))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompareCollisionInfo(
|
||||||
|
graph.CollisionInfo,
|
||||||
|
flat.CollisionInfo,
|
||||||
|
out path,
|
||||||
|
out graphValue,
|
||||||
|
out flatValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompareObjectInfo(
|
||||||
|
ObjectInfo a,
|
||||||
|
ObjectInfo b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (Different("ObjectInfo.State", a.State, b.State, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.StepUpHeight", a.StepUpHeight, b.StepUpHeight, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.StepDownHeight", a.StepDownHeight, b.StepDownHeight, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.Ethereal", a.Ethereal, b.Ethereal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.StepDown", a.StepDown, b.StepDown, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.Scale", a.Scale, b.Scale, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.MoverPhysicsState", a.MoverPhysicsState, b.MoverPhysicsState, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.TargetId", a.TargetId, b.TargetId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.SelfEntityId", a.SelfEntityId, b.SelfEntityId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.MoverHasGravity", a.MoverHasGravity, b.MoverHasGravity, out path, out av, out bv)) return true;
|
||||||
|
if (Different("ObjectInfo.VelocityKilled", a.VelocityKilled, b.VelocityKilled, out path, out av, out bv)) return true;
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompareCollisionInfo(
|
||||||
|
CollisionInfo a,
|
||||||
|
CollisionInfo b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (Different("CollisionInfo.ContactPlaneValid", a.ContactPlaneValid, b.ContactPlaneValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.ContactPlane", a.ContactPlane, b.ContactPlane, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.ContactPlaneCellId", a.ContactPlaneCellId, b.ContactPlaneCellId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.ContactPlaneIsWater", a.ContactPlaneIsWater, b.ContactPlaneIsWater, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.LastKnownContactPlaneValid", a.LastKnownContactPlaneValid, b.LastKnownContactPlaneValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.LastKnownContactPlane", a.LastKnownContactPlane, b.LastKnownContactPlane, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.LastKnownContactPlaneCellId", a.LastKnownContactPlaneCellId, b.LastKnownContactPlaneCellId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.LastKnownContactPlaneIsWater", a.LastKnownContactPlaneIsWater, b.LastKnownContactPlaneIsWater, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.SlidingNormalValid", a.SlidingNormalValid, b.SlidingNormalValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.SlidingNormal", a.SlidingNormal, b.SlidingNormal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.CollisionNormalValid", a.CollisionNormalValid, b.CollisionNormalValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.CollisionNormal", a.CollisionNormal, b.CollisionNormal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.CollidedWithEnvironment", a.CollidedWithEnvironment, b.CollidedWithEnvironment, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.FramesStationaryFall", a.FramesStationaryFall, b.FramesStationaryFall, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.AdjustOffset", a.AdjustOffset, b.AdjustOffset, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.LastCollidedObjectGuid", a.LastCollidedObjectGuid, b.LastCollidedObjectGuid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("CollisionInfo.ContactPlaneWriteCount", a.ContactPlaneWriteCount, b.ContactPlaneWriteCount, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentList("CollisionInfo.CollideObjectGuids", a.CollideObjectGuids, b.CollideObjectGuids, out path, out av, out bv)) return true;
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompareSpherePath(
|
||||||
|
SpherePath a,
|
||||||
|
SpherePath b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (Different("SpherePath.NumSphere", a.NumSphere, b.NumSphere, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentSpheres("SpherePath.LocalSphere", a.LocalSphere, b.LocalSphere, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentSpheres("SpherePath.GlobalSphere", a.GlobalSphere, b.GlobalSphere, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentSpheres("SpherePath.GlobalCurrCenter", a.GlobalCurrCenter, b.GlobalCurrCenter, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.BeginPos", a.BeginPos, b.BeginPos, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.EndPos", a.EndPos, b.EndPos, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CurPos", a.CurPos, b.CurPos, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CheckPos", a.CheckPos, b.CheckPos, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.BeginOrientation", a.BeginOrientation, b.BeginOrientation, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.EndOrientation", a.EndOrientation, b.EndOrientation, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CurOrientation", a.CurOrientation, b.CurOrientation, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CheckOrientation", a.CheckOrientation, b.CheckOrientation, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CurCellId", a.CurCellId, b.CurCellId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CheckCellId", a.CheckCellId, b.CheckCellId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CarriedBlockOrigin", a.CarriedBlockOrigin, b.CarriedBlockOrigin, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.GlobalOffset", a.GlobalOffset, b.GlobalOffset, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.StepUp", a.StepUp, b.StepUp, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.StepUpNormal", a.StepUpNormal, b.StepUpNormal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.Collide", a.Collide, b.Collide, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.StepDown", a.StepDown, b.StepDown, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.StepDownAmt", a.StepDownAmt, b.StepDownAmt, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.WalkInterp", a.WalkInterp, b.WalkInterp, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.WalkableValid", a.WalkableValid, b.WalkableValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.WalkablePlane", a.WalkablePlane, b.WalkablePlane, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentVectors("SpherePath.WalkableVertices", a.WalkableVertices, b.WalkableVertices, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.WalkableUp", a.WalkableUp, b.WalkableUp, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.WalkableAllowance", a.WalkableAllowance, b.WalkableAllowance, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.LastWalkableValid", a.LastWalkableValid, b.LastWalkableValid, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.LastWalkablePlane", a.LastWalkablePlane, b.LastWalkablePlane, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentVectors("SpherePath.LastWalkableVertices", a.LastWalkableVertices, b.LastWalkableVertices, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.LastWalkableUp", a.LastWalkableUp, b.LastWalkableUp, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.BackupCheckPos", a.BackupCheckPos, b.BackupCheckPos, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.BackupCheckCellId", a.BackupCheckCellId, b.BackupCheckCellId, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.NegPolyHit", a.NegPolyHit, b.NegPolyHit, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.NegStepUp", a.NegStepUp, b.NegStepUp, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.NegCollisionNormal", a.NegCollisionNormal, b.NegCollisionNormal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.CheckWalkable", a.CheckWalkable, b.CheckWalkable, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.InsertType", a.InsertType, b.InsertType, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.PlacementAllowsSliding", a.PlacementAllowsSliding, b.PlacementAllowsSliding, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.ObstructionEthereal", a.ObstructionEthereal, b.ObstructionEthereal, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.BldgCheck", a.BldgCheck, b.BldgCheck, out path, out av, out bv)) return true;
|
||||||
|
if (Different("SpherePath.HitsInteriorCell", a.HitsInteriorCell, b.HitsInteriorCell, out path, out av, out bv)) return true;
|
||||||
|
if (DifferentList("SpherePath.CellCandidates", a.CellCandidates.OrderedIds, b.CellCandidates.OrderedIds, out path, out av, out bv)) return true;
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DifferentSpheres(
|
||||||
|
string pathPrefix,
|
||||||
|
Sphere[] a,
|
||||||
|
Sphere[] b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < a.Length; i++)
|
||||||
|
{
|
||||||
|
if (Different($"{pathPrefix}[{i}].Origin", a[i].Origin, b[i].Origin, out path, out av, out bv)) return true;
|
||||||
|
if (Different($"{pathPrefix}[{i}].Radius", a[i].Radius, b[i].Radius, out path, out av, out bv)) return true;
|
||||||
|
}
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DifferentVectors(
|
||||||
|
string pathPrefix,
|
||||||
|
Vector3[]? a,
|
||||||
|
Vector3[]? b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (a is null || b is null)
|
||||||
|
{
|
||||||
|
if (a is null && b is null)
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
return Difference(pathPrefix, a is null ? "null" : "array", b is null ? "null" : "array", out path, out av, out bv);
|
||||||
|
}
|
||||||
|
if (a.Length != b.Length)
|
||||||
|
return Difference(pathPrefix + ".Length", a.Length.ToString(CultureInfo.InvariantCulture), b.Length.ToString(CultureInfo.InvariantCulture), out path, out av, out bv);
|
||||||
|
for (int i = 0; i < a.Length; i++)
|
||||||
|
if (Different($"{pathPrefix}[{i}]", a[i], b[i], out path, out av, out bv)) return true;
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DifferentList<T>(
|
||||||
|
string pathPrefix,
|
||||||
|
IReadOnlyList<T> a,
|
||||||
|
IReadOnlyList<T> b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (a.Count != b.Count)
|
||||||
|
return Difference(pathPrefix + ".Count", a.Count.ToString(CultureInfo.InvariantCulture), b.Count.ToString(CultureInfo.InvariantCulture), out path, out av, out bv);
|
||||||
|
for (int i = 0; i < a.Count; i++)
|
||||||
|
if (!EqualityComparer<T>.Default.Equals(a[i], b[i]))
|
||||||
|
return Difference($"{pathPrefix}[{i}]", Format(a[i]), Format(b[i]), out path, out av, out bv);
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Different<T>(
|
||||||
|
string candidatePath,
|
||||||
|
T a,
|
||||||
|
T b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
if (Exact(a, b))
|
||||||
|
return Equal(out path, out av, out bv);
|
||||||
|
return Difference(candidatePath, Format(a), Format(b), out path, out av, out bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Exact<T>(T a, T b)
|
||||||
|
{
|
||||||
|
if (a is float af && b is float bf)
|
||||||
|
return BitConverter.SingleToInt32Bits(af) == BitConverter.SingleToInt32Bits(bf);
|
||||||
|
if (a is Vector3 av && b is Vector3 bv)
|
||||||
|
return Exact(av.X, bv.X) && Exact(av.Y, bv.Y) && Exact(av.Z, bv.Z);
|
||||||
|
if (a is Quaternion aq && b is Quaternion bq)
|
||||||
|
return Exact(aq.X, bq.X) && Exact(aq.Y, bq.Y) &&
|
||||||
|
Exact(aq.Z, bq.Z) && Exact(aq.W, bq.W);
|
||||||
|
if (a is Plane ap && b is Plane bp)
|
||||||
|
return Exact(ap.Normal, bp.Normal) && Exact(ap.D, bp.D);
|
||||||
|
return EqualityComparer<T>.Default.Equals(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Format<T>(T value) => value switch
|
||||||
|
{
|
||||||
|
null => "null",
|
||||||
|
float f => $"0x{BitConverter.SingleToInt32Bits(f):X8}",
|
||||||
|
Vector3 v => $"{Format(v.X)},{Format(v.Y)},{Format(v.Z)}",
|
||||||
|
Quaternion q => $"{Format(q.X)},{Format(q.Y)},{Format(q.Z)},{Format(q.W)}",
|
||||||
|
Plane p => $"{Format(p.Normal)}/{Format(p.D)}",
|
||||||
|
IFormattable formattable => formattable.ToString(
|
||||||
|
null,
|
||||||
|
CultureInfo.InvariantCulture),
|
||||||
|
_ => value.ToString() ?? string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static bool Difference(
|
||||||
|
string candidatePath,
|
||||||
|
string a,
|
||||||
|
string b,
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
path = candidatePath;
|
||||||
|
av = a;
|
||||||
|
bv = b;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Equal(
|
||||||
|
out string path,
|
||||||
|
out string av,
|
||||||
|
out string bv)
|
||||||
|
{
|
||||||
|
path = string.Empty;
|
||||||
|
av = string.Empty;
|
||||||
|
bv = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,49 +23,172 @@ internal static class CollisionTraversal
|
||||||
PhysicsDataCache cache,
|
PhysicsDataCache cache,
|
||||||
CellPhysics cell)
|
CellPhysics cell)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
return flat.RootIndex >= 0;
|
return flat.RootIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cell.CellBSP?.Root is not null;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
return cell.CellBSP?.Root is not null;
|
||||||
|
|
||||||
|
bool flatResult = false;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatResult = (cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment")).RootIndex >= 0;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphResult = cell.CellBSP?.Root is not null;
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordBoolean(
|
||||||
|
sample,
|
||||||
|
"HasCellContainment",
|
||||||
|
cell.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"HasCellContainment",
|
||||||
|
cell.SourceId,
|
||||||
|
flatFault,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool HasPhysics(
|
internal static bool HasPhysics(
|
||||||
PhysicsDataCache cache,
|
PhysicsDataCache cache,
|
||||||
CellPhysics cell)
|
CellPhysics cell)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
return flat.RootIndex >= 0;
|
return flat.RootIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cell.BSP?.Root is not null;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
return cell.BSP?.Root is not null;
|
||||||
|
|
||||||
|
bool flatResult = false;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatResult = (cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics")).RootIndex >= 0;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphResult = cell.BSP?.Root is not null;
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordBoolean(
|
||||||
|
sample,
|
||||||
|
"HasCellPhysics",
|
||||||
|
cell.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"HasCellPhysics",
|
||||||
|
cell.SourceId,
|
||||||
|
flatFault,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool HasPhysics(
|
internal static bool HasPhysics(
|
||||||
PhysicsDataCache cache,
|
PhysicsDataCache cache,
|
||||||
GfxObjPhysics gfxObject)
|
GfxObjPhysics gfxObject)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("GfxObj physics");
|
throw MissingFlat("GfxObj physics");
|
||||||
return flat.RootIndex >= 0;
|
return flat.RootIndex >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return gfxObject.BSP.Root is not null;
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
return gfxObject.BSP.Root is not null;
|
||||||
|
|
||||||
|
bool flatResult = false;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatResult = (gfxObject.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("GfxObj physics")).RootIndex >= 0;
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool graphResult = gfxObject.BSP.Root is not null;
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordBoolean(
|
||||||
|
sample,
|
||||||
|
"HasGfxObjPhysics",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"HasGfxObjPhysics",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
flatFault,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static FlatCollisionSphere RootBoundingSphere(
|
internal static FlatCollisionSphere RootBoundingSphere(
|
||||||
PhysicsDataCache cache,
|
PhysicsDataCache cache,
|
||||||
CellPhysics cell)
|
CellPhysics cell)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
|
|
@ -73,7 +196,39 @@ internal static class CollisionTraversal
|
||||||
}
|
}
|
||||||
|
|
||||||
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
DatReaderWriter.Types.Sphere sphere = cell.BSP!.Root!.BoundingSphere;
|
||||||
return new FlatCollisionSphere(sphere.Origin, sphere.Radius);
|
var graphResult =
|
||||||
|
new FlatCollisionSphere(sphere.Origin, sphere.Radius);
|
||||||
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
return graphResult;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics");
|
||||||
|
FlatCollisionSphere flatResult =
|
||||||
|
flat.Nodes[flat.RootIndex].BoundingSphere;
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
shadow.RecordSphere(
|
||||||
|
sample,
|
||||||
|
"RootBoundingSphere",
|
||||||
|
cell.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
if (shadow.IsFlatPass)
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"RootBoundingSphere",
|
||||||
|
cell.SourceId,
|
||||||
|
fault,
|
||||||
|
string.Empty);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool PointInsideCell(
|
internal static bool PointInsideCell(
|
||||||
|
|
@ -81,14 +236,63 @@ internal static class CollisionTraversal
|
||||||
CellPhysics cell,
|
CellPhysics cell,
|
||||||
Vector3 localPoint)
|
Vector3 localPoint)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
return FlatBspQuery.PointInsideCellBsp(flat, localPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BSPQuery.PointInsideCellBsp(cell.CellBSP?.Root, localPoint);
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
return BSPQuery.PointInsideCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localPoint);
|
||||||
|
|
||||||
|
bool flatResult = false;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatResult = FlatBspQuery.PointInsideCellBsp(
|
||||||
|
cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment"),
|
||||||
|
localPoint);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
bool graphResult = BSPQuery.PointInsideCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localPoint);
|
||||||
|
string input = CollisionShadowVerifier.FormatInput(
|
||||||
|
localPoint,
|
||||||
|
0f);
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordBoolean(
|
||||||
|
sample,
|
||||||
|
"PointInsideCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"PointInsideCell",
|
||||||
|
cell.SourceId,
|
||||||
|
flatFault,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool SphereIntersectsCell(
|
internal static bool SphereIntersectsCell(
|
||||||
|
|
@ -97,7 +301,7 @@ internal static class CollisionTraversal
|
||||||
Vector3 localCenter,
|
Vector3 localCenter,
|
||||||
float radius)
|
float radius)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
FlatCellContainmentBsp flat = cell.FlatContainmentBsp ??
|
||||||
throw MissingFlat("cell containment");
|
throw MissingFlat("cell containment");
|
||||||
|
|
@ -107,10 +311,61 @@ internal static class CollisionTraversal
|
||||||
radius);
|
radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BSPQuery.SphereIntersectsCellBsp(
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
{
|
||||||
|
return BSPQuery.SphereIntersectsCellBsp(
|
||||||
|
cell.CellBSP?.Root,
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool flatResult = false;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatResult = FlatBspQuery.SphereIntersectsCellBsp(
|
||||||
|
cell.FlatContainmentBsp ??
|
||||||
|
throw MissingFlat("cell containment"),
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
bool graphResult = BSPQuery.SphereIntersectsCellBsp(
|
||||||
cell.CellBSP?.Root,
|
cell.CellBSP?.Root,
|
||||||
localCenter,
|
localCenter,
|
||||||
radius);
|
radius);
|
||||||
|
string input = CollisionShadowVerifier.FormatInput(
|
||||||
|
localCenter,
|
||||||
|
radius);
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordBoolean(
|
||||||
|
sample,
|
||||||
|
"SphereIntersectsCell",
|
||||||
|
cell.SourceId,
|
||||||
|
graphResult,
|
||||||
|
flatResult,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"SphereIntersectsCell",
|
||||||
|
cell.SourceId,
|
||||||
|
flatFault,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
return graphResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TransitionState FindCollisions(
|
internal static TransitionState FindCollisions(
|
||||||
|
|
@ -129,7 +384,7 @@ internal static class CollisionTraversal
|
||||||
PhysicsEngine? engine,
|
PhysicsEngine? engine,
|
||||||
Vector3 worldOrigin)
|
Vector3 worldOrigin)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = cell.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("cell physics");
|
throw MissingFlat("cell physics");
|
||||||
|
|
@ -149,7 +404,58 @@ internal static class CollisionTraversal
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BSPQuery.FindCollisions(
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
{
|
||||||
|
return BSPQuery.FindCollisions(
|
||||||
|
cell.BSP?.Root,
|
||||||
|
cell.Resolved,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transition flatTransition = shadow.PrepareShadow(transition);
|
||||||
|
TransitionState flatState = TransitionState.Invalid;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatState = FlatBspQuery.FindCollisions(
|
||||||
|
cell.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("cell physics"),
|
||||||
|
flatTransition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
TransitionState graphState = BSPQuery.FindCollisions(
|
||||||
cell.BSP?.Root,
|
cell.BSP?.Root,
|
||||||
cell.Resolved,
|
cell.Resolved,
|
||||||
transition,
|
transition,
|
||||||
|
|
@ -164,6 +470,39 @@ internal static class CollisionTraversal
|
||||||
localToWorld,
|
localToWorld,
|
||||||
engine,
|
engine,
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
|
string input = CollisionShadowVerifier.FormatInput(
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
worldOrigin);
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordTransition(
|
||||||
|
sample,
|
||||||
|
"FindCellCollisions",
|
||||||
|
cell.SourceId,
|
||||||
|
graphState,
|
||||||
|
flatState,
|
||||||
|
transition,
|
||||||
|
flatTransition,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"FindCellCollisions",
|
||||||
|
cell.SourceId,
|
||||||
|
flatFault,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
return graphState;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TransitionState FindCollisions(
|
internal static TransitionState FindCollisions(
|
||||||
|
|
@ -182,7 +521,7 @@ internal static class CollisionTraversal
|
||||||
PhysicsEngine? engine,
|
PhysicsEngine? engine,
|
||||||
Vector3 worldOrigin)
|
Vector3 worldOrigin)
|
||||||
{
|
{
|
||||||
if (cache.CollisionTraversalMode == CollisionTraversalMode.Flat)
|
if (UseFlat(cache))
|
||||||
{
|
{
|
||||||
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
FlatPhysicsBsp flat = gfxObject.FlatPhysicsBsp ??
|
||||||
throw MissingFlat("GfxObj physics");
|
throw MissingFlat("GfxObj physics");
|
||||||
|
|
@ -202,7 +541,58 @@ internal static class CollisionTraversal
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BSPQuery.FindCollisions(
|
CollisionShadowVerifier? shadow = cache.CollisionShadow;
|
||||||
|
if (shadow is null || !shadow.TrySample(out long sample))
|
||||||
|
{
|
||||||
|
return BSPQuery.FindCollisions(
|
||||||
|
gfxObject.BSP.Root,
|
||||||
|
gfxObject.Resolved,
|
||||||
|
transition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transition flatTransition = shadow.PrepareShadow(transition);
|
||||||
|
TransitionState flatState = TransitionState.Invalid;
|
||||||
|
Exception? flatFault = null;
|
||||||
|
shadow.BeginFlatPass();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
flatState = FlatBspQuery.FindCollisions(
|
||||||
|
gfxObject.FlatPhysicsBsp ??
|
||||||
|
throw MissingFlat("GfxObj physics"),
|
||||||
|
flatTransition,
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
engine,
|
||||||
|
worldOrigin);
|
||||||
|
}
|
||||||
|
catch (Exception fault)
|
||||||
|
{
|
||||||
|
flatFault = fault;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shadow.EndFlatPass();
|
||||||
|
}
|
||||||
|
|
||||||
|
TransitionState graphState = BSPQuery.FindCollisions(
|
||||||
gfxObject.BSP.Root,
|
gfxObject.BSP.Root,
|
||||||
gfxObject.Resolved,
|
gfxObject.Resolved,
|
||||||
transition,
|
transition,
|
||||||
|
|
@ -217,8 +607,45 @@ internal static class CollisionTraversal
|
||||||
localToWorld,
|
localToWorld,
|
||||||
engine,
|
engine,
|
||||||
worldOrigin);
|
worldOrigin);
|
||||||
|
string input = CollisionShadowVerifier.FormatInput(
|
||||||
|
localSphereCenter,
|
||||||
|
localSphereRadius,
|
||||||
|
hasLocalSphere1,
|
||||||
|
localSphere1Center,
|
||||||
|
localSphere1Radius,
|
||||||
|
localCurrentCenter,
|
||||||
|
localSpaceZ,
|
||||||
|
scale,
|
||||||
|
localToWorld,
|
||||||
|
worldOrigin);
|
||||||
|
if (flatFault is null)
|
||||||
|
{
|
||||||
|
shadow.RecordTransition(
|
||||||
|
sample,
|
||||||
|
"FindGfxObjCollisions",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
graphState,
|
||||||
|
flatState,
|
||||||
|
transition,
|
||||||
|
flatTransition,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shadow.RecordFault(
|
||||||
|
sample,
|
||||||
|
"FindGfxObjCollisions",
|
||||||
|
gfxObject.SourceId,
|
||||||
|
flatFault,
|
||||||
|
input);
|
||||||
|
}
|
||||||
|
return graphState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool UseFlat(PhysicsDataCache cache) =>
|
||||||
|
cache.CollisionTraversalMode == CollisionTraversalMode.Flat ||
|
||||||
|
cache.CollisionShadow?.IsFlatPass == true;
|
||||||
|
|
||||||
private static InvalidOperationException MissingFlat(string kind) =>
|
private static InvalidOperationException MissingFlat(string kind) =>
|
||||||
new(
|
new(
|
||||||
$"Flat collision traversal requires a prepared {kind} asset. " +
|
$"Flat collision traversal requires a prepared {kind} asset. " +
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ namespace AcDream.Core.Physics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
/// Thread-safe cache of physics-relevant data extracted from GfxObj and Setup
|
||||||
/// dat objects during streaming. Populated by the streaming worker thread;
|
/// dat objects during streaming and live-object materialization. Prepared
|
||||||
/// read by the physics engine on the game/render thread. ConcurrentDictionary
|
/// payloads are built/read off the hot path; one update-thread publisher
|
||||||
/// makes cross-thread access safe without a global lock.
|
/// installs each graph/flat pair. ConcurrentDictionary keeps diagnostics and
|
||||||
|
/// tooling reads safe without a global lock.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PhysicsDataCache
|
public sealed class PhysicsDataCache
|
||||||
{
|
{
|
||||||
|
|
@ -21,6 +22,29 @@ public sealed class PhysicsDataCache
|
||||||
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
private readonly ConcurrentDictionary<uint, GfxObjVisualBounds> _visualBounds = new();
|
||||||
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();
|
||||||
|
private readonly ConcurrentDictionary<uint, FlatGfxObjCollisionAsset>
|
||||||
|
_flatGfxObj = new();
|
||||||
|
private readonly ConcurrentDictionary<uint, FlatSetupCollision>
|
||||||
|
_flatSetup = new();
|
||||||
|
private readonly ConcurrentDictionary<uint, FlatCellStructureCollisionAsset>
|
||||||
|
_flatCellStruct = new();
|
||||||
|
private readonly ConcurrentDictionary<uint, FlatEnvCellTopology>
|
||||||
|
_flatEnvCell = new();
|
||||||
|
|
||||||
|
public PhysicsDataCache()
|
||||||
|
{
|
||||||
|
if (PhysicsDiagnostics.CollisionShadowSampleEvery > 0)
|
||||||
|
{
|
||||||
|
CollisionShadow = new CollisionShadowVerifier(
|
||||||
|
PhysicsDiagnostics.CollisionShadowSampleEvery,
|
||||||
|
PhysicsDiagnostics.CollisionShadowArtifactDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal CollisionShadowVerifier? CollisionShadow { get; set; }
|
||||||
|
|
||||||
|
public CollisionShadowStats CollisionShadowStats =>
|
||||||
|
CollisionShadow?.Stats ?? default;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Slice I graph/flat differential selector. Production remains on the
|
/// Slice I graph/flat differential selector. Production remains on the
|
||||||
|
|
@ -55,8 +79,14 @@ public sealed class PhysicsDataCache
|
||||||
/// user collide with decorative meshes that don't have a CylSphere or
|
/// user collide with decorative meshes that don't have a CylSphere or
|
||||||
/// per-part BSP.
|
/// per-part BSP.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CacheGfxObj(uint gfxObjId, GfxObj gfxObj)
|
public void CacheGfxObj(
|
||||||
|
uint gfxObjId,
|
||||||
|
GfxObj gfxObj,
|
||||||
|
FlatGfxObjCollisionAsset? prepared = null)
|
||||||
{
|
{
|
||||||
|
if (prepared is not null)
|
||||||
|
_flatGfxObj.TryAdd(gfxObjId, prepared);
|
||||||
|
|
||||||
// Always cache a visual AABB from the mesh vertices — this is cheap
|
// Always cache a visual AABB from the mesh vertices — this is cheap
|
||||||
// and fed by the mesh data that's already loaded. It serves as the
|
// and fed by the mesh data that's already loaded. It serves as the
|
||||||
// fallback collision shape for pure-visual entities.
|
// fallback collision shape for pure-visual entities.
|
||||||
|
|
@ -65,18 +95,25 @@ public sealed class PhysicsDataCache
|
||||||
_visualBounds[gfxObjId] = ComputeVisualBounds(gfxObj.VertexArray);
|
_visualBounds[gfxObjId] = ComputeVisualBounds(gfxObj.VertexArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_gfxObj.ContainsKey(gfxObjId)) return;
|
if (_gfxObj.TryGetValue(gfxObjId, out GfxObjPhysics? existing))
|
||||||
|
{
|
||||||
|
if (prepared is not null)
|
||||||
|
existing.FlatPhysicsBsp ??= prepared.PhysicsBsp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics)) return;
|
if (!gfxObj.Flags.HasFlag(GfxObjFlags.HasPhysics)) return;
|
||||||
if (gfxObj.PhysicsBSP?.Root is null) return;
|
if (gfxObj.PhysicsBSP?.Root is null) return;
|
||||||
if (gfxObj.VertexArray is null) return;
|
if (gfxObj.VertexArray is null) return;
|
||||||
|
|
||||||
var physics = new GfxObjPhysics
|
var physics = new GfxObjPhysics
|
||||||
{
|
{
|
||||||
|
SourceId = gfxObjId,
|
||||||
BSP = gfxObj.PhysicsBSP,
|
BSP = gfxObj.PhysicsBSP,
|
||||||
PhysicsPolygons = gfxObj.PhysicsPolygons,
|
PhysicsPolygons = gfxObj.PhysicsPolygons,
|
||||||
BoundingSphere = gfxObj.PhysicsBSP.Root.BoundingSphere,
|
BoundingSphere = gfxObj.PhysicsBSP.Root.BoundingSphere,
|
||||||
Vertices = gfxObj.VertexArray,
|
Vertices = gfxObj.VertexArray,
|
||||||
Resolved = ResolvePolygons(gfxObj.PhysicsPolygons, gfxObj.VertexArray),
|
Resolved = ResolvePolygons(gfxObj.PhysicsPolygons, gfxObj.VertexArray),
|
||||||
|
FlatPhysicsBsp = prepared?.PhysicsBsp,
|
||||||
};
|
};
|
||||||
_gfxObj[gfxObjId] = physics;
|
_gfxObj[gfxObjId] = physics;
|
||||||
|
|
||||||
|
|
@ -157,17 +194,30 @@ public sealed class PhysicsDataCache
|
||||||
/// Extract and cache the collision shape data from a Setup.
|
/// Extract and cache the collision shape data from a Setup.
|
||||||
/// No-ops if the id is already cached.
|
/// No-ops if the id is already cached.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CacheSetup(uint setupId, Setup setup)
|
public void CacheSetup(
|
||||||
|
uint setupId,
|
||||||
|
Setup setup,
|
||||||
|
FlatSetupCollision? prepared = null)
|
||||||
{
|
{
|
||||||
if (_setup.ContainsKey(setupId)) return;
|
if (prepared is not null)
|
||||||
|
_flatSetup.TryAdd(setupId, prepared);
|
||||||
|
|
||||||
|
if (_setup.TryGetValue(setupId, out SetupPhysics? existing))
|
||||||
|
{
|
||||||
|
if (prepared is not null)
|
||||||
|
existing.FlatCollision ??= prepared;
|
||||||
|
return;
|
||||||
|
}
|
||||||
_setup[setupId] = new SetupPhysics
|
_setup[setupId] = new SetupPhysics
|
||||||
{
|
{
|
||||||
|
SourceId = setupId,
|
||||||
CylSpheres = setup.CylSpheres ?? new(),
|
CylSpheres = setup.CylSpheres ?? new(),
|
||||||
Spheres = setup.Spheres ?? new(),
|
Spheres = setup.Spheres ?? new(),
|
||||||
Height = setup.Height,
|
Height = setup.Height,
|
||||||
Radius = setup.Radius,
|
Radius = setup.Radius,
|
||||||
StepUpHeight = setup.StepUpHeight,
|
StepUpHeight = setup.StepUpHeight,
|
||||||
StepDownHeight = setup.StepDownHeight,
|
StepDownHeight = setup.StepDownHeight,
|
||||||
|
FlatCollision = prepared,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,9 +226,19 @@ public sealed class PhysicsDataCache
|
||||||
/// (indoor room geometry). No-ops if the id is already cached or the
|
/// (indoor room geometry). No-ops if the id is already cached or the
|
||||||
/// CellStruct has no physics BSP.
|
/// CellStruct has no physics BSP.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CacheCellStruct(uint envCellId, DatReaderWriter.DBObjs.EnvCell envCell,
|
public void CacheCellStruct(
|
||||||
CellStruct cellStruct, Matrix4x4 worldTransform)
|
uint envCellId,
|
||||||
|
DatReaderWriter.DBObjs.EnvCell envCell,
|
||||||
|
CellStruct cellStruct,
|
||||||
|
Matrix4x4 worldTransform,
|
||||||
|
FlatCellStructureCollisionAsset? preparedStructure = null,
|
||||||
|
FlatEnvCellTopology? preparedTopology = null)
|
||||||
{
|
{
|
||||||
|
if (preparedStructure is not null)
|
||||||
|
_flatCellStruct.TryAdd(envCellId, preparedStructure);
|
||||||
|
if (preparedTopology is not null)
|
||||||
|
_flatEnvCell.TryAdd(envCellId, preparedTopology);
|
||||||
|
|
||||||
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
// UCG Stage 1: register in the unified graph for ALL cells — before the
|
||||||
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
// idempotency + null-BSP guards below, so BSP-less cells are still included.
|
||||||
if (!CellGraph.Contains(envCellId))
|
if (!CellGraph.Contains(envCellId))
|
||||||
|
|
@ -216,12 +276,17 @@ public sealed class PhysicsDataCache
|
||||||
|
|
||||||
var cellPhysics = new CellPhysics
|
var cellPhysics = new CellPhysics
|
||||||
{
|
{
|
||||||
|
SourceId = envCellId,
|
||||||
BSP = cellStruct.PhysicsBSP,
|
BSP = cellStruct.PhysicsBSP,
|
||||||
PhysicsPolygons = cellStruct.PhysicsPolygons,
|
PhysicsPolygons = cellStruct.PhysicsPolygons,
|
||||||
Vertices = cellStruct.VertexArray,
|
Vertices = cellStruct.VertexArray,
|
||||||
WorldTransform = worldTransform,
|
WorldTransform = worldTransform,
|
||||||
InverseWorldTransform = inverseTransform,
|
InverseWorldTransform = inverseTransform,
|
||||||
Resolved = resolved,
|
Resolved = resolved,
|
||||||
|
FlatPhysicsBsp = preparedStructure?.PhysicsBsp,
|
||||||
|
FlatContainmentBsp = preparedStructure?.ContainmentBsp,
|
||||||
|
FlatPortalPolygons = preparedStructure?.PortalPolygons,
|
||||||
|
FlatTopology = preparedTopology,
|
||||||
// ── Phase 2 portal fields ──
|
// ── Phase 2 portal fields ──
|
||||||
CellBSP = cellStruct.CellBSP,
|
CellBSP = cellStruct.CellBSP,
|
||||||
Portals = portals,
|
Portals = portals,
|
||||||
|
|
@ -394,9 +459,21 @@ public sealed class PhysicsDataCache
|
||||||
|
|
||||||
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
public SetupPhysics? GetSetup(uint id) => _setup.TryGetValue(id, out var p) ? p : null;
|
||||||
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
public CellPhysics? GetCellStruct(uint id) => _cellStruct.TryGetValue(id, out var p) ? p : null;
|
||||||
|
public FlatGfxObjCollisionAsset? GetFlatGfxObj(uint id) =>
|
||||||
|
_flatGfxObj.TryGetValue(id, out var value) ? value : null;
|
||||||
|
public FlatSetupCollision? GetFlatSetup(uint id) =>
|
||||||
|
_flatSetup.TryGetValue(id, out var value) ? value : null;
|
||||||
|
public FlatCellStructureCollisionAsset? GetFlatCellStruct(uint id) =>
|
||||||
|
_flatCellStruct.TryGetValue(id, out var value) ? value : null;
|
||||||
|
public FlatEnvCellTopology? GetFlatEnvCell(uint id) =>
|
||||||
|
_flatEnvCell.TryGetValue(id, out var value) ? value : null;
|
||||||
public int GfxObjCount => _gfxObj.Count;
|
public int GfxObjCount => _gfxObj.Count;
|
||||||
public int SetupCount => _setup.Count;
|
public int SetupCount => _setup.Count;
|
||||||
public int CellStructCount => _cellStruct.Count;
|
public int CellStructCount => _cellStruct.Count;
|
||||||
|
public int FlatGfxObjCount => _flatGfxObj.Count;
|
||||||
|
public int FlatSetupCount => _flatSetup.Count;
|
||||||
|
public int FlatCellStructCount => _flatCellStruct.Count;
|
||||||
|
public int FlatEnvCellCount => _flatEnvCell.Count;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
/// Indoor walking Phase 1 (2026-05-19). Snapshot of currently-cached
|
||||||
|
|
@ -483,6 +560,12 @@ public sealed class PhysicsDataCache
|
||||||
foreach (var key in _cellStruct.Keys)
|
foreach (var key in _cellStruct.Keys)
|
||||||
if ((key & 0xFFFF0000u) == prefix)
|
if ((key & 0xFFFF0000u) == prefix)
|
||||||
_cellStruct.TryRemove(key, out _);
|
_cellStruct.TryRemove(key, out _);
|
||||||
|
foreach (var key in _flatCellStruct.Keys)
|
||||||
|
if ((key & 0xFFFF0000u) == prefix)
|
||||||
|
_flatCellStruct.TryRemove(key, out _);
|
||||||
|
foreach (var key in _flatEnvCell.Keys)
|
||||||
|
if ((key & 0xFFFF0000u) == prefix)
|
||||||
|
_flatEnvCell.TryRemove(key, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
public BuildingPhysics? GetBuilding(uint landcellId)
|
public BuildingPhysics? GetBuilding(uint landcellId)
|
||||||
|
|
@ -539,6 +622,7 @@ public sealed class ResolvedPolygon
|
||||||
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
/// <summary>Cached physics data for a single GfxObj part.</summary>
|
||||||
public sealed class GfxObjPhysics
|
public sealed class GfxObjPhysics
|
||||||
{
|
{
|
||||||
|
public uint SourceId { get; init; }
|
||||||
public required PhysicsBSPTree BSP { get; init; }
|
public required PhysicsBSPTree BSP { get; init; }
|
||||||
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
public required Dictionary<ushort, Polygon> PhysicsPolygons { get; init; }
|
||||||
public Sphere? BoundingSphere { get; init; }
|
public Sphere? BoundingSphere { get; init; }
|
||||||
|
|
@ -551,21 +635,24 @@ public sealed class GfxObjPhysics
|
||||||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepared integer-indexed shadow representation. Optional until Slice I5
|
/// Prepared integer-indexed representation. Slice I5 guarantees this for
|
||||||
/// dual publication is complete.
|
/// production-published collision objects; graph-only test fixtures may
|
||||||
|
/// omit it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
public FlatPhysicsBsp? FlatPhysicsBsp { get; internal set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
/// <summary>Cached collision shape data for a Setup (character/creature capsule).</summary>
|
||||||
public sealed class SetupPhysics
|
public sealed class SetupPhysics
|
||||||
{
|
{
|
||||||
|
public uint SourceId { get; init; }
|
||||||
public List<CylSphere> CylSpheres { get; init; } = new();
|
public List<CylSphere> CylSpheres { get; init; } = new();
|
||||||
public List<Sphere> Spheres { get; init; } = new();
|
public List<Sphere> Spheres { get; init; } = new();
|
||||||
public float Height { get; init; }
|
public float Height { get; init; }
|
||||||
public float Radius { get; init; }
|
public float Radius { get; init; }
|
||||||
public float StepUpHeight { get; init; }
|
public float StepUpHeight { get; init; }
|
||||||
public float StepDownHeight { get; init; }
|
public float StepDownHeight { get; init; }
|
||||||
|
public FlatSetupCollision? FlatCollision { get; internal set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -575,6 +662,7 @@ public sealed class SetupPhysics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CellPhysics
|
public sealed class CellPhysics
|
||||||
{
|
{
|
||||||
|
public uint SourceId { get; init; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The physics BSP tree for this cell. Nullable so that test fixtures
|
/// The physics BSP tree for this cell. Nullable so that test fixtures
|
||||||
/// can construct a <see cref="CellPhysics"/> from <see cref="Resolved"/>
|
/// can construct a <see cref="CellPhysics"/> from <see cref="Resolved"/>
|
||||||
|
|
@ -593,7 +681,9 @@ public sealed class CellPhysics
|
||||||
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
public required Dictionary<ushort, ResolvedPolygon> Resolved { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepared integer-indexed physics BSP shadow. Optional until Slice I5.
|
/// Prepared integer-indexed physics BSP. Slice I5 guarantees this for
|
||||||
|
/// production-published collision cells; graph-only test fixtures may
|
||||||
|
/// omit it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
public FlatPhysicsBsp? FlatPhysicsBsp { get; init; }
|
||||||
|
|
||||||
|
|
@ -610,11 +700,23 @@ public sealed class CellPhysics
|
||||||
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
public DatReaderWriter.Types.CellBSPTree? CellBSP { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepared integer-indexed cell-containment BSP shadow. Optional until
|
/// Prepared integer-indexed cell-containment BSP. Slice I5 guarantees this
|
||||||
/// Slice I5.
|
/// for production-published cells; graph-only test fixtures may omit it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public FlatCellContainmentBsp? FlatContainmentBsp { get; init; }
|
public FlatCellContainmentBsp? FlatContainmentBsp { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared visible-polygon table addressed by
|
||||||
|
/// <see cref="FlatEnvCellTopology.Portals"/>.
|
||||||
|
/// </summary>
|
||||||
|
public FlatPolygonTable? FlatPortalPolygons { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepared per-EnvCell portal/visibility state. Placement remains on this
|
||||||
|
/// runtime record's world transforms.
|
||||||
|
/// </summary>
|
||||||
|
public FlatEnvCellTopology? FlatTopology { 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>.
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,27 @@ namespace AcDream.Core.Physics;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class PhysicsDiagnostics
|
public static class PhysicsDiagnostics
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Slice I5 graph/flat referee cadence. Zero disables the diagnostic;
|
||||||
|
/// N samples every Nth collision-traversal entry. The graph path remains
|
||||||
|
/// authoritative regardless of the comparison result.
|
||||||
|
/// </summary>
|
||||||
|
public static int CollisionShadowSampleEvery { get; set; } =
|
||||||
|
ParsePositiveInt(
|
||||||
|
Environment.GetEnvironmentVariable(
|
||||||
|
"ACDREAM_COLLISION_SHADOW_EVERY"));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Directory for deterministic Slice I5 mismatch artifacts.
|
||||||
|
/// </summary>
|
||||||
|
public static string CollisionShadowArtifactDirectory { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable(
|
||||||
|
"ACDREAM_COLLISION_SHADOW_DIR")
|
||||||
|
?? Path.Combine(
|
||||||
|
Environment.CurrentDirectory,
|
||||||
|
".test-out",
|
||||||
|
"collision-shadow");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, <see cref="PhysicsEngine.ResolveWithTransition"/> emits
|
/// When true, <see cref="PhysicsEngine.ResolveWithTransition"/> emits
|
||||||
/// one structured <c>[resolve]</c> line per call: input + target +
|
/// one structured <c>[resolve]</c> line per call: input + target +
|
||||||
|
|
@ -962,4 +983,14 @@ public static class PhysicsDiagnostics
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ParsePositiveInt(string? value) =>
|
||||||
|
int.TryParse(
|
||||||
|
value,
|
||||||
|
System.Globalization.NumberStyles.None,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out int parsed)
|
||||||
|
&& parsed > 0
|
||||||
|
? parsed
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,22 @@ public sealed class ObjectInfo
|
||||||
MoverHasGravity = false;
|
MoverHasGravity = false;
|
||||||
VelocityKilled = false;
|
VelocityKilled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void CopyFrom(ObjectInfo source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
State = source.State;
|
||||||
|
StepUpHeight = source.StepUpHeight;
|
||||||
|
StepDownHeight = source.StepDownHeight;
|
||||||
|
Ethereal = source.Ethereal;
|
||||||
|
StepDown = source.StepDown;
|
||||||
|
Scale = source.Scale;
|
||||||
|
MoverPhysicsState = source.MoverPhysicsState;
|
||||||
|
TargetId = source.TargetId;
|
||||||
|
SelfEntityId = source.SelfEntityId;
|
||||||
|
MoverHasGravity = source.MoverHasGravity;
|
||||||
|
VelocityKilled = source.VelocityKilled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -398,6 +414,30 @@ public sealed class CollisionInfo
|
||||||
LastCollidedObjectGuid = null;
|
LastCollidedObjectGuid = null;
|
||||||
ContactPlaneWriteCount = 0;
|
ContactPlaneWriteCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void CopyFrom(CollisionInfo source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
_contactPlaneValid = source._contactPlaneValid;
|
||||||
|
_contactPlane = source._contactPlane;
|
||||||
|
_contactPlaneCellId = source._contactPlaneCellId;
|
||||||
|
_contactPlaneIsWater = source._contactPlaneIsWater;
|
||||||
|
_lastKnownContactPlaneValid = source._lastKnownContactPlaneValid;
|
||||||
|
_lastKnownContactPlane = source._lastKnownContactPlane;
|
||||||
|
_lastKnownContactPlaneCellId = source._lastKnownContactPlaneCellId;
|
||||||
|
_lastKnownContactPlaneIsWater = source._lastKnownContactPlaneIsWater;
|
||||||
|
SlidingNormalValid = source.SlidingNormalValid;
|
||||||
|
SlidingNormal = source.SlidingNormal;
|
||||||
|
CollisionNormalValid = source.CollisionNormalValid;
|
||||||
|
CollisionNormal = source.CollisionNormal;
|
||||||
|
CollidedWithEnvironment = source.CollidedWithEnvironment;
|
||||||
|
FramesStationaryFall = source.FramesStationaryFall;
|
||||||
|
AdjustOffset = source.AdjustOffset;
|
||||||
|
CollideObjectGuids.Clear();
|
||||||
|
CollideObjectGuids.AddRange(source.CollideObjectGuids);
|
||||||
|
LastCollidedObjectGuid = source.LastCollidedObjectGuid;
|
||||||
|
ContactPlaneWriteCount = source.ContactPlaneWriteCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -774,6 +814,65 @@ public sealed class SpherePath
|
||||||
OrderedCellScratch.ResetForReuse();
|
OrderedCellScratch.ResetForReuse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void CopyFrom(SpherePath source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
NumSphere = source.NumSphere;
|
||||||
|
CopySphereArray(source.LocalSphere, LocalSphere);
|
||||||
|
CopySphereArray(source.GlobalSphere, GlobalSphere);
|
||||||
|
CopySphereArray(source.GlobalCurrCenter, GlobalCurrCenter);
|
||||||
|
BeginPos = source.BeginPos;
|
||||||
|
EndPos = source.EndPos;
|
||||||
|
CurPos = source.CurPos;
|
||||||
|
CheckPos = source.CheckPos;
|
||||||
|
BeginOrientation = source.BeginOrientation;
|
||||||
|
EndOrientation = source.EndOrientation;
|
||||||
|
CurOrientation = source.CurOrientation;
|
||||||
|
CheckOrientation = source.CheckOrientation;
|
||||||
|
CurCellId = source.CurCellId;
|
||||||
|
CheckCellId = source.CheckCellId;
|
||||||
|
CarriedBlockOrigin = source.CarriedBlockOrigin;
|
||||||
|
GlobalOffset = source.GlobalOffset;
|
||||||
|
StepUp = source.StepUp;
|
||||||
|
StepUpNormal = source.StepUpNormal;
|
||||||
|
Collide = source.Collide;
|
||||||
|
StepDown = source.StepDown;
|
||||||
|
StepDownAmt = source.StepDownAmt;
|
||||||
|
WalkInterp = source.WalkInterp;
|
||||||
|
WalkableValid = source.WalkableValid;
|
||||||
|
WalkablePlane = source.WalkablePlane;
|
||||||
|
WalkableVertices = source.WalkableVertices is null
|
||||||
|
? null
|
||||||
|
: CopyExact(
|
||||||
|
source.WalkableVertices,
|
||||||
|
ref _walkableVertexStorage);
|
||||||
|
WalkableUp = source.WalkableUp;
|
||||||
|
WalkableAllowance = source.WalkableAllowance;
|
||||||
|
LastWalkableValid = source.LastWalkableValid;
|
||||||
|
LastWalkablePlane = source.LastWalkablePlane;
|
||||||
|
LastWalkableVertices = source.LastWalkableVertices is null
|
||||||
|
? null
|
||||||
|
: CopyExact(
|
||||||
|
source.LastWalkableVertices,
|
||||||
|
ref _lastWalkableVertexStorage);
|
||||||
|
LastWalkableUp = source.LastWalkableUp;
|
||||||
|
BackupCheckPos = source.BackupCheckPos;
|
||||||
|
BackupCheckCellId = source.BackupCheckCellId;
|
||||||
|
NegPolyHit = source.NegPolyHit;
|
||||||
|
NegStepUp = source.NegStepUp;
|
||||||
|
NegCollisionNormal = source.NegCollisionNormal;
|
||||||
|
CheckWalkable = source.CheckWalkable;
|
||||||
|
InsertType = source.InsertType;
|
||||||
|
PlacementAllowsSliding = source.PlacementAllowsSliding;
|
||||||
|
ObstructionEthereal = source.ObstructionEthereal;
|
||||||
|
BldgCheck = source.BldgCheck;
|
||||||
|
HitsInteriorCell = source.HitsInteriorCell;
|
||||||
|
CellCandidates.Clear();
|
||||||
|
foreach (uint id in source.CellCandidates)
|
||||||
|
CellCandidates.Add(id);
|
||||||
|
OrderedCellScratch.ResetForReuse();
|
||||||
|
}
|
||||||
|
|
||||||
private static Vector3[] CopyExact(
|
private static Vector3[] CopyExact(
|
||||||
ReadOnlySpan<Vector3> source,
|
ReadOnlySpan<Vector3> source,
|
||||||
ref Vector3[]? storage)
|
ref Vector3[]? storage)
|
||||||
|
|
@ -800,6 +899,15 @@ public sealed class SpherePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CopySphereArray(Sphere[] source, Sphere[] destination)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < destination.Length; i++)
|
||||||
|
{
|
||||||
|
destination[i].Origin = source[i].Origin;
|
||||||
|
destination[i].Radius = source[i].Radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Slide fallback when step-up fails. Clears the contact-plane state that
|
/// Slide fallback when step-up fails. Clears the contact-plane state that
|
||||||
/// caused the step-up attempt and runs the full sphere-slide computation
|
/// caused the step-up attempt and runs the full sphere-slide computation
|
||||||
|
|
@ -958,6 +1066,14 @@ public sealed class Transition
|
||||||
CollisionInfo.ResetForReuse();
|
CollisionInfo.ResetForReuse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void CopyFrom(Transition source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ObjectInfo.CopyFrom(source.ObjectInfo);
|
||||||
|
SpherePath.CopyFrom(source.SpherePath);
|
||||||
|
CollisionInfo.CopyFrom(source.CollisionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool DumpEdgeSlideEnabled =>
|
private static bool DumpEdgeSlideEnabled =>
|
||||||
Environment.GetEnvironmentVariable("ACDREAM_DUMP_EDGE_SLIDE") == "1";
|
Environment.GetEnvironmentVariable("ACDREAM_DUMP_EDGE_SLIDE") == "1";
|
||||||
|
|
||||||
|
|
|
||||||
29
src/AcDream.Core/World/LandblockCollisionBuild.cs
Normal file
29
src/AcDream.Core/World/LandblockCollisionBuild.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Core.World;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable prepared-collision closure carried by one near-tier landblock
|
||||||
|
/// build. Keys remain the original DAT source IDs. Cell-structure geometry and
|
||||||
|
/// per-EnvCell topology stay separate so package aliases retain one reusable
|
||||||
|
/// geometry identity without conflating cell placement or portal state.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record LandblockCollisionBuild(
|
||||||
|
ImmutableDictionary<uint, FlatGfxObjCollisionAsset> GfxObjs,
|
||||||
|
ImmutableDictionary<uint, FlatSetupCollision> Setups,
|
||||||
|
ImmutableDictionary<uint, FlatCellStructureCollisionAsset> CellStructures,
|
||||||
|
ImmutableDictionary<uint, FlatEnvCellTopology> EnvCells,
|
||||||
|
ImmutableArray<uint> GfxObjIds,
|
||||||
|
ImmutableArray<uint> SetupIds,
|
||||||
|
ImmutableArray<uint> EnvCellIds)
|
||||||
|
{
|
||||||
|
public static readonly LandblockCollisionBuild Empty = new(
|
||||||
|
ImmutableDictionary<uint, FlatGfxObjCollisionAsset>.Empty,
|
||||||
|
ImmutableDictionary<uint, FlatSetupCollision>.Empty,
|
||||||
|
ImmutableDictionary<uint, FlatCellStructureCollisionAsset>.Empty,
|
||||||
|
ImmutableDictionary<uint, FlatEnvCellTopology>.Empty,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
ImmutableArray<uint>.Empty);
|
||||||
|
}
|
||||||
|
|
@ -410,6 +410,12 @@ public sealed class ContentEffectsAudioCompositionTests
|
||||||
(LiveEntityCollisionBuilder)RuntimeHelpers.GetUninitializedObject(
|
(LiveEntityCollisionBuilder)RuntimeHelpers.GetUninitializedObject(
|
||||||
typeof(LiveEntityCollisionBuilder));
|
typeof(LiveEntityCollisionBuilder));
|
||||||
|
|
||||||
|
public LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||||
|
PhysicsDataCache physicsData,
|
||||||
|
IPreparedAssetSource preparedAssets) =>
|
||||||
|
(LiveCollisionAssetPublisher)RuntimeHelpers.GetUninitializedObject(
|
||||||
|
typeof(LiveCollisionAssetPublisher));
|
||||||
|
|
||||||
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) => new();
|
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) => new();
|
||||||
public ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters) => new(emitters);
|
public ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters) => new(emitters);
|
||||||
public ParticleHookSink CreateParticleSink(
|
public ParticleHookSink CreateParticleSink(
|
||||||
|
|
|
||||||
|
|
@ -381,6 +381,7 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, default,
|
||||||
default, default, 0d, 0d, null);
|
default, default, 0d, 0d, null);
|
||||||
|
|
||||||
private static string NewDirectory()
|
private static string NewDirectory()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.Content;
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Physics;
|
||||||
|
|
||||||
|
public sealed class LiveCollisionAssetPublisherTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LiveGfxAndSetup_PublishExactPreparedViewOnce()
|
||||||
|
{
|
||||||
|
const uint GfxId = 0x0100_0042u;
|
||||||
|
const uint SetupId = 0x0200_0042u;
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var source = new RecordingSource();
|
||||||
|
var publisher = new LiveCollisionAssetPublisher(cache, source);
|
||||||
|
GfxObj gfx = PhysicsGfx();
|
||||||
|
var setup = new Setup();
|
||||||
|
|
||||||
|
publisher.CacheGfxObj(GfxId, gfx);
|
||||||
|
publisher.CacheGfxObj(GfxId, gfx);
|
||||||
|
publisher.CacheSetup(SetupId, setup);
|
||||||
|
publisher.CacheSetup(SetupId, setup);
|
||||||
|
|
||||||
|
Assert.Same(source.Gfx, cache.GetFlatGfxObj(GfxId));
|
||||||
|
Assert.Same(
|
||||||
|
source.Gfx.PhysicsBsp,
|
||||||
|
cache.GetGfxObj(GfxId)!.FlatPhysicsBsp);
|
||||||
|
Assert.Same(source.Setup, cache.GetFlatSetup(SetupId));
|
||||||
|
Assert.Same(
|
||||||
|
source.Setup,
|
||||||
|
cache.GetSetup(SetupId)!.FlatCollision);
|
||||||
|
Assert.Equal(1, source.GfxReads);
|
||||||
|
Assert.Equal(1, source.SetupReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VisualOnlyGfx_RemembersPreparedEmptyAssetWithoutGraphRecord()
|
||||||
|
{
|
||||||
|
const uint GfxId = 0x0100_0043u;
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var source = new RecordingSource();
|
||||||
|
var publisher = new LiveCollisionAssetPublisher(cache, source);
|
||||||
|
var gfx = new GfxObj();
|
||||||
|
|
||||||
|
publisher.CacheGfxObj(GfxId, gfx);
|
||||||
|
publisher.CacheGfxObj(GfxId, gfx);
|
||||||
|
|
||||||
|
Assert.Null(cache.GetGfxObj(GfxId));
|
||||||
|
Assert.Same(source.Gfx, cache.GetFlatGfxObj(GfxId));
|
||||||
|
Assert.Equal(1, source.GfxReads);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingPreparedAsset_RejectsGraphOnlyLivePublication()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
var source = new RecordingSource
|
||||||
|
{
|
||||||
|
GfxResult =
|
||||||
|
PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
.Missing,
|
||||||
|
};
|
||||||
|
var publisher = new LiveCollisionAssetPublisher(cache, source);
|
||||||
|
|
||||||
|
InvalidDataException fault = Assert.Throws<InvalidDataException>(
|
||||||
|
() => publisher.CacheGfxObj(0x0100_0044u, PhysicsGfx()));
|
||||||
|
|
||||||
|
Assert.Contains("Missing", fault.Message, StringComparison.Ordinal);
|
||||||
|
Assert.Equal(0, cache.GfxObjCount);
|
||||||
|
Assert.Equal(0, cache.FlatGfxObjCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GfxObj PhysicsGfx() => new()
|
||||||
|
{
|
||||||
|
Flags = GfxObjFlags.HasPhysics,
|
||||||
|
PhysicsBSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode
|
||||||
|
{
|
||||||
|
Type = BSPNodeType.Leaf,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VertexArray = new VertexArray(),
|
||||||
|
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private sealed class RecordingSource : IPreparedCollisionSource
|
||||||
|
{
|
||||||
|
private static readonly FlatPhysicsBsp EmptyPhysics = new(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty);
|
||||||
|
|
||||||
|
public FlatGfxObjCollisionAsset Gfx { get; } =
|
||||||
|
new(EmptyPhysics, null, null);
|
||||||
|
public FlatSetupCollision Setup { get; } = new(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
public int GfxReads { get; private set; }
|
||||||
|
public int SetupReads { get; private set; }
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>?
|
||||||
|
GfxResult { get; init; }
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId) => PreparedAssetPresence.Available;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
GfxReads++;
|
||||||
|
return GfxResult ??
|
||||||
|
PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
.Loaded(Gfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
SetupReads++;
|
||||||
|
return PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
.Loaded(Setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
.Missing;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
PreparedCollisionReadResult<FlatEnvCellTopology>.Missing;
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats => default;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
|
|
@ -32,6 +33,7 @@ public sealed class LandblockBuildFactoryTests
|
||||||
Assert.Empty(result.Landblock.Entities);
|
Assert.Empty(result.Landblock.Entities);
|
||||||
Assert.Same(PhysicsDatBundle.Empty, result.Landblock.PhysicsDats);
|
Assert.Same(PhysicsDatBundle.Empty, result.Landblock.PhysicsDats);
|
||||||
Assert.Null(result.EnvCells);
|
Assert.Null(result.EnvCells);
|
||||||
|
Assert.Null(result.Collisions);
|
||||||
Assert.Equal(HoltburgOrigin, result.Origin);
|
Assert.Equal(HoltburgOrigin, result.Origin);
|
||||||
Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads);
|
Assert.Equal([(typeof(LandBlock), LandblockId)], proxy.Reads);
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +83,40 @@ public sealed class LandblockBuildFactoryTests
|
||||||
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
|
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
|
||||||
Assert.Null(physics.Info);
|
Assert.Null(physics.Info);
|
||||||
Assert.Empty(physics.EnvCells);
|
Assert.Empty(physics.EnvCells);
|
||||||
|
Assert.NotNull(result.Collisions);
|
||||||
|
Assert.Empty(result.Collisions.GfxObjs);
|
||||||
|
Assert.Empty(result.Collisions.Setups);
|
||||||
|
Assert.Empty(result.Collisions.CellStructures);
|
||||||
|
Assert.Empty(result.Collisions.EnvCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NearPreparedFaultRejectsWholeGenerationWhileFarNeverReadsCollision()
|
||||||
|
{
|
||||||
|
var dat = CreateDat(out RecordingDatProxy proxy);
|
||||||
|
AddNearFixture(proxy, LandblockId, environmentId: 1);
|
||||||
|
var source = new TestPreparedCollisionSource(
|
||||||
|
PreparedAssetReadStatus.Missing);
|
||||||
|
var factory = new LandblockBuildFactory(
|
||||||
|
dat,
|
||||||
|
source,
|
||||||
|
new object(),
|
||||||
|
new float[256]);
|
||||||
|
|
||||||
|
LandblockBuild? far = factory.Build(
|
||||||
|
Request(LandblockStreamJobKind.LoadFar));
|
||||||
|
Assert.NotNull(far);
|
||||||
|
Assert.Null(far.Collisions);
|
||||||
|
Assert.Equal(0, source.Reads);
|
||||||
|
|
||||||
|
InvalidDataException failure =
|
||||||
|
Assert.Throws<InvalidDataException>(() =>
|
||||||
|
factory.Build(Request(LandblockStreamJobKind.LoadNear)));
|
||||||
|
Assert.Contains(
|
||||||
|
"complete near-tier generation",
|
||||||
|
failure.Message,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.True(source.Reads > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -103,6 +139,9 @@ public sealed class LandblockBuildFactoryTests
|
||||||
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
|
var physics = Assert.IsType<PhysicsDatBundle>(result.Landblock.PhysicsDats);
|
||||||
Assert.Single(physics.EnvCells);
|
Assert.Single(physics.EnvCells);
|
||||||
Assert.DoesNotContain(missingCell, physics.EnvCells.Keys);
|
Assert.DoesNotContain(missingCell, physics.EnvCells.Keys);
|
||||||
|
Assert.Single(result.Collisions!.CellStructures);
|
||||||
|
Assert.Single(result.Collisions.EnvCells);
|
||||||
|
Assert.DoesNotContain(missingCell, result.Collisions.EnvCells.Keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -215,9 +254,9 @@ public sealed class LandblockBuildFactoryTests
|
||||||
var heights = Enumerable.Range(0, 256).Select(value => (float)value).ToArray();
|
var heights = Enumerable.Range(0, 256).Select(value => (float)value).ToArray();
|
||||||
var factory = new LandblockBuildFactory(
|
var factory = new LandblockBuildFactory(
|
||||||
dat,
|
dat,
|
||||||
|
TestPreparedCollisionSource.Instance,
|
||||||
new object(),
|
new object(),
|
||||||
heights,
|
heights);
|
||||||
new PhysicsDataCache());
|
|
||||||
heights[42] = -1f;
|
heights[42] = -1f;
|
||||||
|
|
||||||
var snapshot = Assert.IsType<float[]>(typeof(LandblockBuildFactory)
|
var snapshot = Assert.IsType<float[]>(typeof(LandblockBuildFactory)
|
||||||
|
|
@ -234,9 +273,9 @@ public sealed class LandblockBuildFactoryTests
|
||||||
|
|
||||||
Assert.Throws<ArgumentException>(() => new LandblockBuildFactory(
|
Assert.Throws<ArgumentException>(() => new LandblockBuildFactory(
|
||||||
dat,
|
dat,
|
||||||
|
TestPreparedCollisionSource.Instance,
|
||||||
new object(),
|
new object(),
|
||||||
new float[255],
|
new float[255]));
|
||||||
new PhysicsDataCache()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -273,6 +312,7 @@ public sealed class LandblockBuildFactoryTests
|
||||||
dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true);
|
dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true);
|
||||||
Assert.DoesNotContain(dependencyTypes, dependency =>
|
Assert.DoesNotContain(dependencyTypes, dependency =>
|
||||||
dependency.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true);
|
dependency.Namespace?.StartsWith("Silk.NET", StringComparison.Ordinal) == true);
|
||||||
|
Assert.DoesNotContain(typeof(PhysicsDataCache), dependencyTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -288,11 +328,15 @@ public sealed class LandblockBuildFactoryTests
|
||||||
float[]? heights = region?.LandDefs.LandHeightTable;
|
float[]? heights = region?.LandDefs.LandHeightTable;
|
||||||
Assert.NotNull(heights);
|
Assert.NotNull(heights);
|
||||||
Assert.True(heights.Length >= 256);
|
Assert.True(heights.Length >= 256);
|
||||||
|
string? pakPath = ResolvePreparedPackagePath(datDirectory);
|
||||||
|
if (pakPath is null)
|
||||||
|
throw SkipException.ForSkip("Installed acdream.pak is required.");
|
||||||
|
using var prepared = new PakPreparedAssetSource(pakPath, bounded);
|
||||||
var factory = new LandblockBuildFactory(
|
var factory = new LandblockBuildFactory(
|
||||||
bounded,
|
bounded,
|
||||||
|
prepared,
|
||||||
new object(),
|
new object(),
|
||||||
heights,
|
heights);
|
||||||
new PhysicsDataCache());
|
|
||||||
LandblockBuildRequest request = Request(LandblockStreamJobKind.LoadNear, generation: 4);
|
LandblockBuildRequest request = Request(LandblockStreamJobKind.LoadNear, generation: 4);
|
||||||
|
|
||||||
LandblockBuild? first = factory.Build(request);
|
LandblockBuild? first = factory.Build(request);
|
||||||
|
|
@ -302,6 +346,7 @@ public sealed class LandblockBuildFactoryTests
|
||||||
Assert.NotNull(second);
|
Assert.NotNull(second);
|
||||||
Assert.Equal(HoltburgOrigin, first.Origin);
|
Assert.Equal(HoltburgOrigin, first.Origin);
|
||||||
Assert.NotNull(first.EnvCells);
|
Assert.NotNull(first.EnvCells);
|
||||||
|
Assert.NotNull(first.Collisions);
|
||||||
Assert.NotEmpty(first.Landblock.Entities);
|
Assert.NotEmpty(first.Landblock.Entities);
|
||||||
AssertEntityBuildsEqual(first.Landblock.Entities, second.Landblock.Entities);
|
AssertEntityBuildsEqual(first.Landblock.Entities, second.Landblock.Entities);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|
@ -335,6 +380,12 @@ public sealed class LandblockBuildFactoryTests
|
||||||
PhysicsDatBundle physics = Assert.IsType<PhysicsDatBundle>(
|
PhysicsDatBundle physics = Assert.IsType<PhysicsDatBundle>(
|
||||||
first.Landblock.PhysicsDats);
|
first.Landblock.PhysicsDats);
|
||||||
Assert.NotNull(physics.Info);
|
Assert.NotNull(physics.Info);
|
||||||
|
Assert.Equal(physics.EnvCells.Keys.Order(), first.Collisions.EnvCells.Keys.Order());
|
||||||
|
Assert.Equal(
|
||||||
|
physics.EnvCells.Keys.Order(),
|
||||||
|
first.Collisions.CellStructures.Keys.Order());
|
||||||
|
Assert.Equal(physics.Setups.Keys.Order(), first.Collisions.Setups.Keys.Order());
|
||||||
|
Assert.Equal(physics.GfxObjs.Keys.Order(), first.Collisions.GfxObjs.Keys.Order());
|
||||||
foreach (WorldEntity entity in first.Landblock.Entities)
|
foreach (WorldEntity entity in first.Landblock.Entities)
|
||||||
{
|
{
|
||||||
if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
if ((entity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u)
|
||||||
|
|
@ -345,7 +396,11 @@ public sealed class LandblockBuildFactoryTests
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LandblockBuildFactory Factory(IDatReaderWriter dat, object gate) =>
|
private static LandblockBuildFactory Factory(IDatReaderWriter dat, object gate) =>
|
||||||
new(dat, gate, new float[256], new PhysicsDataCache());
|
new(
|
||||||
|
dat,
|
||||||
|
TestPreparedCollisionSource.Instance,
|
||||||
|
gate,
|
||||||
|
new float[256]);
|
||||||
|
|
||||||
private static LandblockBuildRequest Request(
|
private static LandblockBuildRequest Request(
|
||||||
LandblockStreamJobKind kind,
|
LandblockStreamJobKind kind,
|
||||||
|
|
@ -444,6 +499,26 @@ public sealed class LandblockBuildFactoryTests
|
||||||
return Directory.Exists(documents) ? documents : null;
|
return Directory.Exists(documents) ? documents : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ResolvePreparedPackagePath(string datDirectory)
|
||||||
|
{
|
||||||
|
string? configured =
|
||||||
|
System.Environment.GetEnvironmentVariable("ACDREAM_PAK_PATH");
|
||||||
|
if (!string.IsNullOrWhiteSpace(configured) && File.Exists(configured))
|
||||||
|
return configured;
|
||||||
|
|
||||||
|
string besideDats = Path.Combine(datDirectory, "acdream.pak");
|
||||||
|
if (File.Exists(besideDats))
|
||||||
|
return besideDats;
|
||||||
|
|
||||||
|
string documents = Path.Combine(
|
||||||
|
System.Environment.GetFolderPath(
|
||||||
|
System.Environment.SpecialFolder.UserProfile),
|
||||||
|
"Documents",
|
||||||
|
"Asheron's Call",
|
||||||
|
"acdream.pak");
|
||||||
|
return File.Exists(documents) ? documents : null;
|
||||||
|
}
|
||||||
|
|
||||||
private class RecordingDatProxy : DispatchProxy
|
private class RecordingDatProxy : DispatchProxy
|
||||||
{
|
{
|
||||||
private readonly Dictionary<(Type Type, uint Id), object> _objects = new();
|
private readonly Dictionary<(Type Type, uint Id), object> _objects = new();
|
||||||
|
|
@ -506,4 +581,97 @@ public sealed class LandblockBuildFactoryTests
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class TestPreparedCollisionSource :
|
||||||
|
IPreparedCollisionSource
|
||||||
|
{
|
||||||
|
private static readonly FlatPhysicsBsp EmptyPhysics = new(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty);
|
||||||
|
private static readonly FlatCellStructureCollisionAsset EmptyCell =
|
||||||
|
new(
|
||||||
|
EmptyPhysics,
|
||||||
|
new FlatCellContainmentBsp(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatCellBspNode>.Empty),
|
||||||
|
FlatPolygonTable.Empty);
|
||||||
|
private static readonly FlatEnvCellTopology EmptyTopology = new(
|
||||||
|
ImmutableArray<FlatEnvCellPortal>.Empty,
|
||||||
|
ImmutableArray<uint>.Empty,
|
||||||
|
seenOutside: false);
|
||||||
|
|
||||||
|
private readonly PreparedAssetReadStatus _status;
|
||||||
|
|
||||||
|
internal TestPreparedCollisionSource(
|
||||||
|
PreparedAssetReadStatus status =
|
||||||
|
PreparedAssetReadStatus.Loaded)
|
||||||
|
{
|
||||||
|
_status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TestPreparedCollisionSource Instance { get; } = new();
|
||||||
|
internal int Reads { get; private set; }
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
AcDream.Content.Pak.PakAssetType type,
|
||||||
|
uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Available;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Result(
|
||||||
|
new FlatGfxObjCollisionAsset(
|
||||||
|
EmptyPhysics,
|
||||||
|
null,
|
||||||
|
null));
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Result(
|
||||||
|
new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f));
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Result(EmptyCell);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
Result(EmptyTopology);
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats => default;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private PreparedCollisionReadResult<T> Result<T>(T value)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
Reads++;
|
||||||
|
return _status switch
|
||||||
|
{
|
||||||
|
PreparedAssetReadStatus.Loaded =>
|
||||||
|
PreparedCollisionReadResult<T>.Loaded(value),
|
||||||
|
PreparedAssetReadStatus.Missing =>
|
||||||
|
PreparedCollisionReadResult<T>.Missing,
|
||||||
|
_ => PreparedCollisionReadResult<T>.Corrupt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
|
|
@ -94,6 +95,254 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
Assert.Equal(0x01000077u, building.ModelId);
|
Assert.Equal(0x01000077u, building.ModelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DualPublication_InstallsExactPreparedClosureAndReleasesCellOwner()
|
||||||
|
{
|
||||||
|
const uint envCellId = 0xA9B40100u;
|
||||||
|
const uint gfxObjId = 0x01000077u;
|
||||||
|
var fixture = Fixture();
|
||||||
|
PhysicsDatBundle baseBundle =
|
||||||
|
CellPortalAndBuildingBundle(envCellId);
|
||||||
|
DatReaderWriter.DBObjs.Environment environment =
|
||||||
|
Assert.Single(baseBundle.Environments.Values);
|
||||||
|
CellStruct cellStruct = Assert.Single(environment.Cells.Values);
|
||||||
|
cellStruct.PhysicsBSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode
|
||||||
|
{
|
||||||
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
||||||
|
BoundingSphere = new Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Zero,
|
||||||
|
Radius = 10f,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
cellStruct.CellBSP = new CellBSPTree
|
||||||
|
{
|
||||||
|
Root = new CellBSPNode
|
||||||
|
{
|
||||||
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
||||||
|
LeafIndex = 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
EnvCell envCell = baseBundle.EnvCells[envCellId];
|
||||||
|
var setup = new Setup();
|
||||||
|
setup.CylSpheres.Add(new CylSphere
|
||||||
|
{
|
||||||
|
Origin = new Vector3(0f, 0f, 0.6f),
|
||||||
|
Radius = 0.4f,
|
||||||
|
Height = 1.2f,
|
||||||
|
});
|
||||||
|
GfxObj gfx = PhysicsGfx();
|
||||||
|
var bundle = new PhysicsDatBundle(
|
||||||
|
baseBundle.Info,
|
||||||
|
baseBundle.EnvCells,
|
||||||
|
baseBundle.Environments,
|
||||||
|
new Dictionary<uint, Setup> { [SetupId] = setup },
|
||||||
|
new Dictionary<uint, GfxObj> { [gfxObjId] = gfx });
|
||||||
|
FlatGfxObjCollisionAsset flatGfx =
|
||||||
|
FlatCollisionAssetBuilder.FlattenGfxObj(gfx);
|
||||||
|
FlatSetupCollision flatSetup =
|
||||||
|
FlatCollisionAssetBuilder.FlattenSetup(setup);
|
||||||
|
FlatCellStructureCollisionAsset flatCell =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellStructure(cellStruct);
|
||||||
|
FlatEnvCellTopology flatTopology =
|
||||||
|
FlatCollisionAssetBuilder.FlattenEnvCellTopology(
|
||||||
|
envCellId,
|
||||||
|
envCell,
|
||||||
|
flatCell.PortalPolygons);
|
||||||
|
var collisions = new LandblockCollisionBuild(
|
||||||
|
ImmutableDictionary<uint, FlatGfxObjCollisionAsset>.Empty
|
||||||
|
.Add(gfxObjId, flatGfx),
|
||||||
|
ImmutableDictionary<uint, FlatSetupCollision>.Empty
|
||||||
|
.Add(SetupId, flatSetup),
|
||||||
|
ImmutableDictionary<uint, FlatCellStructureCollisionAsset>.Empty
|
||||||
|
.Add(envCellId, flatCell),
|
||||||
|
ImmutableDictionary<uint, FlatEnvCellTopology>.Empty
|
||||||
|
.Add(envCellId, flatTopology),
|
||||||
|
[gfxObjId],
|
||||||
|
[SetupId],
|
||||||
|
[envCellId]);
|
||||||
|
WorldEntity entity = new()
|
||||||
|
{
|
||||||
|
Id = 0x80A9B401u,
|
||||||
|
SourceGfxObjOrSetupId = SetupId,
|
||||||
|
Position = new Vector3(12f, 12f, 0f),
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs = [new MeshRef(gfxObjId, Matrix4x4.Identity)],
|
||||||
|
};
|
||||||
|
|
||||||
|
Publish(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(
|
||||||
|
FirstLandblock,
|
||||||
|
[entity],
|
||||||
|
bundle,
|
||||||
|
collisions: collisions));
|
||||||
|
|
||||||
|
Assert.Same(flatGfx, fixture.Cache.GetFlatGfxObj(gfxObjId));
|
||||||
|
Assert.Same(
|
||||||
|
flatGfx.PhysicsBsp,
|
||||||
|
fixture.Cache.GetGfxObj(gfxObjId)!.FlatPhysicsBsp);
|
||||||
|
Assert.Same(flatSetup, fixture.Cache.GetFlatSetup(SetupId));
|
||||||
|
Assert.Same(
|
||||||
|
flatSetup,
|
||||||
|
fixture.Cache.GetSetup(SetupId)!.FlatCollision);
|
||||||
|
Assert.Same(flatCell, fixture.Cache.GetFlatCellStruct(envCellId));
|
||||||
|
Assert.Same(flatTopology, fixture.Cache.GetFlatEnvCell(envCellId));
|
||||||
|
Assert.Same(
|
||||||
|
flatCell.PhysicsBsp,
|
||||||
|
fixture.Cache.GetCellStruct(envCellId)!.FlatPhysicsBsp);
|
||||||
|
|
||||||
|
fixture.Publisher.RemoveLandblock(FirstLandblock);
|
||||||
|
|
||||||
|
Assert.Equal(0, fixture.Cache.CellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatEnvCellCount);
|
||||||
|
Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DualPublication_DemotionRehydrateRevisitAndRemovalOwnFlatCellsExactlyOnce()
|
||||||
|
{
|
||||||
|
const uint firstCellId = 0xA9B40100u;
|
||||||
|
const uint adjacentCellId = 0xAAB40100u;
|
||||||
|
var fixture = Fixture();
|
||||||
|
PhysicsDatBundle firstBundle =
|
||||||
|
CellPortalAndBuildingBundle(firstCellId);
|
||||||
|
PhysicsDatBundle adjacentBundle =
|
||||||
|
CellPortalAndBuildingBundle(adjacentCellId);
|
||||||
|
LandblockCollisionBuild firstCollision =
|
||||||
|
FlatCellClosure(firstBundle, firstCellId);
|
||||||
|
LandblockCollisionBuild adjacentCollision =
|
||||||
|
FlatCellClosure(adjacentBundle, adjacentCellId);
|
||||||
|
|
||||||
|
Publish(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(
|
||||||
|
FirstLandblock,
|
||||||
|
bundle: firstBundle,
|
||||||
|
collisions: firstCollision));
|
||||||
|
Publish(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(
|
||||||
|
AdjacentLandblock,
|
||||||
|
bundle: adjacentBundle,
|
||||||
|
collisions: adjacentCollision));
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
firstCollision.CellStructures[firstCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(firstCellId));
|
||||||
|
Assert.Same(
|
||||||
|
adjacentCollision.CellStructures[adjacentCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(adjacentCellId));
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatEnvCellCount);
|
||||||
|
|
||||||
|
fixture.Publisher.DemoteToTerrain(FirstLandblock);
|
||||||
|
|
||||||
|
Assert.Null(fixture.Cache.GetFlatCellStruct(firstCellId));
|
||||||
|
Assert.Null(fixture.Cache.GetFlatEnvCell(firstCellId));
|
||||||
|
Assert.Same(
|
||||||
|
adjacentCollision.CellStructures[adjacentCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(adjacentCellId));
|
||||||
|
Assert.Equal(1, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(1, fixture.Cache.FlatEnvCellCount);
|
||||||
|
|
||||||
|
LandblockCollisionBuild rehydratedCollision =
|
||||||
|
FlatCellClosure(firstBundle, firstCellId);
|
||||||
|
Publish(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(
|
||||||
|
FirstLandblock,
|
||||||
|
bundle: firstBundle,
|
||||||
|
collisions: rehydratedCollision));
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
rehydratedCollision.CellStructures[firstCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(firstCellId));
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatEnvCellCount);
|
||||||
|
|
||||||
|
LandblockCollisionBuild revisitCollision =
|
||||||
|
FlatCellClosure(firstBundle, firstCellId);
|
||||||
|
Publish(
|
||||||
|
fixture.Publisher,
|
||||||
|
Build(
|
||||||
|
FirstLandblock,
|
||||||
|
bundle: firstBundle,
|
||||||
|
collisions: revisitCollision));
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
revisitCollision.CellStructures[firstCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(firstCellId));
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(2, fixture.Cache.FlatEnvCellCount);
|
||||||
|
|
||||||
|
fixture.Publisher.RemoveLandblock(FirstLandblock);
|
||||||
|
fixture.Publisher.RemoveLandblock(AdjacentLandblock);
|
||||||
|
|
||||||
|
Assert.Equal(0, fixture.Cache.CellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatEnvCellCount);
|
||||||
|
Assert.Equal(0, fixture.Engine.LandblockCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DualPublication_CancelledPreparedReceiptPublishesNeitherView()
|
||||||
|
{
|
||||||
|
const uint envCellId = 0xA9B40100u;
|
||||||
|
var fixture = Fixture();
|
||||||
|
PhysicsDatBundle bundle =
|
||||||
|
CellPortalAndBuildingBundle(envCellId);
|
||||||
|
CellStruct sourceCell =
|
||||||
|
bundle.Environments.Values.Single().Cells.Values.Single();
|
||||||
|
sourceCell.PhysicsBSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode
|
||||||
|
{
|
||||||
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
||||||
|
BoundingSphere = new Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Zero,
|
||||||
|
Radius = 10f,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
LandblockCollisionBuild collision =
|
||||||
|
FlatCellClosure(bundle, envCellId);
|
||||||
|
|
||||||
|
LandblockPhysicsPublication cancelled =
|
||||||
|
fixture.Publisher.PreparePublication(
|
||||||
|
RenderReceipt(Build(
|
||||||
|
FirstLandblock,
|
||||||
|
bundle: bundle,
|
||||||
|
collisions: collision)));
|
||||||
|
|
||||||
|
Assert.Equal(0, fixture.Engine.LandblockCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.CellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(0, fixture.Cache.FlatEnvCellCount);
|
||||||
|
Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||||
|
|
||||||
|
LandblockPhysicsPublication accepted =
|
||||||
|
fixture.Publisher.PreparePublication(
|
||||||
|
RenderReceipt(Build(
|
||||||
|
FirstLandblock,
|
||||||
|
bundle: bundle,
|
||||||
|
collisions: collision)));
|
||||||
|
fixture.Publisher.BeginPublication(accepted);
|
||||||
|
fixture.Publisher.CompletePublication(accepted);
|
||||||
|
|
||||||
|
Assert.Same(
|
||||||
|
collision.CellStructures[envCellId],
|
||||||
|
fixture.Cache.GetFlatCellStruct(envCellId));
|
||||||
|
Assert.Equal(1, fixture.Cache.CellStructCount);
|
||||||
|
Assert.Equal(1, fixture.Cache.FlatCellStructCount);
|
||||||
|
Assert.Equal(1, fixture.Cache.FlatEnvCellCount);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CompletePublication_PhysicsBspSuppressesSetupCylinderFallback()
|
public void CompletePublication_PhysicsBspSuppressesSetupCylinderFallback()
|
||||||
{
|
{
|
||||||
|
|
@ -592,6 +841,25 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static GfxObj PhysicsGfx() => new()
|
||||||
|
{
|
||||||
|
Flags = DatReaderWriter.Enums.GfxObjFlags.HasPhysics,
|
||||||
|
PhysicsBSP = new PhysicsBSPTree
|
||||||
|
{
|
||||||
|
Root = new PhysicsBSPNode
|
||||||
|
{
|
||||||
|
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
||||||
|
BoundingSphere = new Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Zero,
|
||||||
|
Radius = 1.25f,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
||||||
|
VertexArray = new VertexArray(),
|
||||||
|
};
|
||||||
|
|
||||||
private static PhysicsDatBundle CellPortalAndBuildingBundle(uint envCellId)
|
private static PhysicsDatBundle CellPortalAndBuildingBundle(uint envCellId)
|
||||||
{
|
{
|
||||||
var cellStruct = new CellStruct
|
var cellStruct = new CellStruct
|
||||||
|
|
@ -658,6 +926,33 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
new Dictionary<uint, GfxObj>());
|
new Dictionary<uint, GfxObj>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LandblockCollisionBuild FlatCellClosure(
|
||||||
|
PhysicsDatBundle bundle,
|
||||||
|
uint envCellId)
|
||||||
|
{
|
||||||
|
EnvCell envCell = bundle.EnvCells[envCellId];
|
||||||
|
DatReaderWriter.DBObjs.Environment environment =
|
||||||
|
bundle.Environments.Values.Single();
|
||||||
|
CellStruct cellStruct = environment.Cells[envCell.CellStructure];
|
||||||
|
FlatCellStructureCollisionAsset flatCell =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellStructure(cellStruct);
|
||||||
|
FlatEnvCellTopology topology =
|
||||||
|
FlatCollisionAssetBuilder.FlattenEnvCellTopology(
|
||||||
|
envCellId,
|
||||||
|
envCell,
|
||||||
|
flatCell.PortalPolygons);
|
||||||
|
return new LandblockCollisionBuild(
|
||||||
|
ImmutableDictionary<uint, FlatGfxObjCollisionAsset>.Empty,
|
||||||
|
ImmutableDictionary<uint, FlatSetupCollision>.Empty,
|
||||||
|
ImmutableDictionary<uint, FlatCellStructureCollisionAsset>.Empty
|
||||||
|
.Add(envCellId, flatCell),
|
||||||
|
ImmutableDictionary<uint, FlatEnvCellTopology>.Empty
|
||||||
|
.Add(envCellId, topology),
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[envCellId]);
|
||||||
|
}
|
||||||
|
|
||||||
private static WorldEntity CylinderEntity(
|
private static WorldEntity CylinderEntity(
|
||||||
uint id,
|
uint id,
|
||||||
Vector3 position,
|
Vector3 position,
|
||||||
|
|
@ -674,7 +969,8 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
IReadOnlyList<WorldEntity>? entities = null,
|
IReadOnlyList<WorldEntity>? entities = null,
|
||||||
PhysicsDatBundle? bundle = null,
|
PhysicsDatBundle? bundle = null,
|
||||||
LandblockBuildOrigin? origin = null)
|
LandblockBuildOrigin? origin = null,
|
||||||
|
LandblockCollisionBuild? collisions = null)
|
||||||
{
|
{
|
||||||
var landblockInfo = new LandBlockInfo();
|
var landblockInfo = new LandBlockInfo();
|
||||||
PhysicsDatBundle physics = bundle ?? new PhysicsDatBundle(
|
PhysicsDatBundle physics = bundle ?? new PhysicsDatBundle(
|
||||||
|
|
@ -689,7 +985,8 @@ public sealed class LandblockPhysicsPublisherTests
|
||||||
FlatHeightmap(),
|
FlatHeightmap(),
|
||||||
entities ?? Array.Empty<WorldEntity>(),
|
entities ?? Array.Empty<WorldEntity>(),
|
||||||
physics),
|
physics),
|
||||||
Origin: origin ?? new LandblockBuildOrigin(0xA9, 0xB4));
|
Origin: origin ?? new LandblockBuildOrigin(0xA9, 0xB4),
|
||||||
|
Collisions: collisions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LandBlock FlatHeightmap() => new()
|
private static LandBlock FlatHeightmap() => new()
|
||||||
|
|
|
||||||
294
tests/AcDream.Core.Tests/Physics/CollisionShadowVerifierTests.cs
Normal file
294
tests/AcDream.Core.Tests/Physics/CollisionShadowVerifierTests.cs
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
public sealed class CollisionShadowVerifierTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MatchingDualView_RefereesEveryTraversalWithoutChangingGraph()
|
||||||
|
{
|
||||||
|
string directory = NewArtifactDirectory();
|
||||||
|
PhysicsDataCache cache = CacheWithShadow(directory);
|
||||||
|
CellPhysics cell = MatchingCell(0xA9B4_0100u);
|
||||||
|
cache.RegisterCellStructForTest(cell.SourceId, cell);
|
||||||
|
var transition = SeedTransition();
|
||||||
|
|
||||||
|
Assert.True(CollisionTraversal.HasCellContainment(cache, cell));
|
||||||
|
Assert.True(CollisionTraversal.HasPhysics(cache, cell));
|
||||||
|
FlatCollisionSphere root =
|
||||||
|
CollisionTraversal.RootBoundingSphere(cache, cell);
|
||||||
|
Assert.Equal(10f, root.Radius);
|
||||||
|
Assert.True(CollisionTraversal.PointInsideCell(
|
||||||
|
cache,
|
||||||
|
cell,
|
||||||
|
Vector3.Zero));
|
||||||
|
Assert.True(CollisionTraversal.SphereIntersectsCell(
|
||||||
|
cache,
|
||||||
|
cell,
|
||||||
|
Vector3.Zero,
|
||||||
|
0.48f));
|
||||||
|
TransitionState state = CollisionTraversal.FindCollisions(
|
||||||
|
cache,
|
||||||
|
cell,
|
||||||
|
transition,
|
||||||
|
Vector3.Zero,
|
||||||
|
0.48f,
|
||||||
|
false,
|
||||||
|
Vector3.Zero,
|
||||||
|
0f,
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.UnitZ,
|
||||||
|
1f,
|
||||||
|
Quaternion.Identity,
|
||||||
|
engine: null,
|
||||||
|
worldOrigin: Vector3.Zero);
|
||||||
|
|
||||||
|
Assert.Equal(TransitionState.OK, state);
|
||||||
|
CollisionShadowStats stats = cache.CollisionShadowStats;
|
||||||
|
Assert.Equal(6, stats.Queries);
|
||||||
|
Assert.Equal(6, stats.Samples);
|
||||||
|
Assert.Equal(6, stats.Matches);
|
||||||
|
Assert.Equal(0, stats.Mismatches);
|
||||||
|
Assert.Equal(0, stats.Faults);
|
||||||
|
Assert.Empty(Directory.EnumerateFiles(directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Mismatch_WritesDeterministicArtifactAndReturnsGraphResult()
|
||||||
|
{
|
||||||
|
string directory = NewArtifactDirectory();
|
||||||
|
PhysicsDataCache cache = CacheWithShadow(directory);
|
||||||
|
CellPhysics matching = MatchingCell(0xA9B4_0100u);
|
||||||
|
CellPhysics mismatched = WithFlatPhysics(
|
||||||
|
matching,
|
||||||
|
new FlatPhysicsBsp(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty));
|
||||||
|
|
||||||
|
bool graphResult = CollisionTraversal.HasPhysics(cache, mismatched);
|
||||||
|
|
||||||
|
Assert.True(graphResult);
|
||||||
|
CollisionShadowStats stats = cache.CollisionShadowStats;
|
||||||
|
Assert.Equal(1, stats.Mismatches);
|
||||||
|
Assert.Equal(0, stats.Faults);
|
||||||
|
string artifact = Assert.Single(Directory.EnumerateFiles(directory));
|
||||||
|
Assert.Equal(
|
||||||
|
"collision-shadow-00000001.json",
|
||||||
|
Path.GetFileName(artifact));
|
||||||
|
using JsonDocument json = JsonDocument.Parse(File.ReadAllText(artifact));
|
||||||
|
JsonElement root = json.RootElement;
|
||||||
|
Assert.Equal(1, root.GetProperty("SchemaVersion").GetInt32());
|
||||||
|
Assert.Equal("HasCellPhysics", root.GetProperty("Kind").GetString());
|
||||||
|
Assert.Equal("0xA9B40100", root.GetProperty("SourceId").GetString());
|
||||||
|
Assert.Equal("Result", root.GetProperty("Difference").GetString());
|
||||||
|
Assert.Equal("True", root.GetProperty("Graph").GetString());
|
||||||
|
Assert.Equal("False", root.GetProperty("Flat").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingFlat_IsRecordedAsFaultAndGraphRemainsAuthoritative()
|
||||||
|
{
|
||||||
|
string directory = NewArtifactDirectory();
|
||||||
|
PhysicsDataCache cache = CacheWithShadow(directory);
|
||||||
|
CellPhysics cell = WithoutFlatContainment(
|
||||||
|
MatchingCell(0xA9B4_0100u));
|
||||||
|
|
||||||
|
bool graphResult = CollisionTraversal.PointInsideCell(
|
||||||
|
cache,
|
||||||
|
cell,
|
||||||
|
Vector3.Zero);
|
||||||
|
|
||||||
|
Assert.True(graphResult);
|
||||||
|
Assert.Equal(1, cache.CollisionShadowStats.Faults);
|
||||||
|
string artifact = Assert.Single(Directory.EnumerateFiles(directory));
|
||||||
|
Assert.Contains(
|
||||||
|
"FlatFault",
|
||||||
|
File.ReadAllText(artifact),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TransitionCopyAndComparer_CoverCompleteLogicalState()
|
||||||
|
{
|
||||||
|
Transition source = SeedTransition();
|
||||||
|
source.ObjectInfo.State =
|
||||||
|
ObjectInfoState.Contact | ObjectInfoState.IsPlayer;
|
||||||
|
source.ObjectInfo.TargetId = 0x5000_0001u;
|
||||||
|
source.ObjectInfo.VelocityKilled = true;
|
||||||
|
source.SpherePath.NumSphere = 2;
|
||||||
|
source.SpherePath.LocalSphere[1].Origin = Vector3.UnitZ;
|
||||||
|
source.SpherePath.LocalSphere[1].Radius = 0.48f;
|
||||||
|
source.SpherePath.CarriedBlockOrigin = new Vector3(192f, -192f, 0f);
|
||||||
|
source.SpherePath.SetWalkable(
|
||||||
|
new Plane(Vector3.UnitZ, -1f),
|
||||||
|
[
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.UnitX,
|
||||||
|
Vector3.UnitY,
|
||||||
|
],
|
||||||
|
Vector3.UnitZ);
|
||||||
|
source.SpherePath.CellCandidates.Add(0xA9B4_0001u);
|
||||||
|
source.SpherePath.CellCandidates.Add(0xA9B4_0100u);
|
||||||
|
source.CollisionInfo.SetContactPlane(
|
||||||
|
new Plane(Vector3.UnitZ, -1f),
|
||||||
|
0xA9B4_0100u);
|
||||||
|
source.CollisionInfo.SetSlidingNormal(Vector3.UnitX);
|
||||||
|
source.CollisionInfo.CollideObjectGuids.Add(0x8000_0001u);
|
||||||
|
source.CollisionInfo.LastCollidedObjectGuid = 0x8000_0001u;
|
||||||
|
|
||||||
|
var clone = new Transition();
|
||||||
|
clone.CopyFrom(source);
|
||||||
|
|
||||||
|
Assert.False(TransitionExactComparer.TryFindDifference(
|
||||||
|
source,
|
||||||
|
clone,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
|
clone.SpherePath.WalkableVertices![1].X =
|
||||||
|
BitConverter.Int32BitsToSingle(
|
||||||
|
BitConverter.SingleToInt32Bits(
|
||||||
|
clone.SpherePath.WalkableVertices[1].X) + 1);
|
||||||
|
Assert.True(TransitionExactComparer.TryFindDifference(
|
||||||
|
source,
|
||||||
|
clone,
|
||||||
|
out string path,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
|
Assert.Equal("SpherePath.WalkableVertices[1]", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DisabledGraphTraversal_AllocatesZeroBytes()
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache
|
||||||
|
{
|
||||||
|
CollisionShadow = null,
|
||||||
|
};
|
||||||
|
CellPhysics cell = MatchingCell(0xA9B4_0100u);
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
_ = CollisionTraversal.HasPhysics(cache, cell);
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 10_000; i++)
|
||||||
|
_ = CollisionTraversal.HasPhysics(cache, cell);
|
||||||
|
long allocated =
|
||||||
|
GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PhysicsDataCache CacheWithShadow(string directory)
|
||||||
|
{
|
||||||
|
var cache = new PhysicsDataCache();
|
||||||
|
cache.CollisionShadow = new CollisionShadowVerifier(1, directory);
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CellPhysics MatchingCell(uint sourceId)
|
||||||
|
{
|
||||||
|
var physicsRoot = new PhysicsBSPNode
|
||||||
|
{
|
||||||
|
Type = BSPNodeType.Leaf,
|
||||||
|
BoundingSphere = new Sphere
|
||||||
|
{
|
||||||
|
Origin = Vector3.Zero,
|
||||||
|
Radius = 10f,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var containmentRoot = new CellBSPNode
|
||||||
|
{
|
||||||
|
Type = BSPNodeType.Leaf,
|
||||||
|
LeafIndex = 1,
|
||||||
|
};
|
||||||
|
var resolved = new Dictionary<ushort, ResolvedPolygon>();
|
||||||
|
return new CellPhysics
|
||||||
|
{
|
||||||
|
SourceId = sourceId,
|
||||||
|
BSP = new PhysicsBSPTree { Root = physicsRoot },
|
||||||
|
Resolved = resolved,
|
||||||
|
CellBSP = new CellBSPTree { Root = containmentRoot },
|
||||||
|
FlatPhysicsBsp =
|
||||||
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(
|
||||||
|
physicsRoot,
|
||||||
|
resolved),
|
||||||
|
FlatContainmentBsp =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellContainmentBsp(
|
||||||
|
containmentRoot),
|
||||||
|
WorldTransform = Matrix4x4.Identity,
|
||||||
|
InverseWorldTransform = Matrix4x4.Identity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transition SeedTransition()
|
||||||
|
{
|
||||||
|
var transition = new Transition();
|
||||||
|
transition.SpherePath.InitPath(
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.Zero,
|
||||||
|
0xA9B4_0100u,
|
||||||
|
0.48f);
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CellPhysics WithFlatPhysics(
|
||||||
|
CellPhysics source,
|
||||||
|
FlatPhysicsBsp flat) => new()
|
||||||
|
{
|
||||||
|
SourceId = source.SourceId,
|
||||||
|
BSP = source.BSP,
|
||||||
|
PhysicsPolygons = source.PhysicsPolygons,
|
||||||
|
Vertices = source.Vertices,
|
||||||
|
WorldTransform = source.WorldTransform,
|
||||||
|
InverseWorldTransform = source.InverseWorldTransform,
|
||||||
|
Resolved = source.Resolved,
|
||||||
|
FlatPhysicsBsp = flat,
|
||||||
|
CellBSP = source.CellBSP,
|
||||||
|
FlatContainmentBsp = source.FlatContainmentBsp,
|
||||||
|
FlatPortalPolygons = source.FlatPortalPolygons,
|
||||||
|
FlatTopology = source.FlatTopology,
|
||||||
|
Portals = source.Portals,
|
||||||
|
PortalPolygons = source.PortalPolygons,
|
||||||
|
VisibleCellIds = source.VisibleCellIds,
|
||||||
|
SeenOutside = source.SeenOutside,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static CellPhysics WithoutFlatContainment(
|
||||||
|
CellPhysics source) => new()
|
||||||
|
{
|
||||||
|
SourceId = source.SourceId,
|
||||||
|
BSP = source.BSP,
|
||||||
|
PhysicsPolygons = source.PhysicsPolygons,
|
||||||
|
Vertices = source.Vertices,
|
||||||
|
WorldTransform = source.WorldTransform,
|
||||||
|
InverseWorldTransform = source.InverseWorldTransform,
|
||||||
|
Resolved = source.Resolved,
|
||||||
|
FlatPhysicsBsp = source.FlatPhysicsBsp,
|
||||||
|
CellBSP = source.CellBSP,
|
||||||
|
FlatContainmentBsp = null,
|
||||||
|
FlatPortalPolygons = source.FlatPortalPolygons,
|
||||||
|
FlatTopology = source.FlatTopology,
|
||||||
|
Portals = source.Portals,
|
||||||
|
PortalPolygons = source.PortalPolygons,
|
||||||
|
VisibleCellIds = source.VisibleCellIds,
|
||||||
|
SeenOutside = source.SeenOutside,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string NewArtifactDirectory()
|
||||||
|
{
|
||||||
|
string directory = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
"acdream-collision-shadow-tests",
|
||||||
|
Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,8 @@ param(
|
||||||
[switch]$DenseTown,
|
[switch]$DenseTown,
|
||||||
[switch]$CaptureContention,
|
[switch]$CaptureContention,
|
||||||
[switch]$SkipRuntimeCounters,
|
[switch]$SkipRuntimeCounters,
|
||||||
[int]$LoginTimeoutSeconds = 90
|
[int]$LoginTimeoutSeconds = 90,
|
||||||
|
[int]$CollisionShadowEvery = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
|
|
@ -67,6 +68,7 @@ $markerLog = "$prefix.markers.log"
|
||||||
$sampleCsv = "$prefix.samples.csv"
|
$sampleCsv = "$prefix.samples.csv"
|
||||||
$reportJson = "$prefix.report.json"
|
$reportJson = "$prefix.report.json"
|
||||||
$artifactDir = "$prefix.artifacts"
|
$artifactDir = "$prefix.artifacts"
|
||||||
|
$collisionShadowDir = Join-Path $artifactDir 'collision-shadow'
|
||||||
$checkpointTimeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
$checkpointTimeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
||||||
$frameHistory = Join-Path $artifactDir 'frame-history.csv'
|
$frameHistory = Join-Path $artifactDir 'frame-history.csv'
|
||||||
$frameSummary = Join-Path $artifactDir 'frame-history-summary.json'
|
$frameSummary = Join-Path $artifactDir 'frame-history-summary.json'
|
||||||
|
|
@ -440,6 +442,26 @@ function Add-CanonicalCheckpointFailures([Diagnostics.Process]$Client) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($CollisionShadowEvery -gt 0) {
|
||||||
|
$shadow = $checkpoint.resources.PSObject.Properties['collisionShadow']
|
||||||
|
if ($null -eq $shadow) {
|
||||||
|
$failures.Add("canonical checkpoint '$expectedName' is missing resources.collisionShadow")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($field in @('mismatches', 'faults')) {
|
||||||
|
$property = $shadow.Value.PSObject.Properties[$field]
|
||||||
|
if ($null -eq $property) {
|
||||||
|
$failures.Add(
|
||||||
|
"canonical checkpoint '$expectedName' is missing resources.collisionShadow.$field")
|
||||||
|
}
|
||||||
|
elseif ([long]$property.Value -ne 0) {
|
||||||
|
$failures.Add(
|
||||||
|
"canonical checkpoint '$expectedName' has resources.collisionShadow.$field=$($property.Value), expected zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$streamingWork =
|
$streamingWork =
|
||||||
$checkpoint.resources.PSObject.Properties['streamingWork']
|
$checkpoint.resources.PSObject.Properties['streamingWork']
|
||||||
if ($null -eq $streamingWork) {
|
if ($null -eq $streamingWork) {
|
||||||
|
|
@ -753,6 +775,10 @@ $env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $artifactDir
|
||||||
$env:ACDREAM_DUMP_MOVE_TRUTH = '1'
|
$env:ACDREAM_DUMP_MOVE_TRUTH = '1'
|
||||||
$env:ACDREAM_NO_AUDIO = $null
|
$env:ACDREAM_NO_AUDIO = $null
|
||||||
$env:ACDREAM_WB_DIAG = $null
|
$env:ACDREAM_WB_DIAG = $null
|
||||||
|
$env:ACDREAM_COLLISION_SHADOW_EVERY =
|
||||||
|
if ($CollisionShadowEvery -gt 0) { "$CollisionShadowEvery" } else { $null }
|
||||||
|
$env:ACDREAM_COLLISION_SHADOW_DIR =
|
||||||
|
if ($CollisionShadowEvery -gt 0) { $collisionShadowDir } else { $null }
|
||||||
|
|
||||||
# Machine-readable disclosure of the exact launch configuration (2026-07-24
|
# Machine-readable disclosure of the exact launch configuration (2026-07-24
|
||||||
# measurement-tooling review — the prior audit could only see
|
# measurement-tooling review — the prior audit could only see
|
||||||
|
|
@ -781,6 +807,7 @@ $envDisclosure = [pscustomobject][ordered]@{
|
||||||
DenseTown = [bool]$DenseTown
|
DenseTown = [bool]$DenseTown
|
||||||
CaptureContention = [bool]$CaptureContention
|
CaptureContention = [bool]$CaptureContention
|
||||||
RuntimeCounters = -not [bool]$SkipRuntimeCounters
|
RuntimeCounters = -not [bool]$SkipRuntimeCounters
|
||||||
|
CollisionShadowEvery = $CollisionShadowEvery
|
||||||
Route = $routeFileName
|
Route = $routeFileName
|
||||||
EnvironmentVariables = @($acdreamEnvVars)
|
EnvironmentVariables = @($acdreamEnvVars)
|
||||||
}
|
}
|
||||||
|
|
@ -897,6 +924,25 @@ try {
|
||||||
30
|
30
|
||||||
$canonicalCheckpoints = @(Read-CanonicalCheckpoints)
|
$canonicalCheckpoints = @(Read-CanonicalCheckpoints)
|
||||||
Add-CanonicalCheckpointFailures $process
|
Add-CanonicalCheckpointFailures $process
|
||||||
|
if ($CollisionShadowEvery -gt 0 -and $canonicalCheckpoints.Count -gt 0) {
|
||||||
|
$lastShadow =
|
||||||
|
$canonicalCheckpoints[-1].resources.PSObject.Properties['collisionShadow']
|
||||||
|
$shadowSamples = if ($null -ne $lastShadow) {
|
||||||
|
$lastShadow.Value.PSObject.Properties['samples']
|
||||||
|
}
|
||||||
|
if ($null -eq $shadowSamples -or [long]$shadowSamples.Value -le 0) {
|
||||||
|
$failures.Add('collision shadow did not sample any query')
|
||||||
|
}
|
||||||
|
$mismatchArtifacts = @(
|
||||||
|
Get-ChildItem -LiteralPath $collisionShadowDir `
|
||||||
|
-Filter 'collision-shadow-*.json' `
|
||||||
|
-File `
|
||||||
|
-ErrorAction SilentlyContinue)
|
||||||
|
if ($mismatchArtifacts.Count -ne 0) {
|
||||||
|
$failures.Add(
|
||||||
|
"collision shadow emitted $($mismatchArtifacts.Count) mismatch artifact(s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (-not $DenseTown) {
|
if (-not $DenseTown) {
|
||||||
Add-RelativeGateFailures
|
Add-RelativeGateFailures
|
||||||
}
|
}
|
||||||
|
|
@ -980,6 +1026,7 @@ finally {
|
||||||
BinaryCommit = $binaryCommit
|
BinaryCommit = $binaryCommit
|
||||||
BinaryMatchesSource = $binaryMatchesSource
|
BinaryMatchesSource = $binaryMatchesSource
|
||||||
SessionName = $env:SESSIONNAME
|
SessionName = $env:SESSIONNAME
|
||||||
|
CollisionShadowEvery = $CollisionShadowEvery
|
||||||
ExitCode = $exitCode
|
ExitCode = $exitCode
|
||||||
GracefulExit = $gracefulExit
|
GracefulExit = $gracefulExit
|
||||||
Failures = @($failures)
|
Failures = @($failures)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ param(
|
||||||
[string]$Password = $env:ACDREAM_TEST_PASS,
|
[string]$Password = $env:ACDREAM_TEST_PASS,
|
||||||
[string]$AceLogPath = 'C:\ACE\Server\ACE_Log.txt',
|
[string]$AceLogPath = 'C:\ACE\Server\ACE_Log.txt',
|
||||||
[switch]$SkipBuild,
|
[switch]$SkipBuild,
|
||||||
[int]$SessionTimeoutSeconds = 420
|
[int]$SessionTimeoutSeconds = 420,
|
||||||
|
[int]$CollisionShadowEvery = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
|
|
@ -178,6 +179,24 @@ function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
|
||||||
if ($null -eq $resources.lastFrameProfile) {
|
if ($null -eq $resources.lastFrameProfile) {
|
||||||
$failures.Add("${SessionLabel}/${name}: no frame-profiler sample was available")
|
$failures.Add("${SessionLabel}/${name}: no frame-profiler sample was available")
|
||||||
}
|
}
|
||||||
|
if ($CollisionShadowEvery -gt 0) {
|
||||||
|
$shadow = $resources.PSObject.Properties['collisionShadow']
|
||||||
|
if ($null -eq $shadow) {
|
||||||
|
$failures.Add("${SessionLabel}/${name}: collision shadow counters are missing")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($field in @('mismatches', 'faults')) {
|
||||||
|
$property = $shadow.Value.PSObject.Properties[$field]
|
||||||
|
if ($null -eq $property) {
|
||||||
|
$failures.Add("${SessionLabel}/${name}: collisionShadow.$field is missing")
|
||||||
|
}
|
||||||
|
elseif ([long]$property.Value -ne 0) {
|
||||||
|
$failures.Add(
|
||||||
|
"${SessionLabel}/${name}: collisionShadow.$field=$($property.Value), expected zero")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-Session(
|
function Invoke-Session(
|
||||||
|
|
@ -193,6 +212,7 @@ function Invoke-Session(
|
||||||
$stdout = Join-Path $sessionDir 'stdout.log'
|
$stdout = Join-Path $sessionDir 'stdout.log'
|
||||||
$stderr = Join-Path $sessionDir 'stderr.log'
|
$stderr = Join-Path $sessionDir 'stderr.log'
|
||||||
$timeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
$timeline = Join-Path $artifactDir 'world-lifecycle.checkpoints.jsonl'
|
||||||
|
$collisionShadowDir = Join-Path $artifactDir 'collision-shadow'
|
||||||
$client = $null
|
$client = $null
|
||||||
$clientPort = $null
|
$clientPort = $null
|
||||||
$graceful = $false
|
$graceful = $false
|
||||||
|
|
@ -215,6 +235,10 @@ function Invoke-Session(
|
||||||
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $artifactDir
|
$env:ACDREAM_AUTOMATION_ARTIFACT_DIR = $artifactDir
|
||||||
$env:ACDREAM_DUMP_MOVE_TRUTH = $null
|
$env:ACDREAM_DUMP_MOVE_TRUTH = $null
|
||||||
$env:ACDREAM_WB_DIAG = $null
|
$env:ACDREAM_WB_DIAG = $null
|
||||||
|
$env:ACDREAM_COLLISION_SHADOW_EVERY =
|
||||||
|
if ($CollisionShadowEvery -gt 0) { "$CollisionShadowEvery" } else { $null }
|
||||||
|
$env:ACDREAM_COLLISION_SHADOW_DIR =
|
||||||
|
if ($CollisionShadowEvery -gt 0) { $collisionShadowDir } else { $null }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$client = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
$client = Start-Process -FilePath $exe -WorkingDirectory $Repository `
|
||||||
|
|
@ -248,6 +272,24 @@ function Invoke-Session(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach ($checkpoint in $checkpoints) { Validate-Checkpoint $Label $checkpoint }
|
foreach ($checkpoint in $checkpoints) { Validate-Checkpoint $Label $checkpoint }
|
||||||
|
if ($CollisionShadowEvery -gt 0 -and $checkpoints.Count -gt 0) {
|
||||||
|
$lastShadow = $checkpoints[-1].resources.PSObject.Properties['collisionShadow']
|
||||||
|
$samples = if ($null -ne $lastShadow) {
|
||||||
|
$lastShadow.Value.PSObject.Properties['samples']
|
||||||
|
}
|
||||||
|
if ($null -eq $samples -or [long]$samples.Value -le 0) {
|
||||||
|
$failures.Add("${Label}: collision shadow did not sample any query")
|
||||||
|
}
|
||||||
|
$mismatchArtifacts = @(
|
||||||
|
Get-ChildItem -LiteralPath $collisionShadowDir `
|
||||||
|
-Filter 'collision-shadow-*.json' `
|
||||||
|
-File `
|
||||||
|
-ErrorAction SilentlyContinue)
|
||||||
|
if ($mismatchArtifacts.Count -ne 0) {
|
||||||
|
$failures.Add(
|
||||||
|
"${Label}: collision shadow emitted $($mismatchArtifacts.Count) mismatch artifact(s)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($name in $ExpectedScreenshots) {
|
foreach ($name in $ExpectedScreenshots) {
|
||||||
$png = Join-Path $artifactDir "screenshots\$name.png"
|
$png = Join-Path $artifactDir "screenshots\$name.png"
|
||||||
|
|
@ -373,6 +415,7 @@ $report = [pscustomobject][ordered]@{
|
||||||
Commit = (& git -C $Repository rev-parse HEAD).Trim()
|
Commit = (& git -C $Repository rev-parse HEAD).Trim()
|
||||||
SourceStatus = @(& git -C $Repository status --short)
|
SourceStatus = @(& git -C $Repository status --short)
|
||||||
SessionName = $env:SESSIONNAME
|
SessionName = $env:SESSIONNAME
|
||||||
|
CollisionShadowEvery = $CollisionShadowEvery
|
||||||
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
|
VideoControllers = @(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue |
|
||||||
ForEach-Object { [pscustomobject]@{
|
ForEach-Object { [pscustomobject]@{
|
||||||
Name = $_.Name
|
Name = $_.Name
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue