feat(content): bake and read flat collision assets
Append strict collision/topology payloads to the existing prepared package so later physics cutover can drop parsed DAT graphs without adding a second mapping or changing traversal behavior. The full 2,232,170-key catalog is deterministic across worker counts, exact-byte aliased, corruption-isolated, and cancellation-safe.
This commit is contained in:
parent
d9300c7854
commit
9cd42417a8
29 changed files with 2868 additions and 63 deletions
12
AGENTS.md
12
AGENTS.md
|
|
@ -163,17 +163,23 @@ fresh-vs-reused results and complete reset/reentrancy gates. Slice I2 is also
|
||||||
complete: immutable physics/containment BSP, polygon, Setup, bounds, and
|
complete: immutable physics/containment BSP, polygon, Setup, bounds, and
|
||||||
EnvCell-topology records preserve source float bits and ordering across
|
EnvCell-topology records preserve source float bits and ordering across
|
||||||
synthetic corruption gates and representative installed DATs without changing
|
synthetic corruption gates and representative installed DATs without changing
|
||||||
production traversal.
|
production traversal. Slice I3 is complete: bake-tool 4 appends typed GfxObj,
|
||||||
|
Setup, CellStruct, and EnvCell-topology collision payloads; the collision
|
||||||
|
interface shares the render package's single mmap. Two complete
|
||||||
|
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
||||||
|
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
||||||
|
package, Release build, and 8,371-test / 5-skip gates 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 I3 from
|
passes. Slice I is active at I4 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:
|
||||||
`docs/research/2026-07-25-slice-h-closeout.md` and
|
`docs/research/2026-07-25-slice-h-closeout.md` and
|
||||||
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
||||||
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
||||||
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md`.
|
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.md`.
|
||||||
|
|
||||||
**Structural prerequisite before new M4 subsystem work:** all eight
|
**Structural prerequisite before new M4 subsystem work:** all eight
|
||||||
behavior-preserving `GameWindow` decomposition slices and the automated
|
behavior-preserving `GameWindow` decomposition slices and the automated
|
||||||
|
|
|
||||||
12
CLAUDE.md
12
CLAUDE.md
|
|
@ -161,17 +161,23 @@ fresh-vs-reused results and complete reset/reentrancy gates. Slice I2 is also
|
||||||
complete: immutable physics/containment BSP, polygon, Setup, bounds, and
|
complete: immutable physics/containment BSP, polygon, Setup, bounds, and
|
||||||
EnvCell-topology records preserve source float bits and ordering across
|
EnvCell-topology records preserve source float bits and ordering across
|
||||||
synthetic corruption gates and representative installed DATs without changing
|
synthetic corruption gates and representative installed DATs without changing
|
||||||
production traversal.
|
production traversal. Slice I3 is complete: bake-tool 4 appends typed GfxObj,
|
||||||
|
Setup, CellStruct, and EnvCell-topology collision payloads; the collision
|
||||||
|
interface shares the render package's single mmap. Two complete
|
||||||
|
29,908,271,024-byte bakes produced 2,232,170 typed keys, zero failures, and the
|
||||||
|
same SHA-256 across 16 and 7 workers. Corruption, cancellation, installed
|
||||||
|
package, Release build, and 8,371-test / 5-skip gates 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 I3 from
|
passes. Slice I is active at I4 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:
|
||||||
`docs/research/2026-07-25-slice-h-closeout.md` and
|
`docs/research/2026-07-25-slice-h-closeout.md` and
|
||||||
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
`docs/research/2026-07-25-slice-g5-production-profile.md` and
|
||||||
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
`docs/research/2026-07-25-slice-i1-transition-scratch.md` and
|
||||||
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md`.
|
`docs/research/2026-07-25-slice-i2-flat-collision-schema.md` and
|
||||||
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.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
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,8 @@ src/
|
||||||
TerrainSurface.cs -> triangle-aware terrain contact (done)
|
TerrainSurface.cs -> triangle-aware terrain contact (done)
|
||||||
BSPQuery.cs -> partial retail BSP dispatcher (active in L.2)
|
BSPQuery.cs -> partial retail BSP dispatcher (active in L.2)
|
||||||
TransitionTypes.cs -> SpherePath / CollisionInfo / transition helpers (active in L.2)
|
TransitionTypes.cs -> SpherePath / CollisionInfo / transition helpers (active in L.2)
|
||||||
|
FlatCollisionAssets.cs -> immutable array/index collision schema (Slice I2)
|
||||||
|
FlatCollisionAssetBuilder.cs -> deterministic DAT graph preparation (Slice I2/I3)
|
||||||
PhysicsDataCache.cs -> GfxObj / Setup / CellStruct collision data (done, active)
|
PhysicsDataCache.cs -> GfxObj / Setup / CellStruct collision data (done, active)
|
||||||
ShadowObjectRegistry.cs -> broadphase for nearby physics objects (active)
|
ShadowObjectRegistry.cs -> broadphase for nearby physics objects (active)
|
||||||
PhysicsEngine.cs -> ResolveWithTransition active player path
|
PhysicsEngine.cs -> ResolveWithTransition active player path
|
||||||
|
|
|
||||||
|
|
@ -120,8 +120,9 @@ EnvCell, Surface, palette, and texture graphs during portals. The validated
|
||||||
machine-local `acdream.pak` is opened through Content's
|
machine-local `acdream.pak` is opened through Content's
|
||||||
`IPreparedAssetSource`; typed GfxObj and EnvCell requests deserialize immutable
|
`IPreparedAssetSource`; typed GfxObj and EnvCell requests deserialize immutable
|
||||||
`ObjectMeshData` while retaining the existing App worker, staging, render-thread
|
`ObjectMeshData` while retaining the existing App worker, staging, render-thread
|
||||||
upload, cache, ownership, and shutdown contracts. The format-1/bake-tool-3
|
upload, cache, ownership, and shutdown contracts. The original
|
||||||
payload persists exact batch translucency so App does not reconstruct a
|
format-1/bake-tool-3 render payload persists exact batch translucency so App
|
||||||
|
does not reconstruct a
|
||||||
`GfxObjMesh` for metadata. Setup activation uses the package TOC as an explicit
|
`GfxObjMesh` for metadata. Setup activation uses the package TOC as an explicit
|
||||||
type-presence index before reading valid Setup records through the bounded DAT
|
type-presence index before reading valid Setup records through the bounded DAT
|
||||||
cache. `DatPreparedAssetSource` and `MeshExtractor` remain explicit
|
cache. `DatPreparedAssetSource` and `MeshExtractor` remain explicit
|
||||||
|
|
@ -131,6 +132,18 @@ bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes
|
||||||
installed-DAT gates are recorded in
|
installed-DAT gates are recorded in
|
||||||
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`.
|
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`.
|
||||||
|
|
||||||
|
**Slice I3 prepared collision extension (2026-07-25).** The package remains
|
||||||
|
format 1 and retains mesh type values 1–3; bake-tool 4 appends typed GfxObj,
|
||||||
|
Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the
|
||||||
|
immutable flat records and deterministic raw-DAT flattener. Content owns the
|
||||||
|
strict little-endian codec and `IPreparedCollisionSource`.
|
||||||
|
`PakPreparedAssetSource` implements the render and collision interfaces over
|
||||||
|
one mmap; it does not create a second DAT reader or mapping. CellStruct
|
||||||
|
payloads alias only after exact serialized-byte comparison, while each
|
||||||
|
EnvCell topology remains independently keyed. Production traversal is still
|
||||||
|
the parsed graph oracle until Slice I6. Full-catalog evidence:
|
||||||
|
`docs/research/2026-07-25-slice-i3-prepared-collision-package.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
|
||||||
|
|
|
||||||
|
|
@ -1660,7 +1660,7 @@ port in any phase — no separate listing here.
|
||||||
> `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 I3 from
|
> connected lifecycle/reconnect gate. Slice I is active at I4 from
|
||||||
> [its detailed plan](2026-07-25-modern-runtime-slice-i.md).
|
> [its detailed plan](2026-07-25-modern-runtime-slice-i.md).
|
||||||
|
|
||||||
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
**Spec:** `docs/superpowers/specs/2026-07-05-modern-pipeline-design.md` (the
|
||||||
|
|
@ -1685,11 +1685,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 is defined and package integration continues in Slice I3. |
|
| MP-Alloc (hard sites) | EnvCell settled-camera rebuild gate + physics `Transition` reuse | 🟡 Physics half SHIPPED 2026-07-25 — retail-shaped reset-complete transition/query scratch is bit-identical and 0 B/resolve across five profiles; immutable flat collision storage and its complete deterministic prepared package are shipped; traversal differential work continues in Slice I4. |
|
||||||
| 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 and I2 immutable flat collision schema shipped; I3 package serialization/bake is active. |
|
| MP4 | Zero-alloc frame loop + flat physics data (residual, post-MP-Alloc) | 🟡 ACTIVE — Slice I1 zero-allocation transition scratch, I2 immutable schema, and I3 complete deterministic collision package shipped; I4 flat traversal shadow implementation is active. |
|
||||||
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
| MP5 | Job-system parallelism | ⚪ stretch, evidence-gated |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,13 @@ deterministic immutable physics/containment BSP, polygon, Setup, bounds, and
|
||||||
EnvCell-topology records without cutting traversal over. Synthetic corruption
|
EnvCell-topology records without cutting traversal over. Synthetic corruption
|
||||||
gates and representative installed-DAT source identity pass bit-for-bit. The
|
gates and representative installed-DAT source identity pass bit-for-bit. The
|
||||||
complete I2 Release gate is 3,826 App tests / 3 skips and 8,355 solution tests
|
complete I2 Release gate is 3,826 App tests / 3 skips and 8,355 solution tests
|
||||||
/ 5 skips.
|
/ 5 skips. I3 appends four collision package types and a separate typed
|
||||||
|
prepared-collision interface over the existing single mmap. Two complete
|
||||||
|
29,908,271,024-byte bakes contain 2,232,170 keys with zero failures and the
|
||||||
|
same SHA-256 across different worker counts; cancellation/corruption gates and
|
||||||
|
8,371 solution tests / 5 skips pass.
|
||||||
Slice H is complete: retained UI/frame work, exact bounded light selection,
|
Slice H is complete: retained UI/frame work, exact bounded light selection,
|
||||||
and pooled/borrowed/direct network I/O pass. Slice I is active at I3 under
|
and pooled/borrowed/direct network I/O pass. Slice I is active at I4 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`.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
**Date:** 2026-07-24
|
**Date:** 2026-07-24
|
||||||
|
|
||||||
**Status:** Slices A–H and I0–I2 are complete. Slices F–L were explicitly
|
**Status:** Slices A–H and I0–I3 are complete. Slices F–L were explicitly
|
||||||
approved 2026-07-24; Slice I3 is active.
|
approved 2026-07-24; Slice I4 is active.
|
||||||
|
|
||||||
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
**Scope:** Reconcile and sequence the existing Modern Pipeline (`MP`) and
|
||||||
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
Linux/headless (`LH`) tracks using the 2026-07-24 connected performance audit.
|
||||||
|
|
@ -1200,8 +1200,10 @@ grounded walkable-publication profiles each measure 0 B/resolve, while
|
||||||
fresh-vs-reused output and body state remain bit-identical. I2 now supplies
|
fresh-vs-reused output and body state remain bit-identical. I2 now supplies
|
||||||
validated immutable flat collision records: synthetic and installed-DAT
|
validated immutable flat collision records: synthetic and installed-DAT
|
||||||
source identity, exact float bits, deterministic pre-order, and corruption
|
source identity, exact float bits, deterministic pre-order, and corruption
|
||||||
tripwires pass without changing production traversal. I3 package
|
tripwires pass without changing production traversal. I3 now packages all
|
||||||
serialization and complete-catalog baking are the current execution point.
|
flat collision/topology records through a shared typed mmap source: two
|
||||||
|
complete 2,232,170-key bakes are byte-identical with zero failures. I4 flat
|
||||||
|
traversal shadow implementation is the current execution point.
|
||||||
Evidence:
|
Evidence:
|
||||||
[`../research/2026-07-25-slice-g5-production-profile.md`](../research/2026-07-25-slice-g5-production-profile.md)
|
[`../research/2026-07-25-slice-g5-production-profile.md`](../research/2026-07-25-slice-g5-production-profile.md)
|
||||||
and
|
and
|
||||||
|
|
@ -1209,6 +1211,8 @@ and
|
||||||
and
|
and
|
||||||
[`../research/2026-07-25-slice-i2-flat-collision-schema.md`](../research/2026-07-25-slice-i2-flat-collision-schema.md)
|
[`../research/2026-07-25-slice-i2-flat-collision-schema.md`](../research/2026-07-25-slice-i2-flat-collision-schema.md)
|
||||||
and
|
and
|
||||||
|
[`../research/2026-07-25-slice-i3-prepared-collision-package.md`](../research/2026-07-25-slice-i3-prepared-collision-package.md)
|
||||||
|
and
|
||||||
[`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 intended order is therefore:
|
The intended order is therefore:
|
||||||
|
|
@ -1222,7 +1226,7 @@ honest metrics + committed baselines (A — exit criteria block C)
|
||||||
-> incremental render scene (F)
|
-> incremental render scene (F)
|
||||||
-> delta GPU submission (G)
|
-> delta GPU submission (G)
|
||||||
-> residual frame cleanup (H-a, H-b, H-c)
|
-> residual frame cleanup (H-a, H-b, H-c)
|
||||||
-> flat collision assets (I — CURRENT at I3)
|
-> flat collision assets (I — CURRENT at I4)
|
||||||
-> presentation-independent runtime (J — §0.3 authorization)
|
-> presentation-independent runtime (J — §0.3 authorization)
|
||||||
-> Linux/headless/multi-session (K)
|
-> Linux/headless/multi-session (K)
|
||||||
-> evidence-gated GPU jobs (L)
|
-> evidence-gated GPU jobs (L)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
# Modern runtime Slice I — flat collision assets and zero-allocation physics
|
||||||
|
|
||||||
**Status:** I0–I2 COMPLETE — retail oracle, zero-allocation transition
|
**Status:** I0–I3 COMPLETE — retail oracle, zero-allocation transition
|
||||||
scratch, and deterministic immutable collision records fixed; I3 active
|
scratch, deterministic immutable collision records, and complete prepared
|
||||||
|
package fixed; I4 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
|
||||||
|
|
@ -176,7 +177,12 @@ Evidence:
|
||||||
|
|
||||||
Gate: two independent bakes are byte-identical; every collision key required
|
Gate: two independent bakes are byte-identical; every collision key required
|
||||||
by the canonical route is present; a failed/cancelled bake cannot replace the
|
by the canonical route is present; a failed/cancelled bake cannot replace the
|
||||||
last good pak.
|
last good pak. **PASSED 2026-07-25:** two complete 29,908,271,024-byte bakes
|
||||||
|
with different worker counts produced the same SHA-256 and 2,232,170 typed
|
||||||
|
keys with zero failures. Strict serialization/corruption/cancellation gates,
|
||||||
|
representative installed-package reads, Release build, and 8,371 solution
|
||||||
|
tests / 5 skips pass. Production traversal remains graph-only. Evidence:
|
||||||
|
[`../research/2026-07-25-slice-i3-prepared-collision-package.md`](../research/2026-07-25-slice-i3-prepared-collision-package.md).
|
||||||
|
|
||||||
### I4 — flat traversal shadow implementation
|
### I4 — flat traversal shadow implementation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Slice I3 — prepared flat-collision package
|
||||||
|
|
||||||
|
**Date:** 2026-07-25
|
||||||
|
**Scope:** Modern Runtime Slice I3 only
|
||||||
|
**Behavioral boundary:** storage, serialization, bake, and prepared lookup;
|
||||||
|
production collision traversal remains on the parsed-DAT graph oracle.
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The existing format-1 `acdream.pak` now carries four appended asset types:
|
||||||
|
|
||||||
|
| Numeric type | Payload |
|
||||||
|
|---:|---|
|
||||||
|
| 4 | GfxObj collision BSP, polygon table, bounding sphere, and visual bounds |
|
||||||
|
| 5 | Setup cylinders, spheres, and movement dimensions |
|
||||||
|
| 6 | reusable CellStruct physics BSP, containment BSP, and portal polygons |
|
||||||
|
| 7 | independently keyed EnvCell portals, visibility, and `seen_outside` |
|
||||||
|
|
||||||
|
The existing mesh type values 1–3 did not change. Bake-tool identity advanced
|
||||||
|
from 3 to 4; the package binary container remains format 1.
|
||||||
|
|
||||||
|
`FlatCollisionAssetSerializer` is a strict little-endian codec. It:
|
||||||
|
|
||||||
|
- writes every `float` through `SingleToInt32Bits`;
|
||||||
|
- retains node, polygon, vertex, Setup-shape, portal, and visible-cell order;
|
||||||
|
- checks counts before allocation and validates combined byte requirements;
|
||||||
|
- checks cancellation during large rows;
|
||||||
|
- rejects wrong payload kinds, unsupported schema versions, non-zero reserved
|
||||||
|
bits, invalid enum/tree/range contracts, truncation, and trailing bytes;
|
||||||
|
- remains behind the package entry CRC, so a CRC or structural fault marks
|
||||||
|
only that typed entry corrupt.
|
||||||
|
|
||||||
|
`IPreparedCollisionSource` is separate from the render-only
|
||||||
|
`IPreparedAssetSource` API. `PakPreparedAssetSource` implements both over the
|
||||||
|
same `PakReader` and therefore maps the 29.9 GB immutable package once, not
|
||||||
|
once per subsystem. Render and collision counters are independent.
|
||||||
|
|
||||||
|
CellStruct collision geometry is addressed by every EnvCell source key. The
|
||||||
|
bake emits ordinary TOC aliases within the same source group and across groups
|
||||||
|
only after comparing the complete serialized payload bytes. Per-cell topology
|
||||||
|
is always written under its own key and is never inferred from shared render
|
||||||
|
geometry.
|
||||||
|
|
||||||
|
## Complete installed-DAT bake
|
||||||
|
|
||||||
|
Two independent full bakes used 16 and 7 workers:
|
||||||
|
|
||||||
|
| Metric | Result |
|
||||||
|
|---|---:|
|
||||||
|
| GfxObj mesh keys | 15,318 |
|
||||||
|
| Setup mesh keys | 5,935 |
|
||||||
|
| EnvCell mesh keys | 729,888 |
|
||||||
|
| GfxObj collision keys | 15,318 |
|
||||||
|
| Setup collision keys | 5,935 |
|
||||||
|
| CellStruct collision keys | 729,888 |
|
||||||
|
| EnvCell topology keys | 729,888 |
|
||||||
|
| Total typed keys | 2,232,170 |
|
||||||
|
| Physical blobs | 781,196 |
|
||||||
|
| Failures | 0 |
|
||||||
|
| File bytes | 29,908,271,024 |
|
||||||
|
| SHA-256 | `FEE8595D512DCB119C98B3D9911CDF9A905C97B624BCB826CEAF8DFD3F67269C` |
|
||||||
|
|
||||||
|
Both complete files have identical length and SHA-256. Exact-byte
|
||||||
|
deduplication produced:
|
||||||
|
|
||||||
|
- 9,363 physical GfxObj collision blobs behind 15,318 keys;
|
||||||
|
- 979 physical Setup collision blobs behind 5,935 keys;
|
||||||
|
- 2,596 physical CellStruct collision blobs behind 729,888 keys.
|
||||||
|
|
||||||
|
The 16-worker bake completed and validated in 107.5 seconds; the 7-worker bake
|
||||||
|
completed and validated in 80.5 seconds. Peak bake-process memory was
|
||||||
|
4.43–4.89 GB working set and 3.81–4.21 GB private. This is offline extraction
|
||||||
|
state and is not mapped as committed client memory; the production source
|
||||||
|
memory-maps the immutable file and pages only accessed ranges.
|
||||||
|
|
||||||
|
The validated bake-tool-4 package was installed at the machine-local DAT
|
||||||
|
location for subsequent Slice I gates.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- strict codec round-trip and exact-float-bit tests for all four payloads;
|
||||||
|
- every truncation of a representative payload rejected;
|
||||||
|
- wrong-kind, negative/impossible count, trailing-byte, CRC, and valid-CRC
|
||||||
|
structural-corruption tests;
|
||||||
|
- concurrent prepared reads and separate render/collision accounting;
|
||||||
|
- raw-DAT versus current cached-source preparation on representative installed
|
||||||
|
GfxObjs, Setups, and EnvCells;
|
||||||
|
- filtered 1/8- and 8/3-worker bake equivalence and exact alias tests;
|
||||||
|
- complete installed package reads for representative GfxObj, Setup,
|
||||||
|
CellStruct, and EnvCell topology keys;
|
||||||
|
- failed validation and timed cancellation preserve the last published file;
|
||||||
|
- Release build: zero warnings;
|
||||||
|
- complete Release suite: 8,371 passed / 5 skipped.
|
||||||
|
|
||||||
|
No divergence-register row changes: no gameplay decision or traversal route
|
||||||
|
changed. Slice I4 adds the flat traversal referee while the graph path remains
|
||||||
|
authoritative.
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="AcDream.Bake.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- NO Silk.NET / GL packages here — ever. acdream-bake is an offline CLI
|
<!-- NO Silk.NET / GL packages here — ever. acdream-bake is an offline CLI
|
||||||
that drives the GL-free AcDream.Content.MeshExtractor; it must not
|
that drives the GL-free AcDream.Content.MeshExtractor; it must not
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ public static class BakeArtifactValidator
|
||||||
public static void Validate(
|
public static void Validate(
|
||||||
string path,
|
string path,
|
||||||
in PakHeader expectedHeader,
|
in PakHeader expectedHeader,
|
||||||
int expectedTocCount)
|
int expectedTocCount,
|
||||||
|
IReadOnlyDictionary<PakAssetType, int>? expectedTypeCounts = null)
|
||||||
{
|
{
|
||||||
using var reader = new PakReader(path);
|
using var reader = new PakReader(path);
|
||||||
var actual = reader.Header;
|
var actual = reader.Header;
|
||||||
|
|
@ -35,5 +36,18 @@ public static class BakeArtifactValidator
|
||||||
$"{expectedTocCount}");
|
$"{expectedTocCount}");
|
||||||
|
|
||||||
reader.ValidateTocStructure();
|
reader.ValidateTocStructure();
|
||||||
|
if (expectedTypeCounts is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach ((PakAssetType type, int expectedCount) in expectedTypeCounts)
|
||||||
|
{
|
||||||
|
int actualCount = reader.CountEntries(type);
|
||||||
|
if (actualCount != expectedCount)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"bake catalog type {type} contains {actualCount} keys; " +
|
||||||
|
$"expected {expectedCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,12 @@ using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
using AcDream.Content.Pak;
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Options;
|
using DatReaderWriter.Options;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
|
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
|
||||||
|
|
||||||
namespace AcDream.Bake;
|
namespace AcDream.Bake;
|
||||||
|
|
@ -30,6 +33,16 @@ public sealed record BakeReport
|
||||||
public required int EnvCellKeys { get; init; }
|
public required int EnvCellKeys { get; init; }
|
||||||
public required int UniqueEnvCellGeometries { get; init; }
|
public required int UniqueEnvCellGeometries { get; init; }
|
||||||
public required int EnvCellAliases { get; init; }
|
public required int EnvCellAliases { get; init; }
|
||||||
|
public required int GfxObjCollisionKeys { get; init; }
|
||||||
|
public required int UniqueGfxObjCollisions { get; init; }
|
||||||
|
public required int GfxObjCollisionAliases { get; init; }
|
||||||
|
public required int SetupCollisionKeys { get; init; }
|
||||||
|
public required int UniqueSetupCollisions { get; init; }
|
||||||
|
public required int SetupCollisionAliases { get; init; }
|
||||||
|
public required int CellStructureCollisionKeys { get; init; }
|
||||||
|
public required int UniqueCellStructureCollisions { get; init; }
|
||||||
|
public required int CellStructureCollisionAliases { get; init; }
|
||||||
|
public required int EnvCellTopologyKeys { get; init; }
|
||||||
public required int SideStagedKeys { get; init; }
|
public required int SideStagedKeys { get; init; }
|
||||||
public int SideStagedDuplicateKeys { get; init; }
|
public int SideStagedDuplicateKeys { get; init; }
|
||||||
public required int PhysicalBlobs { get; init; }
|
public required int PhysicalBlobs { get; init; }
|
||||||
|
|
@ -40,6 +53,11 @@ public sealed record BakeReport
|
||||||
public required long OutputBytes { get; init; }
|
public required long OutputBytes { get; init; }
|
||||||
public required long PeakWorkingSetBytes { get; init; }
|
public required long PeakWorkingSetBytes { get; init; }
|
||||||
public required long PeakPrivateBytes { get; init; }
|
public required long PeakPrivateBytes { get; init; }
|
||||||
|
public required IReadOnlyDictionary<PakAssetType, int> TypeCounts
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
init;
|
||||||
|
}
|
||||||
|
|
||||||
public double EnvCellDedupRatio =>
|
public double EnvCellDedupRatio =>
|
||||||
UniqueEnvCellGeometries == 0 ? 0 : (double)EnvCellKeys / UniqueEnvCellGeometries;
|
UniqueEnvCellGeometries == 0 ? 0 : (double)EnvCellKeys / UniqueEnvCellGeometries;
|
||||||
|
|
@ -80,7 +98,8 @@ public static class BakeRunner
|
||||||
(temporaryPath, result) => BakeArtifactValidator.Validate(
|
(temporaryPath, result) => BakeArtifactValidator.Validate(
|
||||||
temporaryPath,
|
temporaryPath,
|
||||||
result.Header,
|
result.Header,
|
||||||
result.TotalKeys),
|
result.TotalKeys,
|
||||||
|
result.TypeCounts),
|
||||||
options.CancellationToken);
|
options.CancellationToken);
|
||||||
totalStopwatch.Stop();
|
totalStopwatch.Stop();
|
||||||
|
|
||||||
|
|
@ -371,9 +390,402 @@ public static class BakeRunner
|
||||||
sideStagedWritten++;
|
sideStagedWritten++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- immutable flat collision payloads ------------------------
|
||||||
|
|
||||||
|
uint[] collisionGfxIds = gfxObjIds
|
||||||
|
.Concat(
|
||||||
|
sideStagedByKey.Keys.Select(
|
||||||
|
key => PakKey.Decompose(key).FileId))
|
||||||
|
.Distinct()
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
|
var collisionWork =
|
||||||
|
new List<(ulong Key, PakAssetType Type, uint FileId)>(
|
||||||
|
collisionGfxIds.Length + setupIds.Count);
|
||||||
|
collisionWork.AddRange(
|
||||||
|
collisionGfxIds.Select(
|
||||||
|
id => (
|
||||||
|
PakKey.Compose(PakAssetType.GfxObjCollision, id),
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
id)));
|
||||||
|
collisionWork.AddRange(
|
||||||
|
setupIds.Select(
|
||||||
|
id => (
|
||||||
|
PakKey.Compose(PakAssetType.SetupCollision, id),
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
id)));
|
||||||
|
collisionWork.Sort((left, right) => left.Key.CompareTo(right.Key));
|
||||||
|
|
||||||
|
var gfxCollisionAliases = new ExactPayloadAliasCatalog();
|
||||||
|
var setupCollisionAliases = new ExactPayloadAliasCatalog();
|
||||||
|
var cellStructureAliases = new ExactPayloadAliasCatalog();
|
||||||
|
int gfxCollisionWritten = 0;
|
||||||
|
int uniqueGfxCollisions = 0;
|
||||||
|
int gfxCollisionAliasCount = 0;
|
||||||
|
int setupCollisionWritten = 0;
|
||||||
|
int uniqueSetupCollisions = 0;
|
||||||
|
int setupCollisionAliasCount = 0;
|
||||||
|
int cellStructureWritten = 0;
|
||||||
|
int uniqueCellStructures = 0;
|
||||||
|
int cellStructureAliasCount = 0;
|
||||||
|
int envCellTopologyWritten = 0;
|
||||||
|
long collisionCompleted = 0;
|
||||||
|
int totalCollisionJobs =
|
||||||
|
collisionWork.Count
|
||||||
|
+ envCatalog.UniqueGeometryCount
|
||||||
|
+ envCatalog.CellCount;
|
||||||
|
var collisionStopwatch = Stopwatch.StartNew();
|
||||||
|
lastProgressReport.Restart();
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine(
|
||||||
|
$"collision catalog: {collisionGfxIds.Length:N0} GfxObj, " +
|
||||||
|
$"{setupIds.Count:N0} Setup, " +
|
||||||
|
$"{envCatalog.UniqueGeometryCount:N0} CellStruct source groups, " +
|
||||||
|
$"{envCatalog.CellCount:N0} EnvCell topology records");
|
||||||
|
|
||||||
|
for (int batchStart = 0;
|
||||||
|
batchStart < collisionWork.Count;
|
||||||
|
batchStart += BatchSize)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var batch = collisionWork
|
||||||
|
.Skip(batchStart)
|
||||||
|
.Take(BatchSize)
|
||||||
|
.ToArray();
|
||||||
|
var batchResults =
|
||||||
|
new ConcurrentBag<CollisionPayloadResult>();
|
||||||
|
|
||||||
|
Parallel.ForEach(
|
||||||
|
batch,
|
||||||
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = options.Threads,
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
},
|
||||||
|
item =>
|
||||||
|
{
|
||||||
|
var (key, type, fileId) = item;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] payload;
|
||||||
|
if (type == PakAssetType.GfxObjCollision)
|
||||||
|
{
|
||||||
|
if (!datReaderWriter.Portal.TryGet<GfxObj>(
|
||||||
|
fileId,
|
||||||
|
out GfxObj? gfxObj)
|
||||||
|
|| gfxObj is null)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
type,
|
||||||
|
fileId,
|
||||||
|
"GfxObj DAT record was not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatGfxObjCollisionAsset asset =
|
||||||
|
FlatCollisionAssetBuilder.FlattenGfxObj(
|
||||||
|
gfxObj);
|
||||||
|
payload = FlatCollisionAssetSerializer.Serialize(
|
||||||
|
asset,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!datReaderWriter.Portal.TryGet<Setup>(
|
||||||
|
fileId,
|
||||||
|
out Setup? setup)
|
||||||
|
|| setup is null)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
type,
|
||||||
|
fileId,
|
||||||
|
"Setup DAT record was not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatSetupCollision asset =
|
||||||
|
FlatCollisionAssetBuilder.FlattenSetup(
|
||||||
|
setup);
|
||||||
|
payload = FlatCollisionAssetSerializer.Serialize(
|
||||||
|
asset,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
batchResults.Add(
|
||||||
|
new CollisionPayloadResult(
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
fileId,
|
||||||
|
payload));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
failures.Add((type, fileId, exception.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref collisionCompleted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (CollisionPayloadResult result in
|
||||||
|
batchResults.OrderBy(result => result.Key))
|
||||||
|
{
|
||||||
|
ExactPayloadAliasCatalog aliases =
|
||||||
|
result.Type == PakAssetType.GfxObjCollision
|
||||||
|
? gfxCollisionAliases
|
||||||
|
: setupCollisionAliases;
|
||||||
|
bool alias = WriteExactPayload(
|
||||||
|
writer,
|
||||||
|
result.Key,
|
||||||
|
result.Payload,
|
||||||
|
aliases);
|
||||||
|
writtenKeys.Add(result.Key);
|
||||||
|
|
||||||
|
if (result.Type == PakAssetType.GfxObjCollision)
|
||||||
|
{
|
||||||
|
gfxCollisionWritten++;
|
||||||
|
if (alias)
|
||||||
|
gfxCollisionAliasCount++;
|
||||||
|
else
|
||||||
|
uniqueGfxCollisions++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
setupCollisionWritten++;
|
||||||
|
if (alias)
|
||||||
|
setupCollisionAliasCount++;
|
||||||
|
else
|
||||||
|
uniqueSetupCollisions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportProgressIfDue(
|
||||||
|
collisionCompleted,
|
||||||
|
totalCollisionJobs,
|
||||||
|
failures.Count,
|
||||||
|
collisionStopwatch.Elapsed,
|
||||||
|
lastProgressReport,
|
||||||
|
final: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int groupBatchStart = 0;
|
||||||
|
groupBatchStart < envCatalog.Groups.Count;
|
||||||
|
groupBatchStart += BatchSize)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
EnvCellGeometryGroup[] groupBatch = envCatalog.Groups
|
||||||
|
.Skip(groupBatchStart)
|
||||||
|
.Take(BatchSize)
|
||||||
|
.ToArray();
|
||||||
|
var structureResults =
|
||||||
|
new ConcurrentBag<CellStructurePayloadResult>();
|
||||||
|
|
||||||
|
Parallel.ForEach(
|
||||||
|
groupBatch,
|
||||||
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = options.Threads,
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
},
|
||||||
|
group =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint environmentDid =
|
||||||
|
0x0D00_0000u | group.EnvironmentId;
|
||||||
|
if (!datReaderWriter.Portal.TryGet<DatEnvironment>(
|
||||||
|
environmentDid,
|
||||||
|
out DatEnvironment? environment)
|
||||||
|
|| environment is null
|
||||||
|
|| !environment.Cells.TryGetValue(
|
||||||
|
group.CellStructure,
|
||||||
|
out CellStruct? cellStruct)
|
||||||
|
|| cellStruct is null)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
group.FileIds[0],
|
||||||
|
$"environment 0x{environmentDid:X8} cell " +
|
||||||
|
$"structure {group.CellStructure} was not found"));
|
||||||
|
Interlocked.Add(
|
||||||
|
ref collisionCompleted,
|
||||||
|
group.FileIds.Length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatCellStructureCollisionAsset structure =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellStructure(
|
||||||
|
cellStruct);
|
||||||
|
byte[] payload =
|
||||||
|
FlatCollisionAssetSerializer.Serialize(
|
||||||
|
structure,
|
||||||
|
cancellationToken);
|
||||||
|
structureResults.Add(
|
||||||
|
new CellStructurePayloadResult(
|
||||||
|
group,
|
||||||
|
structure,
|
||||||
|
payload));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
group.FileIds[0],
|
||||||
|
exception.Message));
|
||||||
|
Interlocked.Add(
|
||||||
|
ref collisionCompleted,
|
||||||
|
group.FileIds.Length);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref collisionCompleted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (CellStructurePayloadResult result in
|
||||||
|
structureResults.OrderBy(
|
||||||
|
result => result.Group.FileIds[0]))
|
||||||
|
{
|
||||||
|
(int physical, int aliases) = WriteExactPayloadGroup(
|
||||||
|
writer,
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
result.Group.FileIds,
|
||||||
|
result.Payload,
|
||||||
|
cellStructureAliases,
|
||||||
|
writtenKeys);
|
||||||
|
cellStructureWritten += result.Group.FileIds.Length;
|
||||||
|
uniqueCellStructures += physical;
|
||||||
|
cellStructureAliasCount += aliases;
|
||||||
|
|
||||||
|
for (int topologyBatchStart = 0;
|
||||||
|
topologyBatchStart < result.Group.FileIds.Length;
|
||||||
|
topologyBatchStart += BatchSize)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
uint[] topologyBatch = result.Group.FileIds
|
||||||
|
.Skip(topologyBatchStart)
|
||||||
|
.Take(BatchSize)
|
||||||
|
.ToArray();
|
||||||
|
var topologyResults =
|
||||||
|
new ConcurrentBag<CollisionPayloadResult>();
|
||||||
|
|
||||||
|
Parallel.ForEach(
|
||||||
|
topologyBatch,
|
||||||
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = options.Threads,
|
||||||
|
CancellationToken = cancellationToken,
|
||||||
|
},
|
||||||
|
fileId =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!datReaderWriter.Cell.TryGet<EnvCell>(
|
||||||
|
fileId,
|
||||||
|
out EnvCell? envCell)
|
||||||
|
|| envCell is null)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
fileId,
|
||||||
|
"EnvCell DAT record was not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatEnvCellTopology topology =
|
||||||
|
FlatCollisionAssetBuilder
|
||||||
|
.FlattenEnvCellTopology(
|
||||||
|
fileId,
|
||||||
|
envCell,
|
||||||
|
result.Structure
|
||||||
|
.PortalPolygons);
|
||||||
|
byte[] payload =
|
||||||
|
FlatCollisionAssetSerializer.Serialize(
|
||||||
|
topology,
|
||||||
|
cancellationToken);
|
||||||
|
topologyResults.Add(
|
||||||
|
new CollisionPayloadResult(
|
||||||
|
PakKey.Compose(
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
fileId),
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
fileId,
|
||||||
|
payload));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
failures.Add((
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
fileId,
|
||||||
|
exception.Message));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Increment(
|
||||||
|
ref collisionCompleted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (CollisionPayloadResult topology in
|
||||||
|
topologyResults.OrderBy(
|
||||||
|
result => result.Key))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
topology.Key,
|
||||||
|
topology.Payload);
|
||||||
|
writtenKeys.Add(topology.Key);
|
||||||
|
envCellTopologyWritten++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportProgressIfDue(
|
||||||
|
collisionCompleted,
|
||||||
|
totalCollisionJobs,
|
||||||
|
failures.Count,
|
||||||
|
collisionStopwatch.Elapsed,
|
||||||
|
lastProgressReport,
|
||||||
|
final: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collisionStopwatch.Stop();
|
||||||
|
ReportProgressIfDue(
|
||||||
|
collisionCompleted,
|
||||||
|
totalCollisionJobs,
|
||||||
|
failures.Count,
|
||||||
|
collisionStopwatch.Elapsed,
|
||||||
|
lastProgressReport,
|
||||||
|
final: true);
|
||||||
|
|
||||||
writer.Finish();
|
writer.Finish();
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
var typeCounts = new Dictionary<PakAssetType, int>
|
||||||
|
{
|
||||||
|
[PakAssetType.GfxObjMesh] =
|
||||||
|
gfxObjWritten + sideStagedWritten,
|
||||||
|
[PakAssetType.SetupMesh] = setupWritten,
|
||||||
|
[PakAssetType.EnvCellMesh] = envCellKeysWritten,
|
||||||
|
[PakAssetType.GfxObjCollision] = gfxCollisionWritten,
|
||||||
|
[PakAssetType.SetupCollision] = setupCollisionWritten,
|
||||||
|
[PakAssetType.CellStructureCollision] =
|
||||||
|
cellStructureWritten,
|
||||||
|
[PakAssetType.EnvCellTopology] = envCellTopologyWritten,
|
||||||
|
};
|
||||||
var report = new BakeReport
|
var report = new BakeReport
|
||||||
{
|
{
|
||||||
Header = header,
|
Header = header,
|
||||||
|
|
@ -382,9 +794,27 @@ public static class BakeRunner
|
||||||
EnvCellKeys = envCellKeysWritten,
|
EnvCellKeys = envCellKeysWritten,
|
||||||
UniqueEnvCellGeometries = envCellGeometriesWritten,
|
UniqueEnvCellGeometries = envCellGeometriesWritten,
|
||||||
EnvCellAliases = envCellKeysWritten - envCellGeometriesWritten,
|
EnvCellAliases = envCellKeysWritten - envCellGeometriesWritten,
|
||||||
|
GfxObjCollisionKeys = gfxCollisionWritten,
|
||||||
|
UniqueGfxObjCollisions = uniqueGfxCollisions,
|
||||||
|
GfxObjCollisionAliases = gfxCollisionAliasCount,
|
||||||
|
SetupCollisionKeys = setupCollisionWritten,
|
||||||
|
UniqueSetupCollisions = uniqueSetupCollisions,
|
||||||
|
SetupCollisionAliases = setupCollisionAliasCount,
|
||||||
|
CellStructureCollisionKeys = cellStructureWritten,
|
||||||
|
UniqueCellStructureCollisions = uniqueCellStructures,
|
||||||
|
CellStructureCollisionAliases =
|
||||||
|
cellStructureAliasCount,
|
||||||
|
EnvCellTopologyKeys = envCellTopologyWritten,
|
||||||
SideStagedKeys = sideStagedWritten,
|
SideStagedKeys = sideStagedWritten,
|
||||||
PhysicalBlobs =
|
PhysicalBlobs =
|
||||||
gfxObjWritten + setupWritten + envCellGeometriesWritten + sideStagedWritten,
|
gfxObjWritten
|
||||||
|
+ setupWritten
|
||||||
|
+ envCellGeometriesWritten
|
||||||
|
+ sideStagedWritten
|
||||||
|
+ uniqueGfxCollisions
|
||||||
|
+ uniqueSetupCollisions
|
||||||
|
+ uniqueCellStructures
|
||||||
|
+ envCellTopologyWritten,
|
||||||
TotalKeys = writtenKeys.Count,
|
TotalKeys = writtenKeys.Count,
|
||||||
Failures = failures.Count,
|
Failures = failures.Count,
|
||||||
ExtractionAndWriteElapsed = stopwatch.Elapsed,
|
ExtractionAndWriteElapsed = stopwatch.Elapsed,
|
||||||
|
|
@ -392,6 +822,7 @@ public static class BakeRunner
|
||||||
OutputBytes = new FileInfo(options.OutPath).Length,
|
OutputBytes = new FileInfo(options.OutPath).Length,
|
||||||
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
|
PeakWorkingSetBytes = Process.GetCurrentProcess().PeakWorkingSet64,
|
||||||
PeakPrivateBytes = Process.GetCurrentProcess().PeakPagedMemorySize64,
|
PeakPrivateBytes = Process.GetCurrentProcess().PeakPagedMemorySize64,
|
||||||
|
TypeCounts = typeCounts,
|
||||||
};
|
};
|
||||||
|
|
||||||
report = report with { SideStagedDuplicateKeys = sideStagedDuped };
|
report = report with { SideStagedDuplicateKeys = sideStagedDuped };
|
||||||
|
|
@ -400,6 +831,63 @@ public static class BakeRunner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool WriteExactPayload(
|
||||||
|
PakWriter writer,
|
||||||
|
ulong key,
|
||||||
|
byte[] payload,
|
||||||
|
ExactPayloadAliasCatalog aliases)
|
||||||
|
{
|
||||||
|
if (aliases.TryFind(payload, out ulong primaryKey))
|
||||||
|
{
|
||||||
|
writer.AddAlias(key, primaryKey);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.AddBlob(key, payload);
|
||||||
|
aliases.Add(key, payload);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Physical, int Aliases) WriteExactPayloadGroup(
|
||||||
|
PakWriter writer,
|
||||||
|
PakAssetType type,
|
||||||
|
IReadOnlyList<uint> fileIds,
|
||||||
|
byte[] payload,
|
||||||
|
ExactPayloadAliasCatalog aliases,
|
||||||
|
HashSet<ulong> writtenKeys)
|
||||||
|
{
|
||||||
|
if (fileIds.Count == 0)
|
||||||
|
throw new InvalidDataException("collision alias group is empty");
|
||||||
|
|
||||||
|
int firstUnwrittenIndex = 0;
|
||||||
|
ulong primaryKey;
|
||||||
|
int physical;
|
||||||
|
if (aliases.TryFind(payload, out primaryKey))
|
||||||
|
{
|
||||||
|
physical = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
primaryKey = PakKey.Compose(type, fileIds[0]);
|
||||||
|
writer.AddBlob(primaryKey, payload);
|
||||||
|
aliases.Add(primaryKey, payload);
|
||||||
|
writtenKeys.Add(primaryKey);
|
||||||
|
firstUnwrittenIndex = 1;
|
||||||
|
physical = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int aliasCount = 0;
|
||||||
|
for (int i = firstUnwrittenIndex; i < fileIds.Count; i++)
|
||||||
|
{
|
||||||
|
ulong aliasKey = PakKey.Compose(type, fileIds[i]);
|
||||||
|
writer.AddAlias(aliasKey, primaryKey);
|
||||||
|
writtenKeys.Add(aliasKey);
|
||||||
|
aliasCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (physical, aliasCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static int DrainSideStaged(
|
private static int DrainSideStaged(
|
||||||
ConcurrentQueue<ObjectMeshData> sideStaged,
|
ConcurrentQueue<ObjectMeshData> sideStaged,
|
||||||
Dictionary<ulong, ObjectMeshData> sideStagedByKey,
|
Dictionary<ulong, ObjectMeshData> sideStagedByKey,
|
||||||
|
|
@ -450,6 +938,20 @@ public static class BakeRunner
|
||||||
Console.WriteLine($" EnvCell unique: {report.UniqueEnvCellGeometries:N0}");
|
Console.WriteLine($" EnvCell unique: {report.UniqueEnvCellGeometries:N0}");
|
||||||
Console.WriteLine($" EnvCell aliases: {report.EnvCellAliases:N0}");
|
Console.WriteLine($" EnvCell aliases: {report.EnvCellAliases:N0}");
|
||||||
Console.WriteLine($" EnvCell dedup: {report.EnvCellDedupRatio:F1}x");
|
Console.WriteLine($" EnvCell dedup: {report.EnvCellDedupRatio:F1}x");
|
||||||
|
Console.WriteLine(
|
||||||
|
$" Gfx collisions: {report.GfxObjCollisionKeys:N0} keys, " +
|
||||||
|
$"{report.UniqueGfxObjCollisions:N0} blobs, " +
|
||||||
|
$"{report.GfxObjCollisionAliases:N0} aliases");
|
||||||
|
Console.WriteLine(
|
||||||
|
$" Setup collisions: {report.SetupCollisionKeys:N0} keys, " +
|
||||||
|
$"{report.UniqueSetupCollisions:N0} blobs, " +
|
||||||
|
$"{report.SetupCollisionAliases:N0} aliases");
|
||||||
|
Console.WriteLine(
|
||||||
|
$" Cell structures: {report.CellStructureCollisionKeys:N0} keys, " +
|
||||||
|
$"{report.UniqueCellStructureCollisions:N0} blobs, " +
|
||||||
|
$"{report.CellStructureCollisionAliases:N0} aliases");
|
||||||
|
Console.WriteLine(
|
||||||
|
$" Cell topologies: {report.EnvCellTopologyKeys:N0}");
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$" side-staged keys: {report.SideStagedKeys:N0} " +
|
$" side-staged keys: {report.SideStagedKeys:N0} " +
|
||||||
$"({report.SideStagedDuplicateKeys:N0} duplicate keys)");
|
$"({report.SideStagedDuplicateKeys:N0} duplicate keys)");
|
||||||
|
|
@ -526,4 +1028,15 @@ public static class BakeRunner
|
||||||
|
|
||||||
return envCellIds;
|
return envCellIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record CollisionPayloadResult(
|
||||||
|
ulong Key,
|
||||||
|
PakAssetType Type,
|
||||||
|
uint FileId,
|
||||||
|
byte[] Payload);
|
||||||
|
|
||||||
|
private sealed record CellStructurePayloadResult(
|
||||||
|
EnvCellGeometryGroup Group,
|
||||||
|
FlatCellStructureCollisionAsset Structure,
|
||||||
|
byte[] Payload);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
src/AcDream.Bake/ExactPayloadAliasCatalog.cs
Normal file
50
src/AcDream.Bake/ExactPayloadAliasCatalog.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace AcDream.Bake;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Offline-only exact-byte deduplication catalog. SHA-256 narrows candidates;
|
||||||
|
/// a complete byte comparison is still mandatory before any alias is emitted.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ExactPayloadAliasCatalog
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, List<Entry>> _entriesByDigest =
|
||||||
|
new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public bool TryFind(ReadOnlySpan<byte> payload, out ulong primaryKey)
|
||||||
|
{
|
||||||
|
string digest = Digest(payload);
|
||||||
|
if (_entriesByDigest.TryGetValue(digest, out List<Entry>? candidates))
|
||||||
|
{
|
||||||
|
foreach (Entry candidate in candidates)
|
||||||
|
{
|
||||||
|
if (payload.SequenceEqual(candidate.Payload))
|
||||||
|
{
|
||||||
|
primaryKey = candidate.PrimaryKey;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryKey = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(ulong primaryKey, byte[] payload)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(payload);
|
||||||
|
string digest = Digest(payload);
|
||||||
|
if (!_entriesByDigest.TryGetValue(digest, out List<Entry>? entries))
|
||||||
|
{
|
||||||
|
entries = [];
|
||||||
|
_entriesByDigest.Add(digest, entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new Entry(primaryKey, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Digest(ReadOnlySpan<byte> payload) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(payload));
|
||||||
|
|
||||||
|
private sealed record Entry(ulong PrimaryKey, byte[] Payload);
|
||||||
|
}
|
||||||
715
src/AcDream.Content/FlatCollisionAssetSerializer.cs
Normal file
715
src/AcDream.Content/FlatCollisionAssetSerializer.cs
Normal file
|
|
@ -0,0 +1,715 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
|
||||||
|
namespace AcDream.Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strict little-endian package codec for immutable flat collision assets.
|
||||||
|
/// Every float is persisted through its exact IEEE-754 bit pattern. Readers
|
||||||
|
/// reject invalid counts, invalid enum/range/tree contracts, wrong payload
|
||||||
|
/// kinds, non-zero reserved bytes, truncation, and trailing bytes.
|
||||||
|
/// </summary>
|
||||||
|
public static class FlatCollisionAssetSerializer
|
||||||
|
{
|
||||||
|
private const uint Magic = 0x4C_43_43_41u; // "ACCL" little-endian
|
||||||
|
private const byte SchemaVersion = 1;
|
||||||
|
private const int MaximumRows = 16_777_216;
|
||||||
|
|
||||||
|
public static byte[] Serialize(
|
||||||
|
FlatGfxObjCollisionAsset asset,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(asset);
|
||||||
|
var writer = new CollisionWriter();
|
||||||
|
writer.WriteHeader(CollisionPayloadKind.GfxObj);
|
||||||
|
WritePhysicsBsp(writer, asset.PhysicsBsp, cancellationToken);
|
||||||
|
writer.WriteOptionalSphere(asset.BoundingSphere);
|
||||||
|
writer.WriteBoolean(asset.VisualBounds.HasValue);
|
||||||
|
if (asset.VisualBounds is FlatGfxObjVisualBounds visual)
|
||||||
|
{
|
||||||
|
writer.WriteVector3(visual.Min);
|
||||||
|
writer.WriteVector3(visual.Max);
|
||||||
|
writer.WriteVector3(visual.Center);
|
||||||
|
writer.WriteSingle(visual.Radius);
|
||||||
|
writer.WriteVector3(visual.HalfExtents);
|
||||||
|
}
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(
|
||||||
|
FlatSetupCollision asset,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(asset);
|
||||||
|
var writer = new CollisionWriter();
|
||||||
|
writer.WriteHeader(CollisionPayloadKind.Setup);
|
||||||
|
writer.WriteCount(asset.Cylinders.Length, "Setup cylinder");
|
||||||
|
writer.WriteCount(asset.Spheres.Length, "Setup sphere");
|
||||||
|
writer.WriteSingle(asset.Height);
|
||||||
|
writer.WriteSingle(asset.Radius);
|
||||||
|
writer.WriteSingle(asset.StepUpHeight);
|
||||||
|
writer.WriteSingle(asset.StepDownHeight);
|
||||||
|
|
||||||
|
for (int i = 0; i < asset.Cylinders.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
FlatCollisionCylinder cylinder = asset.Cylinders[i];
|
||||||
|
writer.WriteVector3(cylinder.Origin);
|
||||||
|
writer.WriteSingle(cylinder.Radius);
|
||||||
|
writer.WriteSingle(cylinder.Height);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < asset.Spheres.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
writer.WriteSphere(asset.Spheres[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(
|
||||||
|
FlatCellStructureCollisionAsset asset,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(asset);
|
||||||
|
var writer = new CollisionWriter();
|
||||||
|
writer.WriteHeader(CollisionPayloadKind.CellStructure);
|
||||||
|
WritePhysicsBsp(writer, asset.PhysicsBsp, cancellationToken);
|
||||||
|
WriteContainmentBsp(writer, asset.ContainmentBsp, cancellationToken);
|
||||||
|
WritePolygonTable(writer, asset.PortalPolygons, cancellationToken);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(
|
||||||
|
FlatEnvCellTopology asset,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(asset);
|
||||||
|
var writer = new CollisionWriter();
|
||||||
|
writer.WriteHeader(CollisionPayloadKind.EnvCellTopology);
|
||||||
|
writer.WriteCount(asset.Portals.Length, "EnvCell portal");
|
||||||
|
writer.WriteCount(asset.VisibleCellIds.Length, "visible-cell");
|
||||||
|
writer.WriteBoolean(asset.SeenOutside);
|
||||||
|
|
||||||
|
for (int i = 0; i < asset.Portals.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
FlatEnvCellPortal portal = asset.Portals[i];
|
||||||
|
writer.WriteUInt16(portal.OtherCellId);
|
||||||
|
writer.WriteUInt16(portal.PolygonId);
|
||||||
|
writer.WriteUInt16(portal.Flags);
|
||||||
|
writer.WriteInt32(portal.PolygonIndex);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < asset.VisibleCellIds.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
writer.WriteUInt32(asset.VisibleCellIds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return writer.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlatGfxObjCollisionAsset DeserializeGfxObj(
|
||||||
|
ReadOnlySpan<byte> bytes,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var reader = new CollisionReader(bytes, cancellationToken);
|
||||||
|
reader.ReadHeader(CollisionPayloadKind.GfxObj);
|
||||||
|
FlatPhysicsBsp physicsBsp = ReadPhysicsBsp(ref reader);
|
||||||
|
FlatCollisionSphere? boundingSphere = reader.ReadOptionalSphere();
|
||||||
|
FlatGfxObjVisualBounds? visualBounds = null;
|
||||||
|
if (reader.ReadBoolean())
|
||||||
|
{
|
||||||
|
visualBounds = new FlatGfxObjVisualBounds(
|
||||||
|
reader.ReadVector3(),
|
||||||
|
reader.ReadVector3(),
|
||||||
|
reader.ReadVector3(),
|
||||||
|
reader.ReadSingle(),
|
||||||
|
reader.ReadVector3());
|
||||||
|
}
|
||||||
|
reader.RequireEnd();
|
||||||
|
return new FlatGfxObjCollisionAsset(
|
||||||
|
physicsBsp,
|
||||||
|
boundingSphere,
|
||||||
|
visualBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlatSetupCollision DeserializeSetup(
|
||||||
|
ReadOnlySpan<byte> bytes,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var reader = new CollisionReader(bytes, cancellationToken);
|
||||||
|
reader.ReadHeader(CollisionPayloadKind.Setup);
|
||||||
|
int cylinderCount = reader.ReadCount(
|
||||||
|
"Setup cylinder",
|
||||||
|
bytesPerItem: 20);
|
||||||
|
int sphereCount = reader.ReadCount(
|
||||||
|
"Setup sphere",
|
||||||
|
bytesPerItem: 16);
|
||||||
|
reader.RequireRemaining(
|
||||||
|
checked(16L + (20L * cylinderCount) + (16L * sphereCount)),
|
||||||
|
"Setup collision rows");
|
||||||
|
float height = reader.ReadSingle();
|
||||||
|
float radius = reader.ReadSingle();
|
||||||
|
float stepUpHeight = reader.ReadSingle();
|
||||||
|
float stepDownHeight = reader.ReadSingle();
|
||||||
|
|
||||||
|
var cylinders =
|
||||||
|
ImmutableArray.CreateBuilder<FlatCollisionCylinder>(cylinderCount);
|
||||||
|
for (int i = 0; i < cylinderCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
cylinders.Add(new FlatCollisionCylinder(
|
||||||
|
reader.ReadVector3(),
|
||||||
|
reader.ReadSingle(),
|
||||||
|
reader.ReadSingle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var spheres =
|
||||||
|
ImmutableArray.CreateBuilder<FlatCollisionSphere>(sphereCount);
|
||||||
|
for (int i = 0; i < sphereCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
spheres.Add(reader.ReadSphere());
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.RequireEnd();
|
||||||
|
return new FlatSetupCollision(
|
||||||
|
cylinders.MoveToImmutable(),
|
||||||
|
spheres.MoveToImmutable(),
|
||||||
|
height,
|
||||||
|
radius,
|
||||||
|
stepUpHeight,
|
||||||
|
stepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlatCellStructureCollisionAsset DeserializeCellStructure(
|
||||||
|
ReadOnlySpan<byte> bytes,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var reader = new CollisionReader(bytes, cancellationToken);
|
||||||
|
reader.ReadHeader(CollisionPayloadKind.CellStructure);
|
||||||
|
FlatPhysicsBsp physicsBsp = ReadPhysicsBsp(ref reader);
|
||||||
|
FlatCellContainmentBsp containmentBsp = ReadContainmentBsp(ref reader);
|
||||||
|
FlatPolygonTable portalPolygons = ReadPolygonTable(ref reader);
|
||||||
|
reader.RequireEnd();
|
||||||
|
return new FlatCellStructureCollisionAsset(
|
||||||
|
physicsBsp,
|
||||||
|
containmentBsp,
|
||||||
|
portalPolygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlatEnvCellTopology DeserializeEnvCellTopology(
|
||||||
|
ReadOnlySpan<byte> bytes,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var reader = new CollisionReader(bytes, cancellationToken);
|
||||||
|
reader.ReadHeader(CollisionPayloadKind.EnvCellTopology);
|
||||||
|
int portalCount = reader.ReadCount(
|
||||||
|
"EnvCell portal",
|
||||||
|
bytesPerItem: 10);
|
||||||
|
int visibleCount = reader.ReadCount(
|
||||||
|
"visible-cell",
|
||||||
|
bytesPerItem: 4);
|
||||||
|
reader.RequireRemaining(
|
||||||
|
checked(1L + (10L * portalCount) + (4L * visibleCount)),
|
||||||
|
"EnvCell topology rows");
|
||||||
|
bool seenOutside = reader.ReadBoolean();
|
||||||
|
|
||||||
|
var portals =
|
||||||
|
ImmutableArray.CreateBuilder<FlatEnvCellPortal>(portalCount);
|
||||||
|
for (int i = 0; i < portalCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
portals.Add(new FlatEnvCellPortal(
|
||||||
|
reader.ReadUInt16(),
|
||||||
|
reader.ReadUInt16(),
|
||||||
|
reader.ReadUInt16(),
|
||||||
|
reader.ReadInt32()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible =
|
||||||
|
ImmutableArray.CreateBuilder<uint>(visibleCount);
|
||||||
|
for (int i = 0; i < visibleCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
visible.Add(reader.ReadUInt32());
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.RequireEnd();
|
||||||
|
return new FlatEnvCellTopology(
|
||||||
|
portals.MoveToImmutable(),
|
||||||
|
visible.MoveToImmutable(),
|
||||||
|
seenOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WritePhysicsBsp(
|
||||||
|
CollisionWriter writer,
|
||||||
|
FlatPhysicsBsp bsp,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(bsp);
|
||||||
|
writer.WriteInt32(bsp.RootIndex);
|
||||||
|
writer.WriteCount(bsp.Nodes.Length, "physics BSP node");
|
||||||
|
writer.WriteCount(
|
||||||
|
bsp.PolygonIndexStream.Length,
|
||||||
|
"physics BSP polygon-index");
|
||||||
|
|
||||||
|
for (int i = 0; i < bsp.Nodes.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
FlatPhysicsBspNode node = bsp.Nodes[i];
|
||||||
|
writer.WriteUInt32((uint)node.Type);
|
||||||
|
writer.WritePlane(node.SplittingPlane);
|
||||||
|
writer.WriteInt32(node.PositiveChildIndex);
|
||||||
|
writer.WriteInt32(node.NegativeChildIndex);
|
||||||
|
writer.WriteInt32(node.LeafIndex);
|
||||||
|
writer.WriteInt32(node.Solid);
|
||||||
|
writer.WriteSphere(node.BoundingSphere);
|
||||||
|
writer.WriteRange(node.PolygonIndexRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < bsp.PolygonIndexStream.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
writer.WriteInt32(bsp.PolygonIndexStream[i]);
|
||||||
|
}
|
||||||
|
WritePolygonTable(writer, bsp.PolygonTable, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlatPhysicsBsp ReadPhysicsBsp(
|
||||||
|
ref CollisionReader reader)
|
||||||
|
{
|
||||||
|
int rootIndex = reader.ReadInt32();
|
||||||
|
int nodeCount = reader.ReadCount(
|
||||||
|
"physics BSP node",
|
||||||
|
bytesPerItem: 60);
|
||||||
|
int polygonIndexCount = reader.ReadCount(
|
||||||
|
"physics BSP polygon-index",
|
||||||
|
bytesPerItem: 4);
|
||||||
|
reader.RequireRemaining(
|
||||||
|
checked((60L * nodeCount) + (4L * polygonIndexCount) + 8L),
|
||||||
|
"physics BSP rows");
|
||||||
|
|
||||||
|
var nodes =
|
||||||
|
ImmutableArray.CreateBuilder<FlatPhysicsBspNode>(nodeCount);
|
||||||
|
for (int i = 0; i < nodeCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
nodes.Add(new FlatPhysicsBspNode(
|
||||||
|
(BSPNodeType)reader.ReadUInt32(),
|
||||||
|
reader.ReadPlane(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadSphere(),
|
||||||
|
reader.ReadRange()));
|
||||||
|
}
|
||||||
|
|
||||||
|
var polygonIndices =
|
||||||
|
ImmutableArray.CreateBuilder<int>(polygonIndexCount);
|
||||||
|
for (int i = 0; i < polygonIndexCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
polygonIndices.Add(reader.ReadInt32());
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatPolygonTable polygons = ReadPolygonTable(ref reader);
|
||||||
|
return new FlatPhysicsBsp(
|
||||||
|
rootIndex,
|
||||||
|
nodes.MoveToImmutable(),
|
||||||
|
polygonIndices.MoveToImmutable(),
|
||||||
|
polygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteContainmentBsp(
|
||||||
|
CollisionWriter writer,
|
||||||
|
FlatCellContainmentBsp bsp,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(bsp);
|
||||||
|
writer.WriteInt32(bsp.RootIndex);
|
||||||
|
writer.WriteCount(bsp.Nodes.Length, "cell-containment BSP node");
|
||||||
|
for (int i = 0; i < bsp.Nodes.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
FlatCellBspNode node = bsp.Nodes[i];
|
||||||
|
writer.WriteUInt32((uint)node.Type);
|
||||||
|
writer.WritePlane(node.SplittingPlane);
|
||||||
|
writer.WriteInt32(node.PositiveChildIndex);
|
||||||
|
writer.WriteInt32(node.NegativeChildIndex);
|
||||||
|
writer.WriteInt32(node.LeafIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlatCellContainmentBsp ReadContainmentBsp(
|
||||||
|
ref CollisionReader reader)
|
||||||
|
{
|
||||||
|
int rootIndex = reader.ReadInt32();
|
||||||
|
int nodeCount = reader.ReadCount(
|
||||||
|
"cell-containment BSP node",
|
||||||
|
bytesPerItem: 32);
|
||||||
|
var nodes =
|
||||||
|
ImmutableArray.CreateBuilder<FlatCellBspNode>(nodeCount);
|
||||||
|
for (int i = 0; i < nodeCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
nodes.Add(new FlatCellBspNode(
|
||||||
|
(BSPNodeType)reader.ReadUInt32(),
|
||||||
|
reader.ReadPlane(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadInt32(),
|
||||||
|
reader.ReadInt32()));
|
||||||
|
}
|
||||||
|
return new FlatCellContainmentBsp(
|
||||||
|
rootIndex,
|
||||||
|
nodes.MoveToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WritePolygonTable(
|
||||||
|
CollisionWriter writer,
|
||||||
|
FlatPolygonTable table,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(table);
|
||||||
|
writer.WriteCount(table.Polygons.Length, "polygon");
|
||||||
|
writer.WriteCount(table.Vertices.Length, "polygon vertex");
|
||||||
|
for (int i = 0; i < table.Polygons.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
FlatCollisionPolygon polygon = table.Polygons[i];
|
||||||
|
writer.WriteUInt16(polygon.Id);
|
||||||
|
writer.WriteUInt32((uint)polygon.SidesType);
|
||||||
|
writer.WriteInt32(polygon.NumPoints);
|
||||||
|
writer.WriteRange(polygon.VertexRange);
|
||||||
|
writer.WritePlane(polygon.Plane);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < table.Vertices.Length; i++)
|
||||||
|
{
|
||||||
|
CheckCancellation(cancellationToken, i);
|
||||||
|
writer.WriteVector3(table.Vertices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlatPolygonTable ReadPolygonTable(
|
||||||
|
ref CollisionReader reader)
|
||||||
|
{
|
||||||
|
int polygonCount = reader.ReadCount(
|
||||||
|
"polygon",
|
||||||
|
bytesPerItem: 34);
|
||||||
|
int vertexCount = reader.ReadCount(
|
||||||
|
"polygon vertex",
|
||||||
|
bytesPerItem: 12);
|
||||||
|
reader.RequireRemaining(
|
||||||
|
checked((34L * polygonCount) + (12L * vertexCount)),
|
||||||
|
"polygon-table rows");
|
||||||
|
var polygons =
|
||||||
|
ImmutableArray.CreateBuilder<FlatCollisionPolygon>(polygonCount);
|
||||||
|
for (int i = 0; i < polygonCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
polygons.Add(new FlatCollisionPolygon(
|
||||||
|
reader.ReadUInt16(),
|
||||||
|
reader.ReadPlaneAfterMetadata(out CullMode sides, out int count,
|
||||||
|
out FlatIndexRange range),
|
||||||
|
sides,
|
||||||
|
count,
|
||||||
|
range));
|
||||||
|
}
|
||||||
|
|
||||||
|
var vertices = ImmutableArray.CreateBuilder<Vector3>(vertexCount);
|
||||||
|
for (int i = 0; i < vertexCount; i++)
|
||||||
|
{
|
||||||
|
reader.CheckCancellation(i);
|
||||||
|
vertices.Add(reader.ReadVector3());
|
||||||
|
}
|
||||||
|
return new FlatPolygonTable(
|
||||||
|
polygons.MoveToImmutable(),
|
||||||
|
vertices.MoveToImmutable());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckCancellation(
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
int index)
|
||||||
|
{
|
||||||
|
if ((index & 0xFF) == 0)
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CollisionPayloadKind : byte
|
||||||
|
{
|
||||||
|
GfxObj = 1,
|
||||||
|
Setup = 2,
|
||||||
|
CellStructure = 3,
|
||||||
|
EnvCellTopology = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CollisionWriter
|
||||||
|
{
|
||||||
|
private readonly ArrayBufferWriter<byte> _buffer = new(256);
|
||||||
|
|
||||||
|
public void WriteHeader(CollisionPayloadKind kind)
|
||||||
|
{
|
||||||
|
WriteUInt32(Magic);
|
||||||
|
WriteByte((byte)kind);
|
||||||
|
WriteByte(SchemaVersion);
|
||||||
|
WriteUInt16(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteCount(int value, string name)
|
||||||
|
{
|
||||||
|
if ((uint)value > MaximumRows)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{name} count {value} exceeds {MaximumRows}.");
|
||||||
|
}
|
||||||
|
WriteInt32(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteOptionalSphere(FlatCollisionSphere? sphere)
|
||||||
|
{
|
||||||
|
WriteBoolean(sphere.HasValue);
|
||||||
|
if (sphere.HasValue)
|
||||||
|
WriteSphere(sphere.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteBoolean(bool value) => WriteByte(value ? (byte)1 : (byte)0);
|
||||||
|
|
||||||
|
public void WriteSphere(FlatCollisionSphere sphere)
|
||||||
|
{
|
||||||
|
WriteVector3(sphere.Origin);
|
||||||
|
WriteSingle(sphere.Radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteRange(FlatIndexRange range)
|
||||||
|
{
|
||||||
|
WriteInt32(range.Start);
|
||||||
|
WriteInt32(range.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WritePlane(Plane plane)
|
||||||
|
{
|
||||||
|
WriteVector3(plane.Normal);
|
||||||
|
WriteSingle(plane.D);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteVector3(Vector3 vector)
|
||||||
|
{
|
||||||
|
WriteSingle(vector.X);
|
||||||
|
WriteSingle(vector.Y);
|
||||||
|
WriteSingle(vector.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteSingle(float value) =>
|
||||||
|
WriteInt32(BitConverter.SingleToInt32Bits(value));
|
||||||
|
|
||||||
|
public void WriteByte(byte value)
|
||||||
|
{
|
||||||
|
Span<byte> span = _buffer.GetSpan(1);
|
||||||
|
span[0] = value;
|
||||||
|
_buffer.Advance(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteUInt16(ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> span = _buffer.GetSpan(sizeof(ushort));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(span, value);
|
||||||
|
_buffer.Advance(sizeof(ushort));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteInt32(int value)
|
||||||
|
{
|
||||||
|
Span<byte> span = _buffer.GetSpan(sizeof(int));
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(span, value);
|
||||||
|
_buffer.Advance(sizeof(int));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteUInt32(uint value)
|
||||||
|
{
|
||||||
|
Span<byte> span = _buffer.GetSpan(sizeof(uint));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(span, value);
|
||||||
|
_buffer.Advance(sizeof(uint));
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ToArray() => _buffer.WrittenSpan.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ref struct CollisionReader
|
||||||
|
{
|
||||||
|
private readonly ReadOnlySpan<byte> _bytes;
|
||||||
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private int _offset;
|
||||||
|
|
||||||
|
public CollisionReader(
|
||||||
|
ReadOnlySpan<byte> bytes,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_bytes = bytes;
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
|
_offset = 0;
|
||||||
|
_cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Remaining => _bytes.Length - _offset;
|
||||||
|
|
||||||
|
public void ReadHeader(CollisionPayloadKind expectedKind)
|
||||||
|
{
|
||||||
|
uint magic = ReadUInt32();
|
||||||
|
byte kind = ReadByte();
|
||||||
|
byte version = ReadByte();
|
||||||
|
ushort reserved = ReadUInt16();
|
||||||
|
if (magic != Magic)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"collision payload magic 0x{magic:X8} does not match " +
|
||||||
|
$"0x{Magic:X8}.");
|
||||||
|
}
|
||||||
|
if (kind != (byte)expectedKind)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"collision payload kind {kind} does not match " +
|
||||||
|
$"{(byte)expectedKind}.");
|
||||||
|
}
|
||||||
|
if (version != SchemaVersion)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"collision payload schema {version} is unsupported.");
|
||||||
|
}
|
||||||
|
if (reserved != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
"collision payload reserved header bits are non-zero.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReadCount(string name, int bytesPerItem)
|
||||||
|
{
|
||||||
|
int count = ReadInt32();
|
||||||
|
if (count < 0 || count > MaximumRows)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{name} count {count} is outside [0,{MaximumRows}].");
|
||||||
|
}
|
||||||
|
if (bytesPerItem <= 0 || count > Remaining / bytesPerItem)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{name} count {count} exceeds the remaining payload.");
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlatCollisionSphere? ReadOptionalSphere() =>
|
||||||
|
ReadBoolean() ? ReadSphere() : null;
|
||||||
|
|
||||||
|
public bool ReadBoolean()
|
||||||
|
{
|
||||||
|
byte value = ReadByte();
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => throw new InvalidDataException(
|
||||||
|
$"invalid collision boolean value {value}."),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlatCollisionSphere ReadSphere() =>
|
||||||
|
new(ReadVector3(), ReadSingle());
|
||||||
|
|
||||||
|
public FlatIndexRange ReadRange() =>
|
||||||
|
new(ReadInt32(), ReadInt32());
|
||||||
|
|
||||||
|
public Plane ReadPlane() =>
|
||||||
|
new(ReadVector3(), ReadSingle());
|
||||||
|
|
||||||
|
public Plane ReadPlaneAfterMetadata(
|
||||||
|
out CullMode sides,
|
||||||
|
out int numPoints,
|
||||||
|
out FlatIndexRange range)
|
||||||
|
{
|
||||||
|
sides = (CullMode)ReadUInt32();
|
||||||
|
numPoints = ReadInt32();
|
||||||
|
range = ReadRange();
|
||||||
|
return ReadPlane();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 ReadVector3() =>
|
||||||
|
new(ReadSingle(), ReadSingle(), ReadSingle());
|
||||||
|
|
||||||
|
public float ReadSingle() =>
|
||||||
|
BitConverter.Int32BitsToSingle(ReadInt32());
|
||||||
|
|
||||||
|
public byte ReadByte()
|
||||||
|
{
|
||||||
|
Require(sizeof(byte));
|
||||||
|
return _bytes[_offset++];
|
||||||
|
}
|
||||||
|
|
||||||
|
public ushort ReadUInt16()
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> span = Take(sizeof(ushort));
|
||||||
|
return BinaryPrimitives.ReadUInt16LittleEndian(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ReadInt32()
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> span = Take(sizeof(int));
|
||||||
|
return BinaryPrimitives.ReadInt32LittleEndian(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ReadUInt32()
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> span = Take(sizeof(uint));
|
||||||
|
return BinaryPrimitives.ReadUInt32LittleEndian(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CheckCancellation(int index)
|
||||||
|
{
|
||||||
|
if ((index & 0xFF) == 0)
|
||||||
|
_cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequireEnd()
|
||||||
|
{
|
||||||
|
_cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (_offset != _bytes.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"collision payload contains {_bytes.Length - _offset} " +
|
||||||
|
"trailing byte(s).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequireRemaining(long required, string name)
|
||||||
|
{
|
||||||
|
if (required < 0 || required > Remaining)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{name} require {required} bytes but only {Remaining} remain.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReadOnlySpan<byte> Take(int length)
|
||||||
|
{
|
||||||
|
Require(length);
|
||||||
|
ReadOnlySpan<byte> result = _bytes.Slice(_offset, length);
|
||||||
|
_offset += length;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Require(int length)
|
||||||
|
{
|
||||||
|
if (length < 0 || length > Remaining)
|
||||||
|
throw new EndOfStreamException("collision payload is truncated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -131,7 +131,10 @@ internal static class PreparedAssetRequestContract
|
||||||
{
|
{
|
||||||
public static void ValidateType(PakAssetType type)
|
public static void ValidateType(PakAssetType type)
|
||||||
{
|
{
|
||||||
if (!Enum.IsDefined(type))
|
if (type is not (
|
||||||
|
PakAssetType.GfxObjMesh or
|
||||||
|
PakAssetType.SetupMesh or
|
||||||
|
PakAssetType.EnvCellMesh))
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
throw new ArgumentOutOfRangeException(
|
||||||
nameof(type),
|
nameof(type),
|
||||||
|
|
@ -166,7 +169,9 @@ internal static class PreparedAssetRequestContract
|
||||||
/// Production prepared source backed by one immutable memory-mapped pak.
|
/// Production prepared source backed by one immutable memory-mapped pak.
|
||||||
/// There is intentionally no DAT fallback.
|
/// There is intentionally no DAT fallback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PakPreparedAssetSource : IPreparedAssetSource
|
public sealed partial class PakPreparedAssetSource :
|
||||||
|
IPreparedAssetSource,
|
||||||
|
IPreparedCollisionSource
|
||||||
{
|
{
|
||||||
private readonly PakReader _reader;
|
private readonly PakReader _reader;
|
||||||
private readonly Action<string>? _diagnosticSink;
|
private readonly Action<string>? _diagnosticSink;
|
||||||
|
|
|
||||||
206
src/AcDream.Content/IPreparedCollisionSource.cs
Normal file
206
src/AcDream.Content/IPreparedCollisionSource.cs
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
|
||||||
|
namespace AcDream.Content;
|
||||||
|
|
||||||
|
public readonly record struct PreparedCollisionSourceStats(
|
||||||
|
long Probes,
|
||||||
|
long Reads,
|
||||||
|
long Loaded,
|
||||||
|
long Missing,
|
||||||
|
long Corrupt);
|
||||||
|
|
||||||
|
public readonly record struct PreparedCollisionReadResult<T>(
|
||||||
|
PreparedAssetReadStatus Status,
|
||||||
|
T? Data)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
public static PreparedCollisionReadResult<T> Missing =>
|
||||||
|
new(PreparedAssetReadStatus.Missing, null);
|
||||||
|
|
||||||
|
public static PreparedCollisionReadResult<T> Corrupt =>
|
||||||
|
new(PreparedAssetReadStatus.Corrupt, null);
|
||||||
|
|
||||||
|
public static PreparedCollisionReadResult<T> Loaded(T data) =>
|
||||||
|
new(PreparedAssetReadStatus.Loaded, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Typed immutable collision payload source. It is deliberately separate from
|
||||||
|
/// the render-only <see cref="IPreparedAssetSource"/> contract even when one
|
||||||
|
/// concrete package owner implements both interfaces over the same mmap.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPreparedCollisionSource : IDisposable
|
||||||
|
{
|
||||||
|
PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId);
|
||||||
|
|
||||||
|
PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
PreparedCollisionSourceStats CollisionStats { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class PreparedCollisionTypeContract
|
||||||
|
{
|
||||||
|
public static void Validate(PakAssetType type)
|
||||||
|
{
|
||||||
|
if (type is not (
|
||||||
|
PakAssetType.GfxObjCollision or
|
||||||
|
PakAssetType.SetupCollision or
|
||||||
|
PakAssetType.CellStructureCollision or
|
||||||
|
PakAssetType.EnvCellTopology))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(type),
|
||||||
|
type,
|
||||||
|
"unknown prepared collision asset type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class PakPreparedAssetSource
|
||||||
|
{
|
||||||
|
private long _collisionProbes;
|
||||||
|
private long _collisionReads;
|
||||||
|
private long _collisionLoaded;
|
||||||
|
private long _collisionMissing;
|
||||||
|
private long _collisionCorrupt;
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats =>
|
||||||
|
new(
|
||||||
|
Volatile.Read(ref _collisionProbes),
|
||||||
|
Volatile.Read(ref _collisionReads),
|
||||||
|
Volatile.Read(ref _collisionLoaded),
|
||||||
|
Volatile.Read(ref _collisionMissing),
|
||||||
|
Volatile.Read(ref _collisionCorrupt));
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId)
|
||||||
|
{
|
||||||
|
PreparedCollisionTypeContract.Validate(type);
|
||||||
|
Interlocked.Increment(ref _collisionProbes);
|
||||||
|
ulong key = PakKey.Compose(type, sourceFileId);
|
||||||
|
return _reader.ProbeEntry(key) switch
|
||||||
|
{
|
||||||
|
PakEntryState.Available => PreparedAssetPresence.Available,
|
||||||
|
PakEntryState.Corrupt => PreparedAssetPresence.Corrupt,
|
||||||
|
_ => PreparedAssetPresence.Missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
ReadCollision(
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
sourceFileId,
|
||||||
|
static (bytes, token) =>
|
||||||
|
FlatCollisionAssetSerializer.DeserializeGfxObj(bytes, token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision>
|
||||||
|
ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
ReadCollision(
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
sourceFileId,
|
||||||
|
static (bytes, token) =>
|
||||||
|
FlatCollisionAssetSerializer.DeserializeSetup(bytes, token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
ReadCollision(
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
sourceFileId,
|
||||||
|
static (bytes, token) =>
|
||||||
|
FlatCollisionAssetSerializer.DeserializeCellStructure(
|
||||||
|
bytes,
|
||||||
|
token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
ReadCollision(
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
sourceFileId,
|
||||||
|
static (bytes, token) =>
|
||||||
|
FlatCollisionAssetSerializer.DeserializeEnvCellTopology(
|
||||||
|
bytes,
|
||||||
|
token),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
private PreparedCollisionReadResult<T> ReadCollision<T>(
|
||||||
|
PakAssetType type,
|
||||||
|
uint sourceFileId,
|
||||||
|
Func<byte[], CancellationToken, T> deserialize,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
PreparedCollisionTypeContract.Validate(type);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
Interlocked.Increment(ref _collisionReads);
|
||||||
|
|
||||||
|
ulong key = PakKey.Compose(type, sourceFileId);
|
||||||
|
PakObjectReadStatus status =
|
||||||
|
_reader.ReadBlobBytes(key, out byte[]? bytes);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (status == PakObjectReadStatus.Missing)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _collisionMissing);
|
||||||
|
return PreparedCollisionReadResult<T>.Missing;
|
||||||
|
}
|
||||||
|
if (status == PakObjectReadStatus.Corrupt || bytes is null)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _collisionCorrupt);
|
||||||
|
return PreparedCollisionReadResult<T>.Corrupt;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T data = deserialize(bytes, cancellationToken);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
Interlocked.Increment(ref _collisionLoaded);
|
||||||
|
return PreparedCollisionReadResult<T>.Loaded(data);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_reader.MarkPayloadCorrupt(
|
||||||
|
key,
|
||||||
|
$"collision deserialization failed despite matching CRC: " +
|
||||||
|
$"{exception.GetType().Name}: {exception.Message}");
|
||||||
|
Interlocked.Increment(ref _collisionCorrupt);
|
||||||
|
return PreparedCollisionReadResult<T>.Corrupt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,10 +20,11 @@ public static class PakFormat {
|
||||||
/// Identity of the bake algorithm that produced the payloads. Version 2
|
/// Identity of the bake algorithm that produced the payloads. Version 2
|
||||||
/// introduced content-identity EnvCell blobs with ordinary TOC aliases;
|
/// introduced content-identity EnvCell blobs with ordinary TOC aliases;
|
||||||
/// version 3 embeds exact render-pass translucency in each texture batch
|
/// version 3 embeds exact render-pass translucency in each texture batch
|
||||||
/// so production never rebuilds surface metadata from live DAT.
|
/// so production never rebuilds surface metadata from live DAT. Version 4
|
||||||
|
/// adds complete immutable flat collision and EnvCell-topology payloads.
|
||||||
/// The binary format remains version 1.
|
/// The binary format remains version 1.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint CurrentBakeToolVersion = 3;
|
public const uint CurrentBakeToolVersion = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ public enum PakAssetType : byte {
|
||||||
GfxObjMesh = 1,
|
GfxObjMesh = 1,
|
||||||
SetupMesh = 2,
|
SetupMesh = 2,
|
||||||
EnvCellMesh = 3,
|
EnvCellMesh = 3,
|
||||||
|
GfxObjCollision = 4,
|
||||||
|
SetupCollision = 5,
|
||||||
|
CellStructureCollision = 6,
|
||||||
|
EnvCellTopology = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,44 @@ public sealed class PakReader : IDisposable {
|
||||||
ulong key,
|
ulong key,
|
||||||
out ObjectMeshData? data) {
|
out ObjectMeshData? data) {
|
||||||
data = null;
|
data = null;
|
||||||
|
PakAssetType type = PakKey.Decompose(key).Type;
|
||||||
|
if (type is not (
|
||||||
|
PakAssetType.GfxObjMesh or
|
||||||
|
PakAssetType.SetupMesh or
|
||||||
|
PakAssetType.EnvCellMesh)) {
|
||||||
|
return PakObjectReadStatus.Missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
PakObjectReadStatus status = ReadBlobBytes(key, out byte[]? bytes);
|
||||||
|
if (status != PakObjectReadStatus.Loaded || bytes is null)
|
||||||
|
return status;
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = ObjectMeshDataSerializer.Read(bytes);
|
||||||
|
return PakObjectReadStatus.Loaded;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
// Structurally malformed blob behind a valid CRC (bake-side bug or
|
||||||
|
// a tamper that recomputed the CRC). External-file input: demote
|
||||||
|
// to missing, log once — never propagate from a lookup.
|
||||||
|
data = null;
|
||||||
|
MarkPayloadCorrupt(
|
||||||
|
key,
|
||||||
|
$"deserialization failed despite matching CRC: " +
|
||||||
|
$"{ex.GetType().Name}: {ex.Message}");
|
||||||
|
return PakObjectReadStatus.Corrupt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies and CRC-verifies one typed blob without interpreting its schema.
|
||||||
|
/// Content-layer typed sources deserialize the returned bytes and call
|
||||||
|
/// <see cref="MarkPayloadCorrupt"/> on structural failure.
|
||||||
|
/// </summary>
|
||||||
|
internal PakObjectReadStatus ReadBlobBytes(
|
||||||
|
ulong key,
|
||||||
|
out byte[]? bytes) {
|
||||||
|
bytes = null;
|
||||||
int index = BinarySearch(key);
|
int index = BinarySearch(key);
|
||||||
if (index < 0) return PakObjectReadStatus.Missing;
|
if (index < 0) return PakObjectReadStatus.Missing;
|
||||||
|
|
||||||
|
|
@ -156,7 +194,7 @@ public sealed class PakReader : IDisposable {
|
||||||
// Single pass (review finding 4): ONE copy out of the map; CRC and
|
// Single pass (review finding 4): ONE copy out of the map; CRC and
|
||||||
// deserialization both run over this same buffer.
|
// deserialization both run over this same buffer.
|
||||||
ref readonly var entry = ref _toc[index];
|
ref readonly var entry = ref _toc[index];
|
||||||
var bytes = new byte[entry.Length];
|
bytes = new byte[entry.Length];
|
||||||
_accessor.ReadArray((long)entry.Offset, bytes, 0, (int)entry.Length);
|
_accessor.ReadArray((long)entry.Offset, bytes, 0, (int)entry.Length);
|
||||||
|
|
||||||
if (!judged) {
|
if (!judged) {
|
||||||
|
|
@ -164,24 +202,21 @@ public sealed class PakReader : IDisposable {
|
||||||
if (actualCrc != entry.Crc32) {
|
if (actualCrc != entry.Crc32) {
|
||||||
_entryVerdictByTocIndex[index] = 0;
|
_entryVerdictByTocIndex[index] = 0;
|
||||||
LogCorruptionOnce(index, $"crc mismatch (expected 0x{entry.Crc32:X8}, got 0x{actualCrc:X8})");
|
LogCorruptionOnce(index, $"crc mismatch (expected 0x{entry.Crc32:X8}, got 0x{actualCrc:X8})");
|
||||||
|
bytes = null;
|
||||||
return PakObjectReadStatus.Corrupt;
|
return PakObjectReadStatus.Corrupt;
|
||||||
}
|
}
|
||||||
_entryVerdictByTocIndex[index] = 1;
|
_entryVerdictByTocIndex[index] = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return PakObjectReadStatus.Loaded;
|
||||||
data = ObjectMeshDataSerializer.Read(bytes);
|
}
|
||||||
return PakObjectReadStatus.Loaded;
|
|
||||||
}
|
internal void MarkPayloadCorrupt(ulong key, string reason) {
|
||||||
catch (Exception ex) {
|
int index = BinarySearch(key);
|
||||||
// Structurally malformed blob behind a valid CRC (bake-side bug or
|
if (index < 0)
|
||||||
// a tamper that recomputed the CRC). External-file input: demote
|
return;
|
||||||
// to missing, log once — never propagate from a lookup.
|
_entryVerdictByTocIndex[index] = 0;
|
||||||
data = null;
|
LogCorruptionOnce(index, reason);
|
||||||
_entryVerdictByTocIndex[index] = 0;
|
|
||||||
LogCorruptionOnce(index, $"deserialization failed despite matching CRC: {ex.GetType().Name}: {ex.Message}");
|
|
||||||
return PakObjectReadStatus.Corrupt;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns the blob's file offset for alignment assertions in tests.</summary>
|
/// <summary>Returns the blob's file offset for alignment assertions in tests.</summary>
|
||||||
|
|
@ -198,6 +233,20 @@ public sealed class PakReader : IDisposable {
|
||||||
return _toc[index];
|
return _toc[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Counts one typed catalog partition without touching payload bytes.
|
||||||
|
/// Used by the offline publisher to prove package completeness.
|
||||||
|
/// </summary>
|
||||||
|
public int CountEntries(PakAssetType type) {
|
||||||
|
byte rawType = (byte)type;
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < _toc.Length; i++) {
|
||||||
|
if ((byte)(_toc[i].Key >> 56) == rawType)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates the complete immutable TOC before an offline bake publishes
|
/// Validates the complete immutable TOC before an offline bake publishes
|
||||||
/// it. This is intentionally stricter than runtime lookup: key order and
|
/// it. This is intentionally stricter than runtime lookup: key order and
|
||||||
|
|
|
||||||
|
|
@ -46,28 +46,51 @@ public sealed class PakWriter : IDisposable {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddBlob(ulong key, ObjectMeshData data) {
|
public void AddBlob(ulong key, ObjectMeshData data) {
|
||||||
ThrowIfFinished();
|
ThrowIfFinished();
|
||||||
if (!_seenKeys.Add(key)) {
|
ReserveKey(key);
|
||||||
throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
long offset = _stream.Position;
|
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
ObjectMeshDataSerializer.Write(data, ms);
|
ObjectMeshDataSerializer.Write(data, ms);
|
||||||
var bytes = ms.ToArray();
|
var bytes = ms.ToArray();
|
||||||
|
AddReservedBlob(key, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an already serialized immutable payload. This is the package seam
|
||||||
|
/// used by typed non-render assets such as flat collision records.
|
||||||
|
/// </summary>
|
||||||
|
public void AddBlob(ulong key, ReadOnlySpan<byte> bytes) {
|
||||||
|
ThrowIfFinished();
|
||||||
|
ReserveKey(key);
|
||||||
|
AddReservedBlob(key, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddReservedBlob(ulong key, ReadOnlySpan<byte> bytes) {
|
||||||
|
if ((ulong)bytes.Length > uint.MaxValue) {
|
||||||
|
throw new ArgumentException(
|
||||||
|
"one pak payload cannot exceed UInt32.MaxValue bytes",
|
||||||
|
nameof(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
long offset = _stream.Position;
|
||||||
_stream.Write(bytes);
|
_stream.Write(bytes);
|
||||||
PadToAlignment();
|
PadToAlignment();
|
||||||
|
|
||||||
var receipt = new PakTocEntry {
|
var receipt = new PakTocEntry {
|
||||||
Key = key,
|
Key = key,
|
||||||
Offset = (ulong)offset,
|
Offset = (ulong)offset,
|
||||||
Length = (uint)bytes.Length,
|
Length = checked((uint)bytes.Length),
|
||||||
Crc32 = Content.Pak.Crc32.Compute(bytes),
|
Crc32 = Content.Pak.Crc32.Compute(bytes),
|
||||||
};
|
};
|
||||||
_tocEntries.Add(receipt);
|
_tocEntries.Add(receipt);
|
||||||
_receiptByKey.Add(key, receipt);
|
_receiptByKey.Add(key, receipt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReserveKey(ulong key) {
|
||||||
|
if (!_seenKeys.Add(key)) {
|
||||||
|
throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a second independently addressable key for an existing physical
|
/// Adds a second independently addressable key for an existing physical
|
||||||
/// blob. The alias receives a normal TOC row with the source blob's exact
|
/// blob. The alias receives a normal TOC row with the source blob's exact
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
using DatReaderWriter.Types;
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
namespace AcDream.Core.Physics;
|
namespace AcDream.Core.Physics;
|
||||||
|
|
@ -15,6 +17,13 @@ namespace AcDream.Core.Physics;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class FlatCollisionAssetBuilder
|
public static class FlatCollisionAssetBuilder
|
||||||
{
|
{
|
||||||
|
private static FlatPhysicsBsp EmptyPhysicsBsp() =>
|
||||||
|
new(
|
||||||
|
-1,
|
||||||
|
ImmutableArray<FlatPhysicsBspNode>.Empty,
|
||||||
|
ImmutableArray<int>.Empty,
|
||||||
|
FlatPolygonTable.Empty);
|
||||||
|
|
||||||
public static FlatPhysicsBsp FlattenPhysicsBsp(
|
public static FlatPhysicsBsp FlattenPhysicsBsp(
|
||||||
PhysicsBSPNode? root,
|
PhysicsBSPNode? root,
|
||||||
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved)
|
IReadOnlyDictionary<ushort, ResolvedPolygon> resolved)
|
||||||
|
|
@ -270,6 +279,53 @@ public static class FlatCollisionAssetBuilder
|
||||||
source.StepDownHeight);
|
source.StepDownHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the exact Setup collision fields used by
|
||||||
|
/// <see cref="PhysicsDataCache.CacheSetup"/> without retaining the parsed
|
||||||
|
/// DAT object. Source list order and every float bit are preserved.
|
||||||
|
/// </summary>
|
||||||
|
public static FlatSetupCollision FlattenSetup(Setup source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
var cylinders = ImmutableArray.CreateBuilder<FlatCollisionCylinder>(
|
||||||
|
source.CylSpheres?.Count ?? 0);
|
||||||
|
if (source.CylSpheres is not null)
|
||||||
|
{
|
||||||
|
foreach (CylSphere cylinder in source.CylSpheres)
|
||||||
|
{
|
||||||
|
if (cylinder is null)
|
||||||
|
throw new InvalidDataException("Setup cylinder row is null.");
|
||||||
|
cylinders.Add(new FlatCollisionCylinder(
|
||||||
|
cylinder.Origin,
|
||||||
|
cylinder.Radius,
|
||||||
|
cylinder.Height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var spheres = ImmutableArray.CreateBuilder<FlatCollisionSphere>(
|
||||||
|
source.Spheres?.Count ?? 0);
|
||||||
|
if (source.Spheres is not null)
|
||||||
|
{
|
||||||
|
foreach (Sphere sphere in source.Spheres)
|
||||||
|
{
|
||||||
|
if (sphere is null)
|
||||||
|
throw new InvalidDataException("Setup sphere row is null.");
|
||||||
|
spheres.Add(new FlatCollisionSphere(
|
||||||
|
sphere.Origin,
|
||||||
|
sphere.Radius));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FlatSetupCollision(
|
||||||
|
cylinders.MoveToImmutable(),
|
||||||
|
spheres.MoveToImmutable(),
|
||||||
|
source.Height,
|
||||||
|
source.Radius,
|
||||||
|
source.StepUpHeight,
|
||||||
|
source.StepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
public static FlatGfxObjCollisionAsset FlattenGfxObj(
|
public static FlatGfxObjCollisionAsset FlattenGfxObj(
|
||||||
GfxObjPhysics source,
|
GfxObjPhysics source,
|
||||||
GfxObjVisualBounds? visualBounds = null)
|
GfxObjVisualBounds? visualBounds = null)
|
||||||
|
|
@ -297,6 +353,139 @@ public static class FlatCollisionAssetBuilder
|
||||||
flatVisualBounds);
|
flatVisualBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares one parsed GfxObj and drops the source graph. Objects without
|
||||||
|
/// a retail physics BSP still retain their visual bounds because the
|
||||||
|
/// current runtime uses those bounds as its collision fallback.
|
||||||
|
/// </summary>
|
||||||
|
public static FlatGfxObjCollisionAsset FlattenGfxObj(GfxObj source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
GfxObjVisualBounds? visualBounds = source.VertexArray is null
|
||||||
|
? null
|
||||||
|
: PhysicsDataCache.ComputeVisualBounds(source.VertexArray);
|
||||||
|
FlatGfxObjVisualBounds? flatVisualBounds = visualBounds is null
|
||||||
|
? null
|
||||||
|
: new FlatGfxObjVisualBounds(
|
||||||
|
visualBounds.Min,
|
||||||
|
visualBounds.Max,
|
||||||
|
visualBounds.Center,
|
||||||
|
visualBounds.Radius,
|
||||||
|
visualBounds.HalfExtents);
|
||||||
|
|
||||||
|
bool hasPhysics =
|
||||||
|
source.Flags.HasFlag(GfxObjFlags.HasPhysics)
|
||||||
|
&& source.PhysicsBSP?.Root is not null
|
||||||
|
&& source.VertexArray is not null;
|
||||||
|
if (!hasPhysics)
|
||||||
|
{
|
||||||
|
return new FlatGfxObjCollisionAsset(
|
||||||
|
EmptyPhysicsBsp(),
|
||||||
|
null,
|
||||||
|
flatVisualBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||||
|
PhysicsDataCache.ResolvePolygons(
|
||||||
|
source.PhysicsPolygons,
|
||||||
|
source.VertexArray!);
|
||||||
|
Sphere boundingSphere = source.PhysicsBSP!.Root!.BoundingSphere
|
||||||
|
?? throw new InvalidDataException(
|
||||||
|
"GfxObj physics BSP root has no bounding sphere.");
|
||||||
|
|
||||||
|
return new FlatGfxObjCollisionAsset(
|
||||||
|
FlattenPhysicsBsp(source.PhysicsBSP.Root, resolved),
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
boundingSphere.Origin,
|
||||||
|
boundingSphere.Radius),
|
||||||
|
flatVisualBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares reusable CellStruct collision geometry independently of any
|
||||||
|
/// EnvCell placement or topology.
|
||||||
|
/// </summary>
|
||||||
|
public static FlatCellStructureCollisionAsset FlattenCellStructure(
|
||||||
|
CellStruct source)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
||||||
|
FlatPhysicsBsp physicsBsp;
|
||||||
|
if (source.PhysicsBSP?.Root is null)
|
||||||
|
{
|
||||||
|
physicsBsp = EmptyPhysicsBsp();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dictionary<ushort, ResolvedPolygon> resolved =
|
||||||
|
PhysicsDataCache.ResolvePolygons(
|
||||||
|
source.PhysicsPolygons,
|
||||||
|
source.VertexArray);
|
||||||
|
physicsBsp = FlattenPhysicsBsp(
|
||||||
|
source.PhysicsBSP.Root,
|
||||||
|
resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<ushort, ResolvedPolygon> portalResolved =
|
||||||
|
PhysicsDataCache.ResolvePolygons(
|
||||||
|
source.Polygons,
|
||||||
|
source.VertexArray);
|
||||||
|
return new FlatCellStructureCollisionAsset(
|
||||||
|
physicsBsp,
|
||||||
|
FlattenCellContainmentBsp(source.CellBSP?.Root),
|
||||||
|
FlattenPolygonTable(portalResolved));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the per-cell portion of collision publication. Other-cell IDs
|
||||||
|
/// are expanded with the owning landblock exactly as
|
||||||
|
/// <see cref="PhysicsDataCache.CacheCellStruct"/> does.
|
||||||
|
/// </summary>
|
||||||
|
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||||
|
uint envCellId,
|
||||||
|
EnvCell source,
|
||||||
|
FlatPolygonTable portalPolygons)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||||
|
|
||||||
|
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||||
|
source.CellPortals.Count);
|
||||||
|
foreach (CellPortal portal in source.CellPortals)
|
||||||
|
{
|
||||||
|
ushort polygonId = portal.PolygonId;
|
||||||
|
if (!portalPolygons.TryFindPolygonIndex(
|
||||||
|
polygonId,
|
||||||
|
out int polygonIndex))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"EnvCell 0x{envCellId:X8} portal references missing " +
|
||||||
|
$"polygon 0x{polygonId:X4}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
portals.Add(new FlatEnvCellPortal(
|
||||||
|
portal.OtherCellId,
|
||||||
|
polygonId,
|
||||||
|
(ushort)portal.Flags,
|
||||||
|
polygonIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint prefix = envCellId & 0xFFFF_0000u;
|
||||||
|
uint[] visibleIds = source.VisibleCells is null
|
||||||
|
? []
|
||||||
|
: source.VisibleCells
|
||||||
|
.Select(id => prefix | id)
|
||||||
|
.Distinct()
|
||||||
|
.Order()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return new FlatEnvCellTopology(
|
||||||
|
portals.MoveToImmutable(),
|
||||||
|
ImmutableArray.Create(visibleIds),
|
||||||
|
source.Flags.HasFlag(EnvCellFlags.SeenOutside));
|
||||||
|
}
|
||||||
|
|
||||||
public static FlatCellCollisionAsset FlattenCell(CellPhysics source)
|
public static FlatCellCollisionAsset FlattenCell(CellPhysics source)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(source);
|
ArgumentNullException.ThrowIfNull(source);
|
||||||
|
|
@ -310,9 +499,31 @@ public static class FlatCollisionAssetBuilder
|
||||||
source.PortalPolygons
|
source.PortalPolygons
|
||||||
?? new Dictionary<ushort, ResolvedPolygon>());
|
?? new Dictionary<ushort, ResolvedPolygon>());
|
||||||
|
|
||||||
var portals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
FlatEnvCellTopology topology = FlattenEnvCellTopology(
|
||||||
source.Portals.Count);
|
source.Portals,
|
||||||
foreach (PortalInfo portal in source.Portals)
|
source.VisibleCellIds,
|
||||||
|
source.SeenOutside,
|
||||||
|
portalPolygons);
|
||||||
|
var structure = new FlatCellStructureCollisionAsset(
|
||||||
|
physicsBsp,
|
||||||
|
containmentBsp,
|
||||||
|
portalPolygons);
|
||||||
|
return new FlatCellCollisionAsset(structure, topology);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FlatEnvCellTopology FlattenEnvCellTopology(
|
||||||
|
IReadOnlyList<PortalInfo> portals,
|
||||||
|
IEnumerable<uint> visibleCellIds,
|
||||||
|
bool seenOutside,
|
||||||
|
FlatPolygonTable portalPolygons)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(portals);
|
||||||
|
ArgumentNullException.ThrowIfNull(visibleCellIds);
|
||||||
|
ArgumentNullException.ThrowIfNull(portalPolygons);
|
||||||
|
|
||||||
|
var flatPortals = ImmutableArray.CreateBuilder<FlatEnvCellPortal>(
|
||||||
|
portals.Count);
|
||||||
|
foreach (PortalInfo portal in portals)
|
||||||
{
|
{
|
||||||
if (!portalPolygons.TryFindPolygonIndex(
|
if (!portalPolygons.TryFindPolygonIndex(
|
||||||
portal.PolygonId,
|
portal.PolygonId,
|
||||||
|
|
@ -323,24 +534,21 @@ public static class FlatCollisionAssetBuilder
|
||||||
$"0x{portal.PolygonId:X4}.");
|
$"0x{portal.PolygonId:X4}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
portals.Add(new FlatEnvCellPortal(
|
flatPortals.Add(new FlatEnvCellPortal(
|
||||||
portal.OtherCellId,
|
portal.OtherCellId,
|
||||||
portal.PolygonId,
|
portal.PolygonId,
|
||||||
portal.Flags,
|
portal.Flags,
|
||||||
polygonIndex));
|
polygonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint[] visibleIds = source.VisibleCellIds.ToArray();
|
uint[] visibleIds = visibleCellIds
|
||||||
Array.Sort(visibleIds);
|
.Distinct()
|
||||||
var topology = new FlatEnvCellTopology(
|
.Order()
|
||||||
portals.MoveToImmutable(),
|
.ToArray();
|
||||||
|
return new FlatEnvCellTopology(
|
||||||
|
flatPortals.MoveToImmutable(),
|
||||||
ImmutableArray.Create(visibleIds),
|
ImmutableArray.Create(visibleIds),
|
||||||
source.SeenOutside);
|
seenOutside);
|
||||||
var structure = new FlatCellStructureCollisionAsset(
|
|
||||||
physicsBsp,
|
|
||||||
containmentBsp,
|
|
||||||
portalPolygons);
|
|
||||||
return new FlatCellCollisionAsset(structure, topology);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AttachChild(
|
private static void AttachChild(
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ public sealed class PhysicsDataCache
|
||||||
/// Used as a fallback collision shape for entities whose Setup has no
|
/// Used as a fallback collision shape for entities whose Setup has no
|
||||||
/// physics data — we approximate collision using the visual extent.
|
/// physics data — we approximate collision using the visual extent.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
internal static GfxObjVisualBounds ComputeVisualBounds(VertexArray vertexArray)
|
||||||
{
|
{
|
||||||
if (vertexArray.Vertices == null || vertexArray.Vertices.Count == 0)
|
if (vertexArray.Vertices == null || vertexArray.Vertices.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,43 @@ public sealed class BakeDeterminismTests : IDisposable {
|
||||||
$"pak sizes differ between runs: {bytesA.Length} vs {bytesB.Length} bytes");
|
$"pak sizes differ between runs: {bytesA.Length} vs {bytesB.Length} bytes");
|
||||||
Assert.True(bytesA.AsSpan().SequenceEqual(bytesB),
|
Assert.True(bytesA.AsSpan().SequenceEqual(bytesB),
|
||||||
"two bakes of the same id set produced different bytes — the sorted-batching determinism guarantee is broken");
|
"two bakes of the same id set produced different bytes — the sorted-batching determinism guarantee is broken");
|
||||||
|
using var reader = new PakReader(pathA);
|
||||||
|
foreach (uint gfxId in new[]
|
||||||
|
{
|
||||||
|
0x01000001u,
|
||||||
|
0x010002B4u,
|
||||||
|
0x010008A8u,
|
||||||
|
0x010014C3u,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Assert.True(reader.ContainsKey(PakKey.Compose(
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
gfxId)));
|
||||||
|
}
|
||||||
|
foreach (uint setupId in new[]
|
||||||
|
{
|
||||||
|
0x02000001u,
|
||||||
|
0x020019FFu,
|
||||||
|
0x020005D8u,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Assert.True(reader.ContainsKey(PakKey.Compose(
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
setupId)));
|
||||||
|
}
|
||||||
|
foreach (uint cellId in new[]
|
||||||
|
{
|
||||||
|
0xA9B40100u,
|
||||||
|
0xA9B40101u,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Assert.True(reader.ContainsKey(PakKey.Compose(
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
cellId)));
|
||||||
|
Assert.True(reader.ContainsKey(PakKey.Compose(
|
||||||
|
PakAssetType.EnvCellTopology,
|
||||||
|
cellId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -111,7 +148,11 @@ public sealed class BakeDeterminismTests : IDisposable {
|
||||||
Assert.Equal(8, reportA.EnvCellKeys);
|
Assert.Equal(8, reportA.EnvCellKeys);
|
||||||
Assert.Equal(1, reportA.UniqueEnvCellGeometries);
|
Assert.Equal(1, reportA.UniqueEnvCellGeometries);
|
||||||
Assert.Equal(7, reportA.EnvCellAliases);
|
Assert.Equal(7, reportA.EnvCellAliases);
|
||||||
Assert.Equal(1, reportA.PhysicalBlobs);
|
Assert.Equal(8, reportA.CellStructureCollisionKeys);
|
||||||
|
Assert.Equal(1, reportA.UniqueCellStructureCollisions);
|
||||||
|
Assert.Equal(7, reportA.CellStructureCollisionAliases);
|
||||||
|
Assert.Equal(8, reportA.EnvCellTopologyKeys);
|
||||||
|
Assert.Equal(10, reportA.PhysicalBlobs);
|
||||||
Assert.Equal(reportA.TotalKeys, reportB.TotalKeys);
|
Assert.Equal(reportA.TotalKeys, reportB.TotalKeys);
|
||||||
Assert.True(File.ReadAllBytes(pathA).AsSpan().SequenceEqual(File.ReadAllBytes(pathB)));
|
Assert.True(File.ReadAllBytes(pathA).AsSpan().SequenceEqual(File.ReadAllBytes(pathB)));
|
||||||
|
|
||||||
|
|
@ -124,6 +165,27 @@ public sealed class BakeDeterminismTests : IDisposable {
|
||||||
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Length, receipt.Length));
|
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Length, receipt.Length));
|
||||||
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Crc32, receipt.Crc32));
|
Assert.All(receipts, receipt => Assert.Equal(receipts[0].Crc32, receipt.Crc32));
|
||||||
|
|
||||||
|
var collisionReceipts = ids
|
||||||
|
.Select(id => reader.GetTocEntryForTest(
|
||||||
|
PakKey.Compose(PakAssetType.CellStructureCollision, id)))
|
||||||
|
.ToArray();
|
||||||
|
Assert.All(
|
||||||
|
collisionReceipts,
|
||||||
|
receipt => Assert.Equal(
|
||||||
|
collisionReceipts[0].Offset,
|
||||||
|
receipt.Offset));
|
||||||
|
Assert.All(
|
||||||
|
collisionReceipts,
|
||||||
|
receipt => Assert.Equal(
|
||||||
|
collisionReceipts[0].Length,
|
||||||
|
receipt.Length));
|
||||||
|
Assert.Equal(
|
||||||
|
ids.Count,
|
||||||
|
ids.Select(id => reader.GetTocEntryForTest(
|
||||||
|
PakKey.Compose(PakAssetType.EnvCellTopology, id)).Offset)
|
||||||
|
.Distinct()
|
||||||
|
.Count());
|
||||||
|
|
||||||
Assert.True(
|
Assert.True(
|
||||||
reader.TryReadObjectMeshData(
|
reader.TryReadObjectMeshData(
|
||||||
PakKey.Compose(PakAssetType.EnvCellMesh, ids.Min()),
|
PakKey.Compose(PakAssetType.EnvCellMesh, ids.Min()),
|
||||||
|
|
@ -132,4 +194,34 @@ public sealed class BakeDeterminismTests : IDisposable {
|
||||||
Assert.Equal(0xE000_0000_0000_0000UL,
|
Assert.Equal(0xE000_0000_0000_0000UL,
|
||||||
geometry.ObjectId & 0xF000_0000_0000_0000UL);
|
geometry.ObjectId & 0xF000_0000_0000_0000UL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Bake_CancellationCannotReplaceLastGoodPackage()
|
||||||
|
{
|
||||||
|
string? datDir = ResolveDatDir();
|
||||||
|
if (datDir is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string destination = NewTempPakPath();
|
||||||
|
byte[] lastGood = [0x41, 0x43, 0x50, 0x4B, 1, 2, 3, 4];
|
||||||
|
File.WriteAllBytes(destination, lastGood);
|
||||||
|
using var cancellation = new CancellationTokenSource();
|
||||||
|
cancellation.CancelAfter(TimeSpan.FromMilliseconds(20));
|
||||||
|
|
||||||
|
Assert.Throws<OperationCanceledException>(
|
||||||
|
() => BakeRunner.RunDetailed(
|
||||||
|
new BakeOptions
|
||||||
|
{
|
||||||
|
DatDir = datDir,
|
||||||
|
OutPath = destination,
|
||||||
|
Threads = 2,
|
||||||
|
CancellationToken = cancellation.Token,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(lastGood, File.ReadAllBytes(destination));
|
||||||
|
Assert.Empty(
|
||||||
|
Directory.EnumerateFiles(
|
||||||
|
Path.GetDirectoryName(destination)!,
|
||||||
|
$".{Path.GetFileName(destination)}.*.tmp"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,33 @@ public sealed class BakeOutputTransactionTests : IDisposable
|
||||||
() => BakeArtifactValidator.Validate(pak, expected, expectedTocCount: 1));
|
() => BakeArtifactValidator.Validate(pak, expected, expectedTocCount: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ArtifactValidator_RejectsIncompleteTypedCatalog()
|
||||||
|
{
|
||||||
|
string pak = Path.Combine(_directory, "typed-content.pak");
|
||||||
|
var expected = Header();
|
||||||
|
using (var writer = new PakWriter(pak, expected))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.GfxObjMesh, 1),
|
||||||
|
new ObjectMeshData { ObjectId = 1 });
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedTypes = new Dictionary<PakAssetType, int>
|
||||||
|
{
|
||||||
|
[PakAssetType.GfxObjMesh] = 1,
|
||||||
|
[PakAssetType.GfxObjCollision] = 1,
|
||||||
|
};
|
||||||
|
InvalidDataException exception = Assert.Throws<InvalidDataException>(
|
||||||
|
() => BakeArtifactValidator.Validate(
|
||||||
|
pak,
|
||||||
|
expected,
|
||||||
|
expectedTocCount: 1,
|
||||||
|
expectedTypeCounts: expectedTypes));
|
||||||
|
Assert.Contains("GfxObjCollision", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
private PakHeader Header() =>
|
private PakHeader Header() =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
24
tests/AcDream.Bake.Tests/ExactPayloadAliasCatalogTests.cs
Normal file
24
tests/AcDream.Bake.Tests/ExactPayloadAliasCatalogTests.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace AcDream.Bake.Tests;
|
||||||
|
|
||||||
|
public sealed class ExactPayloadAliasCatalogTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AliasRequiresCompleteByteEquality()
|
||||||
|
{
|
||||||
|
var catalog = new ExactPayloadAliasCatalog();
|
||||||
|
byte[] primary = [1, 2, 3, 4, 5];
|
||||||
|
catalog.Add(0x1234UL, primary);
|
||||||
|
|
||||||
|
Assert.True(catalog.TryFind(
|
||||||
|
[1, 2, 3, 4, 5],
|
||||||
|
out ulong primaryKey));
|
||||||
|
Assert.Equal(0x1234UL, primaryKey);
|
||||||
|
|
||||||
|
Assert.False(catalog.TryFind(
|
||||||
|
[1, 2, 3, 4, 4],
|
||||||
|
out _));
|
||||||
|
Assert.False(catalog.TryFind(
|
||||||
|
[1, 2, 3, 4, 5, 0],
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
}
|
||||||
512
tests/AcDream.Content.Tests/FlatCollisionAssetSerializerTests.cs
Normal file
512
tests/AcDream.Content.Tests/FlatCollisionAssetSerializerTests.cs
Normal file
|
|
@ -0,0 +1,512 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
|
||||||
|
namespace AcDream.Content.Tests;
|
||||||
|
|
||||||
|
public sealed class FlatCollisionAssetSerializerTests : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly PreparedAssetCatalogIdentity Identity =
|
||||||
|
new(10, 20, 30, 40, PakFormat.CurrentBakeToolVersion);
|
||||||
|
|
||||||
|
private readonly List<string> _paths = [];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EveryPayload_RoundTripsToByteIdenticalRepresentation()
|
||||||
|
{
|
||||||
|
FlatGfxObjCollisionAsset gfx = GfxAsset();
|
||||||
|
FlatSetupCollision setup = SetupAsset();
|
||||||
|
FlatCellStructureCollisionAsset structure = CellStructureAsset();
|
||||||
|
FlatEnvCellTopology topology = TopologyAsset();
|
||||||
|
|
||||||
|
byte[] gfxBytes = FlatCollisionAssetSerializer.Serialize(gfx);
|
||||||
|
byte[] setupBytes = FlatCollisionAssetSerializer.Serialize(setup);
|
||||||
|
byte[] structureBytes =
|
||||||
|
FlatCollisionAssetSerializer.Serialize(structure);
|
||||||
|
byte[] topologyBytes =
|
||||||
|
FlatCollisionAssetSerializer.Serialize(topology);
|
||||||
|
|
||||||
|
FlatGfxObjCollisionAsset readGfx =
|
||||||
|
FlatCollisionAssetSerializer.DeserializeGfxObj(gfxBytes);
|
||||||
|
FlatSetupCollision readSetup =
|
||||||
|
FlatCollisionAssetSerializer.DeserializeSetup(setupBytes);
|
||||||
|
FlatCellStructureCollisionAsset readStructure =
|
||||||
|
FlatCollisionAssetSerializer.DeserializeCellStructure(
|
||||||
|
structureBytes);
|
||||||
|
FlatEnvCellTopology readTopology =
|
||||||
|
FlatCollisionAssetSerializer.DeserializeEnvCellTopology(
|
||||||
|
topologyBytes);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
gfxBytes,
|
||||||
|
FlatCollisionAssetSerializer.Serialize(readGfx));
|
||||||
|
Assert.Equal(
|
||||||
|
setupBytes,
|
||||||
|
FlatCollisionAssetSerializer.Serialize(readSetup));
|
||||||
|
Assert.Equal(
|
||||||
|
structureBytes,
|
||||||
|
FlatCollisionAssetSerializer.Serialize(readStructure));
|
||||||
|
Assert.Equal(
|
||||||
|
topologyBytes,
|
||||||
|
FlatCollisionAssetSerializer.Serialize(readTopology));
|
||||||
|
|
||||||
|
AssertFloatBits(
|
||||||
|
gfx.VisualBounds!.Value.Radius,
|
||||||
|
readGfx.VisualBounds!.Value.Radius);
|
||||||
|
AssertFloatBits(
|
||||||
|
setup.StepDownHeight,
|
||||||
|
readSetup.StepDownHeight);
|
||||||
|
AssertFloatBits(
|
||||||
|
structure.PhysicsBsp.Nodes[0].SplittingPlane.D,
|
||||||
|
readStructure.PhysicsBsp.Nodes[0].SplittingPlane.D);
|
||||||
|
Assert.Equal(
|
||||||
|
topology.VisibleCellIds.AsEnumerable(),
|
||||||
|
readTopology.VisibleCellIds.AsEnumerable());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encoding_IsLittleEndianAndPreservesSpecialFloatBits()
|
||||||
|
{
|
||||||
|
float special = BitConverter.Int32BitsToSingle(
|
||||||
|
unchecked((int)0xFFC0_1234u));
|
||||||
|
var setup = new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
special,
|
||||||
|
-0f,
|
||||||
|
float.PositiveInfinity,
|
||||||
|
float.NegativeInfinity);
|
||||||
|
|
||||||
|
byte[] bytes = FlatCollisionAssetSerializer.Serialize(setup);
|
||||||
|
|
||||||
|
// Header is "ACCL", kind=Setup, schema=1, reserved=0. Counts follow,
|
||||||
|
// then the exact little-endian IEEE-754 bits for Height.
|
||||||
|
Assert.Equal(
|
||||||
|
new byte[]
|
||||||
|
{
|
||||||
|
0x41, 0x43, 0x43, 0x4C,
|
||||||
|
0x02, 0x01, 0x00, 0x00,
|
||||||
|
},
|
||||||
|
bytes[..8]);
|
||||||
|
Assert.Equal(
|
||||||
|
new byte[] { 0x34, 0x12, 0xC0, 0xFF },
|
||||||
|
bytes[16..20]);
|
||||||
|
|
||||||
|
FlatSetupCollision read =
|
||||||
|
FlatCollisionAssetSerializer.DeserializeSetup(bytes);
|
||||||
|
AssertFloatBits(special, read.Height);
|
||||||
|
AssertFloatBits(-0f, read.Radius);
|
||||||
|
AssertFloatBits(float.PositiveInfinity, read.StepUpHeight);
|
||||||
|
AssertFloatBits(float.NegativeInfinity, read.StepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Readers_RejectWrongKindTruncationTrailingAndInvalidCounts()
|
||||||
|
{
|
||||||
|
byte[] setup = FlatCollisionAssetSerializer.Serialize(SetupAsset());
|
||||||
|
|
||||||
|
Assert.Throws<InvalidDataException>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeGfxObj(setup));
|
||||||
|
|
||||||
|
for (int length = 0; length < setup.Length; length++)
|
||||||
|
{
|
||||||
|
Assert.ThrowsAny<Exception>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||||
|
setup.AsSpan(0, length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Throws<InvalidDataException>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||||
|
[.. setup, 0x00]));
|
||||||
|
|
||||||
|
byte[] negativeCount = (byte[])setup.Clone();
|
||||||
|
negativeCount[8] = 0xFF;
|
||||||
|
negativeCount[9] = 0xFF;
|
||||||
|
negativeCount[10] = 0xFF;
|
||||||
|
negativeCount[11] = 0xFF;
|
||||||
|
Assert.Throws<InvalidDataException>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||||
|
negativeCount));
|
||||||
|
|
||||||
|
byte[] impossibleCombinedCounts = (byte[])setup.Clone();
|
||||||
|
impossibleCombinedCounts[8] = 0x03;
|
||||||
|
impossibleCombinedCounts[12] = 0x03;
|
||||||
|
Assert.Throws<InvalidDataException>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeSetup(
|
||||||
|
impossibleCombinedCounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SerializeAndDeserialize_PropagatePreCancellation()
|
||||||
|
{
|
||||||
|
using var cancellation = new CancellationTokenSource();
|
||||||
|
cancellation.Cancel();
|
||||||
|
|
||||||
|
Assert.Throws<OperationCanceledException>(
|
||||||
|
() => FlatCollisionAssetSerializer.Serialize(
|
||||||
|
GfxAsset(),
|
||||||
|
cancellation.Token));
|
||||||
|
byte[] bytes =
|
||||||
|
FlatCollisionAssetSerializer.Serialize(GfxAsset());
|
||||||
|
Assert.Throws<OperationCanceledException>(
|
||||||
|
() => FlatCollisionAssetSerializer.DeserializeGfxObj(
|
||||||
|
bytes,
|
||||||
|
cancellation.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedSource_ReadsAllCollisionKindsThroughSamePackageOwner()
|
||||||
|
{
|
||||||
|
const uint gfxId = 0x0100_0001u;
|
||||||
|
const uint setupId = 0x0200_0001u;
|
||||||
|
const uint cellId = 0xA9B4_0101u;
|
||||||
|
string path = NewPath();
|
||||||
|
using (var writer = new PakWriter(path, Header()))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.GfxObjCollision, gfxId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(GfxAsset()));
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(
|
||||||
|
PakAssetType.CellStructureCollision,
|
||||||
|
cellId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(CellStructureAsset()));
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.EnvCellTopology, cellId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(TopologyAsset()));
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var owner = new PakPreparedAssetSource(path, Identity);
|
||||||
|
IPreparedAssetSource renderSource = owner;
|
||||||
|
IPreparedCollisionSource collisionSource = owner;
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetPresence.Available,
|
||||||
|
collisionSource.ProbeCollision(
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
gfxId));
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
collisionSource.ReadGfxObjCollision(gfxId).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
collisionSource.ReadSetupCollision(setupId).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
collisionSource.ReadCellStructureCollision(cellId).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
collisionSource.ReadEnvCellTopology(cellId).Status);
|
||||||
|
Assert.Equal(4, collisionSource.CollisionStats.Loaded);
|
||||||
|
Assert.Equal(0, renderSource.Stats.Reads);
|
||||||
|
Assert.Equal(new FileInfo(path).Length, renderSource.MappedVirtualBytes);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(
|
||||||
|
() => renderSource.Probe(
|
||||||
|
PakAssetType.GfxObjCollision,
|
||||||
|
gfxId));
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(
|
||||||
|
() => collisionSource.ProbeCollision(
|
||||||
|
PakAssetType.GfxObjMesh,
|
||||||
|
gfxId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedSource_DistinguishesMissingCrcAndSchemaCorruption()
|
||||||
|
{
|
||||||
|
const uint setupId = 0x0200_0001u;
|
||||||
|
string missingPath = NewPath();
|
||||||
|
using (var writer = new PakWriter(missingPath, Header()))
|
||||||
|
writer.Finish();
|
||||||
|
|
||||||
|
using (var source = new PakPreparedAssetSource(
|
||||||
|
missingPath,
|
||||||
|
Identity))
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Missing,
|
||||||
|
source.ReadSetupCollision(setupId).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
string schemaPath = NewPath();
|
||||||
|
byte[] malformed =
|
||||||
|
[.. FlatCollisionAssetSerializer.Serialize(SetupAsset()), 0x7F];
|
||||||
|
using (var writer = new PakWriter(schemaPath, Header()))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||||
|
malformed);
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var source = new PakPreparedAssetSource(schemaPath, Identity))
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Corrupt,
|
||||||
|
source.ReadSetupCollision(setupId).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetPresence.Corrupt,
|
||||||
|
source.ProbeCollision(
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
setupId));
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Corrupt,
|
||||||
|
source.ReadSetupCollision(setupId).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
string crcPath = NewPath();
|
||||||
|
using (var writer = new PakWriter(crcPath, Header()))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
FlipFirstBlobByte(crcPath);
|
||||||
|
using var corruptSource =
|
||||||
|
new PakPreparedAssetSource(crcPath, Identity);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Corrupt,
|
||||||
|
corruptSource.ReadSetupCollision(setupId).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedSource_ConcurrentReadsRemainIndependentAndComplete()
|
||||||
|
{
|
||||||
|
const uint setupId = 0x0200_0001u;
|
||||||
|
string path = NewPath();
|
||||||
|
using (var writer = new PakWriter(path, Header()))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
PakKey.Compose(PakAssetType.SetupCollision, setupId),
|
||||||
|
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var source = new PakPreparedAssetSource(path, Identity);
|
||||||
|
Parallel.For(
|
||||||
|
0,
|
||||||
|
64,
|
||||||
|
_ =>
|
||||||
|
{
|
||||||
|
PreparedCollisionReadResult<FlatSetupCollision> result =
|
||||||
|
source.ReadSetupCollision(setupId);
|
||||||
|
Assert.Equal(PreparedAssetReadStatus.Loaded, result.Status);
|
||||||
|
Assert.NotNull(result.Data);
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Equal(64, source.CollisionStats.Reads);
|
||||||
|
Assert.Equal(64, source.CollisionStats.Loaded);
|
||||||
|
Assert.Equal(0, source.CollisionStats.Corrupt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RenderDecoder_CannotPoisonCollisionTypedEntry()
|
||||||
|
{
|
||||||
|
const uint setupId = 0x0200_0001u;
|
||||||
|
string path = NewPath();
|
||||||
|
ulong key = PakKey.Compose(
|
||||||
|
PakAssetType.SetupCollision,
|
||||||
|
setupId);
|
||||||
|
using (var writer = new PakWriter(path, Header()))
|
||||||
|
{
|
||||||
|
writer.AddBlob(
|
||||||
|
key,
|
||||||
|
FlatCollisionAssetSerializer.Serialize(SetupAsset()));
|
||||||
|
writer.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var reader = new PakReader(path))
|
||||||
|
{
|
||||||
|
Assert.Equal(
|
||||||
|
PakObjectReadStatus.Missing,
|
||||||
|
reader.ReadObjectMeshData(key, out ObjectMeshData? mesh));
|
||||||
|
Assert.Null(mesh);
|
||||||
|
Assert.Equal(PakEntryState.Available, reader.ProbeEntry(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
using var source = new PakPreparedAssetSource(path, Identity);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
source.ReadSetupCollision(setupId).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FlatGfxObjCollisionAsset GfxAsset() =>
|
||||||
|
new(
|
||||||
|
PhysicsBsp(),
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
new Vector3(Float(0x3F80_0001), -0f, 3f),
|
||||||
|
Float(0x7FC0_1234)),
|
||||||
|
new FlatGfxObjVisualBounds(
|
||||||
|
new Vector3(-1, -2, -3),
|
||||||
|
new Vector3(4, 5, 6),
|
||||||
|
new Vector3(1.5f),
|
||||||
|
Float(0x4123_4567),
|
||||||
|
new Vector3(2.5f, 3.5f, 4.5f)));
|
||||||
|
|
||||||
|
private static FlatSetupCollision SetupAsset() =>
|
||||||
|
new(
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new FlatCollisionCylinder(
|
||||||
|
new Vector3(1, 2, 3),
|
||||||
|
Float(0x3F00_0001),
|
||||||
|
Float(0x4000_0001))),
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
new Vector3(-1, -2, -3),
|
||||||
|
Float(0x4040_0001))),
|
||||||
|
Float(0x4080_0001),
|
||||||
|
Float(0x40A0_0001),
|
||||||
|
Float(0x40C0_0001),
|
||||||
|
Float(0x40E0_0001));
|
||||||
|
|
||||||
|
private static FlatCellStructureCollisionAsset CellStructureAsset() =>
|
||||||
|
new(
|
||||||
|
PhysicsBsp(),
|
||||||
|
new FlatCellContainmentBsp(
|
||||||
|
0,
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new FlatCellBspNode(
|
||||||
|
BSPNodeType.BPIN,
|
||||||
|
new Plane(Vector3.UnitX, -1),
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
0),
|
||||||
|
new FlatCellBspNode(
|
||||||
|
BSPNodeType.Leaf,
|
||||||
|
new Plane(Vector3.UnitY, -2),
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
1),
|
||||||
|
new FlatCellBspNode(
|
||||||
|
BSPNodeType.Leaf,
|
||||||
|
new Plane(Vector3.UnitZ, -3),
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
2))),
|
||||||
|
PolygonTable());
|
||||||
|
|
||||||
|
private static FlatEnvCellTopology TopologyAsset() =>
|
||||||
|
new(
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new FlatEnvCellPortal(0x0102, 3, 4, 0),
|
||||||
|
new FlatEnvCellPortal(0x0103, 8, 2, 1)),
|
||||||
|
ImmutableArray.Create(0xA9B4_0101u, 0xA9B4_0102u),
|
||||||
|
true);
|
||||||
|
|
||||||
|
private static FlatPhysicsBsp PhysicsBsp() =>
|
||||||
|
new(
|
||||||
|
0,
|
||||||
|
ImmutableArray.Create(
|
||||||
|
Node(BSPNodeType.BPIN, 1, 2, 0, 0, 0),
|
||||||
|
Node(BSPNodeType.Leaf, -1, -1, 1, 0, 1),
|
||||||
|
Node(BSPNodeType.Leaf, -1, -1, 2, 1, 1)),
|
||||||
|
ImmutableArray.Create(0, 1),
|
||||||
|
PolygonTable());
|
||||||
|
|
||||||
|
private static FlatPhysicsBspNode Node(
|
||||||
|
BSPNodeType type,
|
||||||
|
int positive,
|
||||||
|
int negative,
|
||||||
|
int leaf,
|
||||||
|
int polygonStart,
|
||||||
|
int polygonCount) =>
|
||||||
|
new(
|
||||||
|
type,
|
||||||
|
new Plane(
|
||||||
|
new Vector3(1 + leaf, 2 + leaf, 3 + leaf),
|
||||||
|
Float(0x3F80_0000u + (uint)leaf)),
|
||||||
|
positive,
|
||||||
|
negative,
|
||||||
|
leaf,
|
||||||
|
leaf & 1,
|
||||||
|
new FlatCollisionSphere(
|
||||||
|
new Vector3(4 + leaf, 5 + leaf, 6 + leaf),
|
||||||
|
7 + leaf),
|
||||||
|
new FlatIndexRange(polygonStart, polygonCount));
|
||||||
|
|
||||||
|
private static FlatPolygonTable PolygonTable() =>
|
||||||
|
new(
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new FlatCollisionPolygon(
|
||||||
|
3,
|
||||||
|
new Plane(Vector3.UnitX, -1),
|
||||||
|
CullMode.Clockwise,
|
||||||
|
3,
|
||||||
|
new FlatIndexRange(0, 3)),
|
||||||
|
new FlatCollisionPolygon(
|
||||||
|
8,
|
||||||
|
new Plane(Vector3.UnitY, -2),
|
||||||
|
CullMode.CounterClockwise,
|
||||||
|
3,
|
||||||
|
new FlatIndexRange(3, 3))),
|
||||||
|
ImmutableArray.Create(
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.UnitY,
|
||||||
|
Vector3.UnitZ,
|
||||||
|
Vector3.One,
|
||||||
|
Vector3.UnitX,
|
||||||
|
Vector3.UnitZ));
|
||||||
|
|
||||||
|
private string NewPath()
|
||||||
|
{
|
||||||
|
string path = Path.Combine(
|
||||||
|
Path.GetTempPath(),
|
||||||
|
$"acdream-flat-collision-{Guid.NewGuid():N}.pak");
|
||||||
|
_paths.Add(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PakHeader Header() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
PortalIteration = Identity.PortalIteration,
|
||||||
|
CellIteration = Identity.CellIteration,
|
||||||
|
HighResIteration = Identity.HighResIteration,
|
||||||
|
LanguageIteration = Identity.LanguageIteration,
|
||||||
|
BakeToolVersion = Identity.BakeToolVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void FlipFirstBlobByte(string path)
|
||||||
|
{
|
||||||
|
using var stream = new FileStream(
|
||||||
|
path,
|
||||||
|
FileMode.Open,
|
||||||
|
FileAccess.ReadWrite,
|
||||||
|
FileShare.None);
|
||||||
|
stream.Position = PakHeader.Size + 4;
|
||||||
|
int value = stream.ReadByte();
|
||||||
|
Assert.NotEqual(-1, value);
|
||||||
|
stream.Position = PakHeader.Size + 4;
|
||||||
|
stream.WriteByte((byte)(value ^ 0xFF));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Float(uint bits) =>
|
||||||
|
BitConverter.Int32BitsToSingle(unchecked((int)bits));
|
||||||
|
|
||||||
|
private static void AssertFloatBits(float expected, float actual) =>
|
||||||
|
Assert.Equal(
|
||||||
|
BitConverter.SingleToInt32Bits(expected),
|
||||||
|
BitConverter.SingleToInt32Bits(actual));
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (string path in _paths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
using AcDream.Content.Pak;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
|
||||||
|
namespace AcDream.Content.Tests;
|
||||||
|
|
||||||
|
public sealed class InstalledPreparedCollisionCatalogTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void InstalledPackage_ContainsAndReadsCanonicalCollisionKeys()
|
||||||
|
{
|
||||||
|
string? datDir = ResolveDatDir();
|
||||||
|
if (datDir is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string packagePath = Path.Combine(datDir, "acdream.pak");
|
||||||
|
if (!File.Exists(packagePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
using var adapter = new DatCollectionAdapter(dats);
|
||||||
|
using var source =
|
||||||
|
new PakPreparedAssetSource(packagePath, adapter);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
source.ReadGfxObjCollision(0x0100_0A2Bu).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
source.ReadSetupCollision(0x0200_0001u).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
source.ReadCellStructureCollision(0xA9B4_013Fu).Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PreparedAssetReadStatus.Loaded,
|
||||||
|
source.ReadEnvCellTopology(0xA9B4_013Fu).Status);
|
||||||
|
Assert.Equal(4, source.CollisionStats.Loaded);
|
||||||
|
Assert.True(source.MappedVirtualBytes > 1L << 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveDatDir()
|
||||||
|
{
|
||||||
|
string? configured =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||||
|
if (!string.IsNullOrWhiteSpace(configured)
|
||||||
|
&& Directory.Exists(configured))
|
||||||
|
{
|
||||||
|
return configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
string fallback = Path.Combine(
|
||||||
|
Environment.GetFolderPath(
|
||||||
|
Environment.SpecialFolder.UserProfile),
|
||||||
|
"Documents",
|
||||||
|
"Asheron's Call");
|
||||||
|
return Directory.Exists(fallback) ? fallback : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,10 @@ public class PakKeyTests {
|
||||||
[InlineData(PakAssetType.GfxObjMesh, 1)]
|
[InlineData(PakAssetType.GfxObjMesh, 1)]
|
||||||
[InlineData(PakAssetType.SetupMesh, 2)]
|
[InlineData(PakAssetType.SetupMesh, 2)]
|
||||||
[InlineData(PakAssetType.EnvCellMesh, 3)]
|
[InlineData(PakAssetType.EnvCellMesh, 3)]
|
||||||
|
[InlineData(PakAssetType.GfxObjCollision, 4)]
|
||||||
|
[InlineData(PakAssetType.SetupCollision, 5)]
|
||||||
|
[InlineData(PakAssetType.CellStructureCollision, 6)]
|
||||||
|
[InlineData(PakAssetType.EnvCellTopology, 7)]
|
||||||
public void AssetType_NumericValuesAreStable(PakAssetType type, byte expected) {
|
public void AssetType_NumericValuesAreStable(PakAssetType type, byte expected) {
|
||||||
// These values are a wire format — pin them so a future refactor can't
|
// These values are a wire format — pin them so a future refactor can't
|
||||||
// silently renumber the enum and corrupt existing paks.
|
// silently renumber the enum and corrupt existing paks.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,24 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
FlatCollisionAssetBuilder.FlattenCell(source);
|
FlatCollisionAssetBuilder.FlattenCell(source);
|
||||||
FlatCellCollisionAsset second =
|
FlatCellCollisionAsset second =
|
||||||
FlatCollisionAssetBuilder.FlattenCell(source);
|
FlatCollisionAssetBuilder.FlattenCell(source);
|
||||||
|
EnvCell datCell = Assert.IsType<EnvCell>(
|
||||||
|
dats.Get<EnvCell>(cellId));
|
||||||
|
var environment =
|
||||||
|
Assert.IsType<DatReaderWriter.DBObjs.Environment>(
|
||||||
|
dats.Get<DatReaderWriter.DBObjs.Environment>(
|
||||||
|
0x0D00_0000u | datCell.EnvironmentId));
|
||||||
|
Assert.True(
|
||||||
|
environment.Cells.TryGetValue(
|
||||||
|
datCell.CellStructure,
|
||||||
|
out CellStruct? cellStruct));
|
||||||
|
Assert.NotNull(cellStruct);
|
||||||
|
FlatCellStructureCollisionAsset directStructure =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCellStructure(cellStruct!);
|
||||||
|
FlatEnvCellTopology directTopology =
|
||||||
|
FlatCollisionAssetBuilder.FlattenEnvCellTopology(
|
||||||
|
cellId,
|
||||||
|
datCell,
|
||||||
|
directStructure.PortalPolygons);
|
||||||
|
|
||||||
AssertPhysicsSourceMatches(source.BSP?.Root, source.Resolved, first.Structure.PhysicsBsp);
|
AssertPhysicsSourceMatches(source.BSP?.Root, source.Resolved, first.Structure.PhysicsBsp);
|
||||||
AssertContainmentSourceMatches(
|
AssertContainmentSourceMatches(
|
||||||
|
|
@ -60,6 +78,22 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
first.Topology.VisibleCellIds.AsEnumerable(),
|
first.Topology.VisibleCellIds.AsEnumerable(),
|
||||||
second.Topology.VisibleCellIds.AsEnumerable());
|
second.Topology.VisibleCellIds.AsEnumerable());
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual(
|
||||||
|
first.Structure.PhysicsBsp,
|
||||||
|
directStructure.PhysicsBsp);
|
||||||
|
Assert.Equal(
|
||||||
|
first.Structure.ContainmentBsp.Nodes.AsEnumerable(),
|
||||||
|
directStructure.ContainmentBsp.Nodes.AsEnumerable());
|
||||||
|
AssertPolygonTableEqual(
|
||||||
|
first.Structure.PortalPolygons,
|
||||||
|
directStructure.PortalPolygons);
|
||||||
|
Assert.Equal(
|
||||||
|
first.Topology.Portals.AsEnumerable(),
|
||||||
|
directTopology.Portals.AsEnumerable());
|
||||||
|
Assert.Equal(
|
||||||
|
first.Topology.VisibleCellIds.AsEnumerable(),
|
||||||
|
directTopology.VisibleCellIds.AsEnumerable());
|
||||||
|
Assert.Equal(first.Topology.SeenOutside, directTopology.SeenOutside);
|
||||||
|
|
||||||
_output.WriteLine(
|
_output.WriteLine(
|
||||||
$"cell 0x{cellId:X8}: physicsNodes={first.Structure.PhysicsBsp.Nodes.Length}, " +
|
$"cell 0x{cellId:X8}: physicsNodes={first.Structure.PhysicsBsp.Nodes.Length}, " +
|
||||||
|
|
@ -97,6 +131,8 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
FlatCollisionAssetBuilder.FlattenGfxObj(
|
FlatCollisionAssetBuilder.FlattenGfxObj(
|
||||||
source,
|
source,
|
||||||
cache.GetVisualBounds(gfxObjId));
|
cache.GetVisualBounds(gfxObjId));
|
||||||
|
FlatGfxObjCollisionAsset direct =
|
||||||
|
FlatCollisionAssetBuilder.FlattenGfxObj(gfxObj);
|
||||||
|
|
||||||
AssertPhysicsSourceMatches(
|
AssertPhysicsSourceMatches(
|
||||||
source.BSP.Root,
|
source.BSP.Root,
|
||||||
|
|
@ -107,6 +143,15 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
AssertVisualBoundsBits(
|
AssertVisualBoundsBits(
|
||||||
Assert.IsType<GfxObjVisualBounds>(cache.GetVisualBounds(gfxObjId)),
|
Assert.IsType<GfxObjVisualBounds>(cache.GetVisualBounds(gfxObjId)),
|
||||||
flat.VisualBounds!.Value);
|
flat.VisualBounds!.Value);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFlatPhysicsEqual(
|
||||||
|
flat.PhysicsBsp,
|
||||||
|
direct.PhysicsBsp);
|
||||||
|
AssertFlatNullableSphereBits(
|
||||||
|
flat.BoundingSphere,
|
||||||
|
direct.BoundingSphere);
|
||||||
|
AssertFlatNullableVisualBoundsBits(
|
||||||
|
flat.VisualBounds,
|
||||||
|
direct.VisualBounds);
|
||||||
|
|
||||||
_output.WriteLine(
|
_output.WriteLine(
|
||||||
$"gfx 0x{gfxObjId:X8}: nodes={flat.PhysicsBsp.Nodes.Length}, " +
|
$"gfx 0x{gfxObjId:X8}: nodes={flat.PhysicsBsp.Nodes.Length}, " +
|
||||||
|
|
@ -127,7 +172,10 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
|
|
||||||
FlatSetupCollision flat =
|
FlatSetupCollision flat =
|
||||||
FlatCollisionAssetBuilder.FlattenSetup(source);
|
FlatCollisionAssetBuilder.FlattenSetup(source);
|
||||||
|
FlatSetupCollision direct =
|
||||||
|
FlatCollisionAssetBuilder.FlattenSetup(setup);
|
||||||
AssertSetupSourceMatches(source, flat);
|
AssertSetupSourceMatches(source, flat);
|
||||||
|
AssertSetupFlatEqual(flat, direct);
|
||||||
|
|
||||||
_output.WriteLine(
|
_output.WriteLine(
|
||||||
$"setup 0x{setupId:X8}: cylinders={flat.Cylinders.Length}, " +
|
$"setup 0x{setupId:X8}: cylinders={flat.Cylinders.Length}, " +
|
||||||
|
|
@ -327,4 +375,114 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
source.HalfExtents,
|
source.HalfExtents,
|
||||||
flat.HalfExtents);
|
flat.HalfExtents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertFlatNullableSphereBits(
|
||||||
|
FlatCollisionSphere? expected,
|
||||||
|
FlatCollisionSphere? actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.HasValue, actual.HasValue);
|
||||||
|
if (!expected.HasValue || !actual.HasValue)
|
||||||
|
return;
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Value.Origin,
|
||||||
|
actual.Value.Origin);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Value.Radius,
|
||||||
|
actual.Value.Radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertFlatNullableVisualBoundsBits(
|
||||||
|
FlatGfxObjVisualBounds? expected,
|
||||||
|
FlatGfxObjVisualBounds? actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.HasValue, actual.HasValue);
|
||||||
|
if (!expected.HasValue || !actual.HasValue)
|
||||||
|
return;
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Value.Min,
|
||||||
|
actual.Value.Min);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Value.Max,
|
||||||
|
actual.Value.Max);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Value.Center,
|
||||||
|
actual.Value.Center);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Value.Radius,
|
||||||
|
actual.Value.Radius);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Value.HalfExtents,
|
||||||
|
actual.Value.HalfExtents);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertSetupFlatEqual(
|
||||||
|
FlatSetupCollision expected,
|
||||||
|
FlatSetupCollision actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Cylinders.Length, actual.Cylinders.Length);
|
||||||
|
Assert.Equal(expected.Spheres.Length, actual.Spheres.Length);
|
||||||
|
for (int i = 0; i < expected.Cylinders.Length; i++)
|
||||||
|
{
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Cylinders[i].Origin,
|
||||||
|
actual.Cylinders[i].Origin);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Cylinders[i].Radius,
|
||||||
|
actual.Cylinders[i].Radius);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Cylinders[i].Height,
|
||||||
|
actual.Cylinders[i].Height);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < expected.Spheres.Length; i++)
|
||||||
|
{
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Spheres[i].Origin,
|
||||||
|
actual.Spheres[i].Origin);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Spheres[i].Radius,
|
||||||
|
actual.Spheres[i].Radius);
|
||||||
|
}
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Height,
|
||||||
|
actual.Height);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.Radius,
|
||||||
|
actual.Radius);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.StepUpHeight,
|
||||||
|
actual.StepUpHeight);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertFloatBits(
|
||||||
|
expected.StepDownHeight,
|
||||||
|
actual.StepDownHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertPolygonTableEqual(
|
||||||
|
FlatPolygonTable expected,
|
||||||
|
FlatPolygonTable actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Polygons.Length, actual.Polygons.Length);
|
||||||
|
Assert.Equal(expected.Vertices.Length, actual.Vertices.Length);
|
||||||
|
for (int i = 0; i < expected.Polygons.Length; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.Polygons[i].Id, actual.Polygons[i].Id);
|
||||||
|
Assert.Equal(
|
||||||
|
expected.Polygons[i].SidesType,
|
||||||
|
actual.Polygons[i].SidesType);
|
||||||
|
Assert.Equal(
|
||||||
|
expected.Polygons[i].NumPoints,
|
||||||
|
actual.Polygons[i].NumPoints);
|
||||||
|
Assert.Equal(
|
||||||
|
expected.Polygons[i].VertexRange,
|
||||||
|
actual.Polygons[i].VertexRange);
|
||||||
|
FlatCollisionAssetBuilderTests.AssertPlaneBits(
|
||||||
|
expected.Polygons[i].Plane,
|
||||||
|
actual.Polygons[i].Plane);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < expected.Vertices.Length; i++)
|
||||||
|
{
|
||||||
|
FlatCollisionAssetBuilderTests.AssertVectorBits(
|
||||||
|
expected.Vertices[i],
|
||||||
|
actual.Vertices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue