test(phys): door setup + GfxObj dat-inspection — Hypothesis A falsified
Read-only deterministic test that opens the real client dat and
dumps Setup 0x020019FF + every GfxObj id it references. Bypasses
PhysicsDataCache's four early-return filters so we see WHAT is in
the dat, not what got into the cache. Skips gracefully when the
dat directory isn't present (keeps CI green).
Result reframes the prior session's investigation:
GfxObj 0x010044B5 (part 0 of the door) DOES have a full door-slab
PhysicsBSP — 6 two-sided (SidesType=Landblock) polygons forming a
1.925m × 0.261m × 2.490m collision volume at frame[0] offset
(-0.006, 0.125, 1.275). Bounding sphere radius 1.975. HasPhysics
flag set. So the handoff's Hypothesis A ("0x010044B5 has no
collision-bearing polygons, only visual") is FALSE.
GfxObj 0x010044B6 (parts 1 + 2, the swinging leaves) IS visual-only
by retail design — HasPhysics clear, PhysicsBSP null, 0 PhysicsPolygons,
but 87 visual Polygons. Our ShadowShapeBuilder skipping these matches
retail's CPhysicsPart::find_obj_collisions short-circuit on
physics_bsp==0 (acclient_2013_pseudo_c.txt:275051) — not a bug.
So the door collision bug is in INTEGRATION, not data. The Task 7
experiment last session registered 0x010044B5's BSP but got zero
[resolve-bldg] attributions. With the data confirmed good, the
next apparatus is a deterministic harness that hydrates 0x010044B5
from a dat dump, registers it via RegisterMultiPart, and sweeps a
player sphere into the door to confirm whether BSP collision fires
in isolation.
Pickup prompt + full reading in
docs/research/2026-05-24-door-dat-inspection-findings.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c89df8e4c0
commit
e1d94d7094
2 changed files with 479 additions and 0 deletions
258
docs/research/2026-05-24-door-dat-inspection-findings.md
Normal file
258
docs/research/2026-05-24-door-dat-inspection-findings.md
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
# Door collision dat inspection — findings
|
||||||
|
|
||||||
|
**Date:** 2026-05-24 (evening, continuation of door collision investigation)
|
||||||
|
**Branch:** `claude/strange-albattani-3fc83c`
|
||||||
|
**Status:** Evidence gathered. Hypothesis A from
|
||||||
|
[`2026-05-24-door-collision-session-handoff.md`](2026-05-24-door-collision-session-handoff.md) **FALSIFIED**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
A deterministic, read-only dat-inspection test
|
||||||
|
([`DoorSetupGfxObjInspectionTests.cs`](../../tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs))
|
||||||
|
opens the real client dat and prints the raw state of door Setup
|
||||||
|
`0x020019FF` + its three referenced GfxObjs.
|
||||||
|
|
||||||
|
**Result — Hypothesis A is wrong.** Part 0 (`0x010044B5`) has a complete
|
||||||
|
1.925 m × 0.261 m × 2.490 m door-sized collision volume in the dat. Six
|
||||||
|
two-sided (`SidesType=Landblock`) physics polygons form the closed door
|
||||||
|
slab. Bounding sphere radius 1.975 m. Setup `Flags=HasPhysicsBSP`.
|
||||||
|
|
||||||
|
Parts 1, 2 (`0x010044B6`) **are** visual-only by design — `HasPhysics`
|
||||||
|
flag is clear, `PhysicsBSP` is null, `PhysicsPolygons.Count = 0`. **This
|
||||||
|
matches retail's `CPhysicsPart::find_obj_collisions`**
|
||||||
|
([`acclient_2013_pseudo_c.txt:275051`](../research/named-retail/acclient_2013_pseudo_c.txt)),
|
||||||
|
which explicitly short-circuits when `physics_bsp == 0`. So retail also
|
||||||
|
runs no collision against `0x010044B6` — and our skip-on-null-BSP
|
||||||
|
behavior is retail-faithful, not a bug.
|
||||||
|
|
||||||
|
**This rewrites the "next-session approach" recommendation in the prior
|
||||||
|
handoff.** The handoff said "if 0x010044B5's BSP has zero floor-touching
|
||||||
|
polys → Hypothesis A confirmed → pivot strategy." The BSP has six
|
||||||
|
collision polygons forming the whole door slab. The pivot is not needed;
|
||||||
|
we need to figure out why our integration of `0x010044B5`'s BSP didn't
|
||||||
|
fire during the Task 7 experiment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Raw dump (verbatim from the test)
|
||||||
|
|
||||||
|
```
|
||||||
|
=== Setup 0x020019FF ===
|
||||||
|
Flags = HasParent, AllowFreeHeading, HasPhysicsBSP (0x0000000D)
|
||||||
|
Radius = 0.1414
|
||||||
|
Height = 0.2000
|
||||||
|
StepUp = 0.0900
|
||||||
|
StepDown = 0.0900
|
||||||
|
CylSpheres = 0
|
||||||
|
Spheres = 1
|
||||||
|
[0] r=0.1000 origin=(0.000,0.000,0.018)
|
||||||
|
Parts = 3
|
||||||
|
[0] gfxObj=0x010044B5
|
||||||
|
[1] gfxObj=0x010044B6
|
||||||
|
[2] gfxObj=0x010044B6
|
||||||
|
PlacementFrames = 1
|
||||||
|
[Default] frameCount=3
|
||||||
|
frame[0] pos=(-0.006,0.125,1.275) rot=(0.000,0.000,0.000,1.000)
|
||||||
|
frame[1] pos=(0.710,0.000,1.210) rot=(0.000,0.000,0.000,1.000)
|
||||||
|
frame[2] pos=(0.710,0.247,1.210) rot=(0.000,0.000,1.000,0.000)
|
||||||
|
|
||||||
|
=== GfxObj 0x010044B5 === (the door slab — has physics)
|
||||||
|
Flags = HasPhysics, HasDrawing, HasDIDDegrade (0x0000000B)
|
||||||
|
HasPhysics = True
|
||||||
|
VertexArray = non-null, 8 vertices
|
||||||
|
PhysicsPolygons = 6 polys
|
||||||
|
PhysicsBSP = non-null
|
||||||
|
PhysicsBSP.Root = non-null
|
||||||
|
Root.Type = BPnN
|
||||||
|
Root.BoundingSphere = (-0.390,-0.056,-0.150) r=1.975
|
||||||
|
BSP tree total polys (including children) = 6
|
||||||
|
PhysicsPolygon AABB sweep (first 6 polys):
|
||||||
|
[0x0000] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(0.971,0.127,-1.236) # bottom face
|
||||||
|
[0x0001] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(-0.954,0.127,1.255) # left face
|
||||||
|
[0x0002] nVerts=4 sides=Landblock min=(-0.954,-0.134, 1.255) max=(0.971,0.127,1.255) # top face
|
||||||
|
[0x0003] nVerts=4 sides=Landblock min=( 0.971,-0.134,-1.236) max=(0.971,0.127,1.255) # right face
|
||||||
|
[0x0004] nVerts=4 sides=Landblock min=(-0.954,-0.134,-1.236) max=(0.971,-0.134,1.255) # front face
|
||||||
|
[0x0005] nVerts=4 sides=Landblock min=(-0.954, 0.127,-1.236) max=(0.971,0.127,1.255) # back face
|
||||||
|
PhysicsPolygons combined AABB: min=(-0.954,-0.134,-1.236) max=(0.971,0.127,1.255)
|
||||||
|
size=(1.925, 0.261, 2.490)
|
||||||
|
|
||||||
|
=== GfxObj 0x010044B6 === (the leaves — visual-only by design)
|
||||||
|
Flags = HasDrawing, HasDIDDegrade (0x0000000A)
|
||||||
|
HasPhysics = False
|
||||||
|
VertexArray = non-null, 40 vertices
|
||||||
|
PhysicsPolygons = 0 polys
|
||||||
|
PhysicsBSP = NULL
|
||||||
|
Polygons (visual) = 87 polys
|
||||||
|
DrawingBSP = non-null
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What this means
|
||||||
|
|
||||||
|
### The data is right
|
||||||
|
|
||||||
|
Part 0's BSP is a six-faced thin slab oriented as a vertical door:
|
||||||
|
1.925 m wide × 0.261 m thin × 2.490 m tall. Placed at frame[0] offset
|
||||||
|
`(-0.006, 0.125, 1.275)`, it occupies entity-local Z ∈ `[0.039, 2.530]` —
|
||||||
|
a standard door height. All six faces are
|
||||||
|
`SidesType=Landblock` (two-sided collision — catches a sphere
|
||||||
|
approaching from either side).
|
||||||
|
|
||||||
|
This is exactly what retail's collision system uses to block doors.
|
||||||
|
No mystery, no missing data, no need to fall back to a wider Cylinder
|
||||||
|
approximation.
|
||||||
|
|
||||||
|
### The leaves are correctly visual-only
|
||||||
|
|
||||||
|
`0x010044B6` is the swinging door leaf (used twice — left + right
|
||||||
|
panels). It has no physics by retail design. Our `ShadowShapeBuilder`
|
||||||
|
skipping these parts matches both the dat and retail's
|
||||||
|
`CPhysicsPart::find_obj_collisions`.
|
||||||
|
|
||||||
|
### So the bug is in integration, not data
|
||||||
|
|
||||||
|
The previous session's Task 7 experiment registered `0x010044B5`'s BSP
|
||||||
|
correctly (we saw `type=BSP gfxObj=0x010044B5 radius=2.000
|
||||||
|
localPos=(-0.006,0.125,1.275)` in the per-shape registration), yet got
|
||||||
|
**zero `[resolve-bldg]` attributions** during live play. With the data
|
||||||
|
now confirmed good, that gap must be in:
|
||||||
|
|
||||||
|
1. **The BSP collision dispatch never enters for the door entry** —
|
||||||
|
`TransitionTypes.cs:2257` silently `continue`s when
|
||||||
|
`engine.DataCache.GetGfxObj(obj.GfxObjId)?.BSP?.Root is null`. If the
|
||||||
|
GfxObj wasn't cached at collision time (race with renderer load), the
|
||||||
|
entry is invisibly skipped. **No log distinguishes this from
|
||||||
|
"queried-and-no-hit."**
|
||||||
|
|
||||||
|
2. **Broadphase placeholder radius** — Task 2's `ShadowShapeBuilder`
|
||||||
|
uses `bspRadius = 2f` as a stand-in pending a Task 5/6 caller
|
||||||
|
replacement. The real dat value is `1.975` — close enough not to be
|
||||||
|
the blocker, but the placeholder convention means callers MUST
|
||||||
|
substitute the real BS radius from `cache.GetGfxObj(gfxId).BoundingSphere.Radius`
|
||||||
|
before registering. If a future caller forgets, the broadphase will
|
||||||
|
still mostly work but won't be tight.
|
||||||
|
|
||||||
|
3. **The broadphase center is the part's FRAME origin, not the BSP's
|
||||||
|
bounding-sphere center.** Frame origin = `(-0.006, 0.125, 1.275)`;
|
||||||
|
BS center in part-local = `(-0.390, -0.056, -0.150)`. Distance:
|
||||||
|
1.48 m. The 2.0 m broadphase radius nominally covers the BS sphere
|
||||||
|
(radius 1.975) from the frame origin only on the side closest to the
|
||||||
|
BS center. For approaches on the opposite side, the broadphase
|
||||||
|
sphere extends 2.0 m + 1.48 m = 3.48 m from the BS center — wider
|
||||||
|
than needed, but never too tight in the door case. Still, a more
|
||||||
|
faithful encoding centers the broadphase on the part's BS center +
|
||||||
|
frame offset, with radius = BS radius.
|
||||||
|
|
||||||
|
4. **BSPQuery against `SidesType=Landblock` polys** — `BSPQuery.cs`
|
||||||
|
pass-through-copies `SidesType` (line 2277) but doesn't filter on
|
||||||
|
it. We have not yet verified that `Landblock`-typed polys actually
|
||||||
|
produce collision hits in our query pipeline against a thin-slab
|
||||||
|
geometry. (Note: indoor cells use `SidesType=Single`-typed cell-floor
|
||||||
|
polys and those work — the cellar replay tests pin that. But Doors'
|
||||||
|
`Landblock` polys may behave differently — particularly w.r.t.
|
||||||
|
two-sided collision.)
|
||||||
|
|
||||||
|
5. **Entity rotation at the doorway** — Holtburg cottage doors face
|
||||||
|
non-cardinal directions. The entity's world rotation
|
||||||
|
`entity_rot` composes with `frame[0].Rotation` (identity for part 0)
|
||||||
|
to produce `obj.Rotation = entity_rot`. The sphere
|
||||||
|
transform `inv(entity_rot) * (sphere_world − obj.Position)` is
|
||||||
|
sensitive to that rotation. If we register with identity (forgetting
|
||||||
|
to plumb the spawn's rotation through), the BSP polys will be
|
||||||
|
oriented "into the world" wrong — passing tests that approach from
|
||||||
|
the wrong axis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended next step
|
||||||
|
|
||||||
|
The handoff's "DO NOT speculate-and-fix again" rule still applies. The
|
||||||
|
right next move is **apparatus-first**, not another implementation
|
||||||
|
attempt:
|
||||||
|
|
||||||
|
**Write a focused unit test** that:
|
||||||
|
|
||||||
|
1. Loads the real `0x010044B5` PhysicsBSP from the dat via the
|
||||||
|
inspection test's pattern (or use `GfxObjDumpSerializer.Hydrate`
|
||||||
|
for a deterministic fixture).
|
||||||
|
2. Constructs a synthetic door entity at a known world position
|
||||||
|
`(132.6, 17.1, 94.08)` with a known rotation (try identity AND a
|
||||||
|
~90° Z rotation to cover both axes).
|
||||||
|
3. Sweeps a player sphere at the door from each of the four
|
||||||
|
compass directions, at off-center positions (50 cm off-center)
|
||||||
|
AND dead center.
|
||||||
|
4. Calls `Transition.FindObjCollisions` / `ResolveWithTransition`
|
||||||
|
directly (apparatus path mirrors the live one).
|
||||||
|
5. Asserts:
|
||||||
|
- Dead-center approach → `Collided` / `Adjusted` / `Slid`
|
||||||
|
with `CollideObjectGuids` containing the door entity.
|
||||||
|
- 50 cm off-center approach → same.
|
||||||
|
- From inside walking out → same.
|
||||||
|
|
||||||
|
If the test fails: we have a deterministic reproduction of the live
|
||||||
|
bug in <500 ms, and we can fix the integration with confidence.
|
||||||
|
If the test passes: the door bug is elsewhere (cell registration,
|
||||||
|
spawn-time race, etc.).
|
||||||
|
|
||||||
|
This is the next apparatus the previous session was building toward
|
||||||
|
when it ran out of cycles. With the data question now closed by the
|
||||||
|
dat inspection, it's the highest-information next move.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What's in the tree right now
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git status --short
|
||||||
|
?? docs/research/2026-05-24-door-dat-inspection-findings.md
|
||||||
|
?? tests/AcDream.Core.Tests/Physics/DoorSetupGfxObjInspectionTests.cs
|
||||||
|
[+ untracked launch logs from prior sessions]
|
||||||
|
```
|
||||||
|
|
||||||
|
Build green; existing tests still pass; new test runs in 34 ms and
|
||||||
|
produces the dump above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pickup prompt for next session
|
||||||
|
|
||||||
|
```
|
||||||
|
Door collision dat inspection (2026-05-24 evening) FALSIFIED
|
||||||
|
Hypothesis A. Part 0 (0x010044B5) has a full door-slab BSP in the
|
||||||
|
dat — 6 Landblock-typed polys forming a 1.925 m × 0.261 m × 2.490 m
|
||||||
|
collision volume. Parts 1, 2 (0x010044B6) are visual-only by retail
|
||||||
|
design (HasPhysics flag clear). Retail and acdream both skip those
|
||||||
|
in CPhysicsPart::find_obj_collisions — that's not a bug.
|
||||||
|
|
||||||
|
Read docs/research/2026-05-24-door-dat-inspection-findings.md
|
||||||
|
|
||||||
|
State both altitudes:
|
||||||
|
Currently working toward: M1.5 — Indoor world feels right
|
||||||
|
Current phase: A6.P4 — door collision investigation continues.
|
||||||
|
Per-part BSP infrastructure (Tasks 1-6) ships
|
||||||
|
already; data is confirmed good in the dat; need
|
||||||
|
to determine WHY our integration of 0x010044B5's
|
||||||
|
BSP didn't fire collisions during the Task 7
|
||||||
|
experiment.
|
||||||
|
|
||||||
|
Next moves (in order):
|
||||||
|
1. Write CellarUpTrajectoryReplay-style apparatus test that
|
||||||
|
loads 0x010044B5's PhysicsBSP from a dat dump, registers a
|
||||||
|
synthetic door via RegisterMultiPart, and sweeps a player
|
||||||
|
sphere at it. Confirm BSP collision fires (or doesn't) in
|
||||||
|
isolation.
|
||||||
|
2. If the test passes → bug is in live registration (likely
|
||||||
|
cell scoping, entity rotation, or race with renderer
|
||||||
|
loading). Investigate live cell membership for door
|
||||||
|
entities.
|
||||||
|
3. If the test fails → bug is in BSPQuery.FindCollisions
|
||||||
|
against thin-slab Landblock-typed polys. Investigate the
|
||||||
|
6-path dispatcher for that case.
|
||||||
|
|
||||||
|
DO NOT re-attempt Task 7 (per-part BSP wiring in
|
||||||
|
RegisterLiveEntityCollision) until the apparatus test confirms
|
||||||
|
the BSP works in isolation. Tasks 1-6 stay; they're correct.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
using Env = System.Environment;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A6.P4 door fix (2026-05-24) — read-only dat inspection. Opens
|
||||||
|
/// the real client dat collection and reports the raw state of door
|
||||||
|
/// Setup 0x020019FF + its three referenced GfxObjs (0x010044B5 and
|
||||||
|
/// 0x010044B6 used twice). Answers the question "does the leaf GfxObj
|
||||||
|
/// 0x010044B6 actually have a non-null PhysicsBSP in the dat, or is it
|
||||||
|
/// genuinely visual-only?" without going through PhysicsDataCache's
|
||||||
|
/// four early-return filters, and without launching the live client.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// SKIP if ACDREAM_DAT_DIR (or the default
|
||||||
|
/// %USERPROFILE%\Documents\Asheron's Call directory) isn't present —
|
||||||
|
/// keeps CI green. Local developer runs always have it.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class DoorSetupGfxObjInspectionTests
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _out;
|
||||||
|
public DoorSetupGfxObjInspectionTests(ITestOutputHelper output) => _out = output;
|
||||||
|
|
||||||
|
private const uint DoorSetupId = 0x020019FFu;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read door setup 0x020019FF and every GfxObj id it references.
|
||||||
|
/// Emits a structured report so we can decide whether the "BSP=null
|
||||||
|
/// on 0x010044B6" handoff finding is a data fact or a load-order /
|
||||||
|
/// loader-bug artifact.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void DoorSetup_AndParts_DatInspection()
|
||||||
|
{
|
||||||
|
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||||
|
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
||||||
|
"Documents", "Asheron's Call");
|
||||||
|
if (!Directory.Exists(datDir))
|
||||||
|
{
|
||||||
|
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
var setup = dats.Get<Setup>(DoorSetupId);
|
||||||
|
Assert.NotNull(setup);
|
||||||
|
|
||||||
|
_out.WriteLine($"=== Setup 0x{DoorSetupId:X8} ===");
|
||||||
|
_out.WriteLine($" Flags = {setup!.Flags} (0x{(uint)setup.Flags:X8})");
|
||||||
|
_out.WriteLine($" Radius = {setup.Radius:F4}");
|
||||||
|
_out.WriteLine($" Height = {setup.Height:F4}");
|
||||||
|
_out.WriteLine($" StepUp = {setup.StepUpHeight:F4}");
|
||||||
|
_out.WriteLine($" StepDown = {setup.StepDownHeight:F4}");
|
||||||
|
_out.WriteLine($" CylSpheres = {setup.CylSpheres.Count}");
|
||||||
|
for (int i = 0; i < setup.CylSpheres.Count; i++)
|
||||||
|
{
|
||||||
|
var c = setup.CylSpheres[i];
|
||||||
|
_out.WriteLine($" [{i}] r={c.Radius:F4} h={c.Height:F4} origin=({c.Origin.X:F3},{c.Origin.Y:F3},{c.Origin.Z:F3})");
|
||||||
|
}
|
||||||
|
_out.WriteLine($" Spheres = {setup.Spheres.Count}");
|
||||||
|
for (int i = 0; i < setup.Spheres.Count; i++)
|
||||||
|
{
|
||||||
|
var s = setup.Spheres[i];
|
||||||
|
_out.WriteLine($" [{i}] r={s.Radius:F4} origin=({s.Origin.X:F3},{s.Origin.Y:F3},{s.Origin.Z:F3})");
|
||||||
|
}
|
||||||
|
_out.WriteLine($" Parts = {setup.Parts.Count}");
|
||||||
|
for (int i = 0; i < setup.Parts.Count; i++)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" [{i}] gfxObj=0x{setup.Parts[i]:X8}");
|
||||||
|
}
|
||||||
|
_out.WriteLine($" PlacementFrames = {setup.PlacementFrames.Count}");
|
||||||
|
foreach (var kv in setup.PlacementFrames)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" [{kv.Key}] frameCount={kv.Value.Frames.Count}");
|
||||||
|
for (int i = 0; i < kv.Value.Frames.Count; i++)
|
||||||
|
{
|
||||||
|
var f = kv.Value.Frames[i];
|
||||||
|
_out.WriteLine($" frame[{i}] pos=({f.Origin.X:F3},{f.Origin.Y:F3},{f.Origin.Z:F3}) " +
|
||||||
|
$"rot=({f.Orientation.X:F3},{f.Orientation.Y:F3},{f.Orientation.Z:F3},{f.Orientation.W:F3})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_out.WriteLine($" HoldingLocations = {setup.HoldingLocations.Count}");
|
||||||
|
|
||||||
|
// Inspect each unique part GfxObj.
|
||||||
|
var uniqueGfxIds = setup.Parts.Distinct().ToList();
|
||||||
|
foreach (uint gfxId in uniqueGfxIds)
|
||||||
|
{
|
||||||
|
_out.WriteLine("");
|
||||||
|
InspectGfxObj(dats, gfxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_out.WriteLine("");
|
||||||
|
_out.WriteLine("=== Cache predicate would skip a part if: ===");
|
||||||
|
_out.WriteLine(" !Flags.HasFlag(HasPhysics) OR PhysicsBSP?.Root is null OR VertexArray is null");
|
||||||
|
_out.WriteLine("(See src/AcDream.Core/Physics/PhysicsDataCache.cs:44-47)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InspectGfxObj(DatCollection dats, uint gfxId)
|
||||||
|
{
|
||||||
|
_out.WriteLine($"=== GfxObj 0x{gfxId:X8} ===");
|
||||||
|
var gfx = dats.Get<GfxObj>(gfxId);
|
||||||
|
if (gfx is null)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" Get<GfxObj>(0x{gfxId:X8}) returned NULL — dat reader couldn't load");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_out.WriteLine($" Flags = {gfx.Flags} (0x{(uint)gfx.Flags:X8})");
|
||||||
|
_out.WriteLine($" HasPhysics = {gfx.Flags.HasFlag(GfxObjFlags.HasPhysics)}");
|
||||||
|
_out.WriteLine($" HasDIDDegrade = {gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade)}");
|
||||||
|
|
||||||
|
_out.WriteLine($" VertexArray = {(gfx.VertexArray is null ? "NULL" : $"non-null, {gfx.VertexArray.Vertices?.Count ?? 0} vertices")}");
|
||||||
|
_out.WriteLine($" PhysicsPolygons = {(gfx.PhysicsPolygons is null ? "NULL" : $"{gfx.PhysicsPolygons.Count} polys")}");
|
||||||
|
_out.WriteLine($" PhysicsBSP = {(gfx.PhysicsBSP is null ? "NULL" : "non-null")}");
|
||||||
|
if (gfx.PhysicsBSP is not null)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" PhysicsBSP.Root = {(gfx.PhysicsBSP.Root is null ? "NULL" : "non-null")}");
|
||||||
|
if (gfx.PhysicsBSP.Root is { } root)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" Root.Type = {root.Type}");
|
||||||
|
_out.WriteLine($" Root.Polygons = {root.Polygons?.Count ?? 0}");
|
||||||
|
_out.WriteLine($" Root.PosNode = {(root.PosNode is null ? "null" : "non-null")}");
|
||||||
|
_out.WriteLine($" Root.NegNode = {(root.NegNode is null ? "null" : "non-null")}");
|
||||||
|
var bs = root.BoundingSphere;
|
||||||
|
if (bs is null)
|
||||||
|
_out.WriteLine(" Root.BoundingSphere = null");
|
||||||
|
else
|
||||||
|
_out.WriteLine($" Root.BoundingSphere = ({bs.Origin.X:F3},{bs.Origin.Y:F3},{bs.Origin.Z:F3}) r={bs.Radius:F3}");
|
||||||
|
|
||||||
|
// Walk the tree to count total polygons.
|
||||||
|
int totalPolys = CountTotalPolys(root);
|
||||||
|
_out.WriteLine($" BSP tree total polys (including children) = {totalPolys}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_out.WriteLine($" Polygons (visual) = {(gfx.Polygons is null ? "NULL" : $"{gfx.Polygons.Count} polys")}");
|
||||||
|
if (gfx.DrawingBSP is null)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" DrawingBSP = NULL");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_out.WriteLine($" DrawingBSP = non-null");
|
||||||
|
_out.WriteLine($" DrawingBSP.Root = {(gfx.DrawingBSP.Root is null ? "NULL" : "non-null")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If physics data is present, dump first few polygon AABBs in local frame
|
||||||
|
// — these tell us WHERE collision-bearing geometry sits relative to the
|
||||||
|
// door entity origin (helps distinguish "frame top above doorway" vs
|
||||||
|
// "leaf spanning the threshold").
|
||||||
|
if (gfx.PhysicsPolygons is { Count: > 0 } polys
|
||||||
|
&& gfx.VertexArray?.Vertices is { } verts && verts.Count > 0)
|
||||||
|
{
|
||||||
|
_out.WriteLine($" PhysicsPolygon AABB sweep (first 6 polys):");
|
||||||
|
int dumped = 0;
|
||||||
|
foreach (var (pid, p) in polys)
|
||||||
|
{
|
||||||
|
if (dumped++ >= 6) break;
|
||||||
|
float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
|
||||||
|
float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
|
||||||
|
int nVerts = p.VertexIds.Count;
|
||||||
|
for (int i = 0; i < nVerts; i++)
|
||||||
|
{
|
||||||
|
if (!verts.TryGetValue((ushort)p.VertexIds[i], out var sv)) { nVerts = -1; break; }
|
||||||
|
var v = sv.Origin;
|
||||||
|
if (v.X < minX) minX = v.X;
|
||||||
|
if (v.Y < minY) minY = v.Y;
|
||||||
|
if (v.Z < minZ) minZ = v.Z;
|
||||||
|
if (v.X > maxX) maxX = v.X;
|
||||||
|
if (v.Y > maxY) maxY = v.Y;
|
||||||
|
if (v.Z > maxZ) maxZ = v.Z;
|
||||||
|
}
|
||||||
|
if (nVerts < 0) { _out.WriteLine($" [0x{pid:X4}] vertex lookup failed"); continue; }
|
||||||
|
_out.WriteLine($" [0x{pid:X4}] nVerts={nVerts} sides={p.SidesType} " +
|
||||||
|
$"min=({minX:F3},{minY:F3},{minZ:F3}) max=({maxX:F3},{maxY:F3},{maxZ:F3})");
|
||||||
|
}
|
||||||
|
if (polys.Count > 6)
|
||||||
|
_out.WriteLine($" ... ({polys.Count - 6} more)");
|
||||||
|
|
||||||
|
// Total physics polygon AABB
|
||||||
|
float gMinX = float.MaxValue, gMinY = float.MaxValue, gMinZ = float.MaxValue;
|
||||||
|
float gMaxX = float.MinValue, gMaxY = float.MinValue, gMaxZ = float.MinValue;
|
||||||
|
foreach (var (_, p) in polys)
|
||||||
|
{
|
||||||
|
int nVerts = p.VertexIds.Count;
|
||||||
|
for (int i = 0; i < nVerts; i++)
|
||||||
|
{
|
||||||
|
if (!verts.TryGetValue((ushort)p.VertexIds[i], out var sv)) continue;
|
||||||
|
var v = sv.Origin;
|
||||||
|
if (v.X < gMinX) gMinX = v.X;
|
||||||
|
if (v.Y < gMinY) gMinY = v.Y;
|
||||||
|
if (v.Z < gMinZ) gMinZ = v.Z;
|
||||||
|
if (v.X > gMaxX) gMaxX = v.X;
|
||||||
|
if (v.Y > gMaxY) gMaxY = v.Y;
|
||||||
|
if (v.Z > gMaxZ) gMaxZ = v.Z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_out.WriteLine($" PhysicsPolygons combined AABB: min=({gMinX:F3},{gMinY:F3},{gMinZ:F3}) max=({gMaxX:F3},{gMaxY:F3},{gMaxZ:F3})");
|
||||||
|
_out.WriteLine($" size=({gMaxX - gMinX:F3},{gMaxY - gMinY:F3},{gMaxZ - gMinZ:F3})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountTotalPolys(DatReaderWriter.Types.PhysicsBSPNode node)
|
||||||
|
{
|
||||||
|
int n = node.Polygons?.Count ?? 0;
|
||||||
|
if (node.PosNode is not null) n += CountTotalPolys(node.PosNode);
|
||||||
|
if (node.NegNode is not null) n += CountTotalPolys(node.NegNode);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue