From 1f11ba9b38118a5743c021f0df7c21b3a25991ef Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 19 May 2026 16:04:45 +0200 Subject: [PATCH] =?UTF-8?q?feat(diag):=20Cluster=20A=20=E2=80=94=20extend?= =?UTF-8?q?=20[cell-cache]=20with=20AABB=20+=20bsphere=20+=20recursive=20p?= =?UTF-8?q?oly=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original Phase E [cell-cache] probe (fda6af7) only showed the BSP root node's direct poly count, which was always 0 for non-trivial trees (internal node root). Extending the probe to: - Recursively walk the BSP tree and count total leaf polys - Detect unmatched poly IDs (BSP leaves referencing IDs not in our resolved dict) - Dump the BSP root bounding sphere (center + radius) - Dump the cell's local AABB (min/max from poly vertices) - Dump the cell's world origin (cellTransform * (0,0,0)) The extended data made the route-δ diagnosis definitive: Holtburg cells DO have full physics polygons in their BSPs (e.g. 0xA9B40143 has 14 polys all resolved, full Z range 0-2.8 m). The bug is upstream — AABB-based cell containment is too tight to capture a standing player at most thresholds between rooms, so the indoor cell-BSP branch fires only intermittently. Retail uses portal traversal (CObjMaint::HandleObjectEnterCell + cell-side portal data) which propagates CellId at door crossings. Our AABB-containment shortcut is partial. This diagnostic stays in place as infrastructure for the follow-up "Indoor portal-based cell tracking" phase. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core/Physics/PhysicsDataCache.cs | 42 +++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/AcDream.Core/Physics/PhysicsDataCache.cs b/src/AcDream.Core/Physics/PhysicsDataCache.cs index 6fd45a4..291aabf 100644 --- a/src/AcDream.Core/Physics/PhysicsDataCache.cs +++ b/src/AcDream.Core/Physics/PhysicsDataCache.cs @@ -174,8 +174,48 @@ public sealed class PhysicsDataCache var root = cellStruct.PhysicsBSP?.Root; int bspRootPolyCount = root?.Polygons?.Count ?? 0; bool bspRootHasChildren = root?.PosNode is not null || root?.NegNode is not null; + + // Recursive walk: count total leaf poly references + how many of + // those poly IDs are absent from the resolved dict. If + // bspTotalLeafPolys == 0 the BSP has no collidable polys at all. + // If bspUnmatchedIds > 0 the BSP references IDs we didn't resolve + // (data-deserialization quirk hypothesis). + int bspTotalLeafPolys = 0; + int bspUnmatchedIds = 0; + if (root is not null) + { + var stack = new System.Collections.Generic.Stack(); + stack.Push(root); + while (stack.Count > 0) + { + var n = stack.Pop(); + if (n.Polygons is not null) + { + foreach (var pid in n.Polygons) + { + bspTotalLeafPolys++; + if (!resolved.ContainsKey(pid)) bspUnmatchedIds++; + } + } + if (n.PosNode is not null) stack.Push(n.PosNode); + if (n.NegNode is not null) stack.Push(n.NegNode); + } + } + + var bs = root?.BoundingSphere; + string bsStr = bs is null + ? "bsphere=n/a" + : System.FormattableString.Invariant( + $"bsphere=({bs.Origin.X:F2},{bs.Origin.Y:F2},{bs.Origin.Z:F2}) r={bs.Radius:F2}"); + + // World origin = cellTransform * (0,0,0,1). Tells us where this cell + // sits in world coordinates, so we can cross-check whether the + // player's worldPos actually lies inside the AABB when transformed + // back to local. + var worldOrigin = Vector3.Transform(Vector3.Zero, worldTransform); + Console.WriteLine(System.FormattableString.Invariant( - $"[cell-cache] envCellId=0x{envCellId:X8} physicsPolyCount={cellStruct.PhysicsPolygons?.Count ?? 0} resolvedCount={resolved.Count} bspRootPolyCount={bspRootPolyCount} bspRootHasChildren={bspRootHasChildren}")); + $"[cell-cache] envCellId=0x{envCellId:X8} physicsPolyCount={cellStruct.PhysicsPolygons?.Count ?? 0} resolvedCount={resolved.Count} bspTotalLeafPolys={bspTotalLeafPolys} bspUnmatchedIds={bspUnmatchedIds} {bsStr} aabbMin=({aabbMin.X:F2},{aabbMin.Y:F2},{aabbMin.Z:F2}) aabbMax=({aabbMax.X:F2},{aabbMax.Y:F2},{aabbMax.Z:F2}) worldOrigin=({worldOrigin.X:F2},{worldOrigin.Y:F2},{worldOrigin.Z:F2})")); } }