fix(phys): A6.P4 door bug — AddAllOutsideCells coord convention + replay apparatus
CellTransit.AddAllOutsideCells assumed sphere coords were absolute world coords (subtracting lbXf = 0xA9 * 192 = 32448 from the sphere position). Production has used landblock-local coords since Phase A.1 (streaming-center landblock at world origin), so the subtraction produced localX = -32316, gridX = -1346 → out-of-range → early return → ZERO outdoor cells added. For outdoor primary cells the bug was masked by GetNearbyObjects's radial sweep. For indoor primary cells (where #98 gates the outdoor sweep), the door's outdoor cell 0xA9B40029 never reached portalReachableCells, the door's BSP was never queried, and the player walked through Holtburg cottage doors unimpeded. Fix: AddAllOutsideCells treats worldSphereCenter as landblock-local directly. Matches retail CLandCell::add_all_outside_cells which uses the per-cell 6-byte landblock-relative position struct. Existing CellTransitAddAllOutsideCellsTests + CellTransitFindCellSetTests updated to use landblock-local sphere coords (they were the only callers using the world-coord convention; production never did). Apparatus shipped: - DoorBugTrajectoryReplayTests — live-capture-driven replay harness that pinpointed the bug per-field at unit-test speed (<500ms iteration) - AddAllOutsideCells_LandblockLocalSphere_AddsDoorOutdoorCell — direct unit test that demonstrates the fix - FindTransitCellsSphere_IndoorExitPortal_AddsOutsideForCapturedSpherePos — verifies cell-portal traversal at the captured sphere position - DoorSetupGfxObjInspectionTests.HoltburgCottage_CellPortals_DatInspection — dat-direct EnvCell + Environment.Cells + portal-poly inspector - Fixture: tests/AcDream.Core.Tests/Fixtures/door-bug/live-capture.jsonl (tick 13558 walkthrough + tick 22760 outdoor block) Visual verification (user-driven at Holtburg cottage door, ~50cm off-center): - outside→inside RUN: now BLOCKS (was: walks through) - outside→inside WALK: presumed blocks (not retested) - inside→outside RUN: PARTIAL — body intersects door, sphere slides through - inside→outside WALK: same partial behavior The remaining inside→outside asymmetry is a SEPARATE bug in BSP collision response for two-sided polygons. The [bsp-test] probe now fires 245 times for the door entity from indoor (was 0 pre-fix) — door IS being queried; the BSP polygon-level collision response is the new bug. Handoff at docs/research/2026-05-25-door-bug-partial-fix-shipped.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a2c432e5a
commit
28cd97be62
8 changed files with 1134 additions and 40 deletions
|
|
@ -218,4 +218,128 @@ public class DoorSetupGfxObjInspectionTests
|
|||
if (node.NegNode is not null) n += CountTotalPolys(node.NegNode);
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── A6.P4 door bug (2026-05-24, apparatus-replay extension) ──
|
||||
// Dat-direct EnvCell portal inspection. The trajectory-replay
|
||||
// harness (DoorBugTrajectoryReplayTests) proved the door BSP is
|
||||
// never queried at indoor primary cell 0xA9B40150 — no [bsp-test]
|
||||
// line fires because GetNearbyObjects returns zero shadows. The
|
||||
// CellTransit.FindCellSet portal traversal isn't surfacing
|
||||
// outdoor cell 0xA9B40029 from indoor cell 0xA9B40150. This test
|
||||
// reads the relevant cells' raw portal lists from the dat so we
|
||||
// can determine whether:
|
||||
// (a) cell 0xA9B40150 has NO portal with OtherCellId=0xFFFF
|
||||
// (no exit-portal → exitOutside is never set → bug is in
|
||||
// the building-shell-transit path, not the cell-graph)
|
||||
// (b) cell 0xA9B40150 HAS such a portal but FindTransitCellsSphere's
|
||||
// sphere-vs-plane test rejects it at the player's position
|
||||
// (c) the portal exists and fires correctly, but AddAllOutsideCells
|
||||
// computes a different outdoor cell than 0xA9B40029
|
||||
// Findings drive the fix direction.
|
||||
[Fact]
|
||||
public void HoltburgCottage_CellPortals_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);
|
||||
|
||||
// Cells from the captured walkthrough:
|
||||
// 0xA9B40150 — indoor doorway cell (failing tick primary cell)
|
||||
// 0xA9B4013F — indoor cottage interior cell (player started here)
|
||||
// 0xA9B40029 — outdoor cell where door is registered
|
||||
var cellIds = new uint[] { 0xA9B40150u, 0xA9B4013Fu, 0xA9B40029u };
|
||||
foreach (uint cellId in cellIds)
|
||||
{
|
||||
_out.WriteLine("");
|
||||
InspectCell(dats, cellId);
|
||||
}
|
||||
}
|
||||
|
||||
private void InspectCell(DatCollection dats, uint cellId)
|
||||
{
|
||||
string typeLabel = (cellId & 0xFFFFu) >= 0x0100u ? "indoor" : "outdoor";
|
||||
_out.WriteLine($"=== Cell 0x{cellId:X8} (low=0x{cellId & 0xFFFFu:X4}, type={typeLabel}) ===");
|
||||
|
||||
bool ok = dats.TryGet<EnvCell>(cellId, out var envCell);
|
||||
if (!ok || envCell is null)
|
||||
{
|
||||
_out.WriteLine($" TryGet<EnvCell>(0x{cellId:X8}) returned false (not in dat or wrong type — outdoor cells use LandBlockInfo, not EnvCell)");
|
||||
return;
|
||||
}
|
||||
|
||||
_out.WriteLine($" Flags = 0x{(uint)envCell.Flags:X8}");
|
||||
_out.WriteLine($" Position.Origin= ({envCell.Position.Origin.X:F3},{envCell.Position.Origin.Y:F3},{envCell.Position.Origin.Z:F3})");
|
||||
var q = envCell.Position.Orientation;
|
||||
_out.WriteLine($" Position.Rot = ({q.X:F4},{q.Y:F4},{q.Z:F4},{q.W:F4})");
|
||||
_out.WriteLine($" EnvironmentId = 0x{envCell.EnvironmentId:X8}");
|
||||
_out.WriteLine($" CellStructure = 0x{envCell.CellStructure:X8}");
|
||||
_out.WriteLine($" StaticObjects = {envCell.StaticObjects.Count}");
|
||||
_out.WriteLine($" VisibleCells = {envCell.VisibleCells.Count}");
|
||||
_out.WriteLine($" CellPortals = {envCell.CellPortals.Count}");
|
||||
for (int i = 0; i < envCell.CellPortals.Count; i++)
|
||||
{
|
||||
var p = envCell.CellPortals[i];
|
||||
string otherIdStr = p.OtherCellId == 0xFFFFu
|
||||
? "0xFFFF (EXIT-OUTSIDE)"
|
||||
: $"0x{p.OtherCellId:X4} (other-indoor)";
|
||||
_out.WriteLine($" [{i}] otherCellId={otherIdStr} polyId=0x{p.PolygonId:X4} flags=0x{(uint)p.Flags:X4}");
|
||||
}
|
||||
|
||||
// Resolve the CellStruct (via Environment) so we can inspect the
|
||||
// portal polygons' plane equations. The plane equation is what
|
||||
// CellTransit.FindTransitCellsSphere tests the sphere against; if
|
||||
// the test wrongly rejects, the door is invisible.
|
||||
// Environment dat IDs are prefixed with 0x0D000000 — see
|
||||
// GameWindow.cs:5388 (`0x0D000000u | envCell.EnvironmentId`).
|
||||
bool envOk = dats.TryGet<DatReaderWriter.DBObjs.Environment>(
|
||||
0x0D000000u | envCell.EnvironmentId, out var environment);
|
||||
if (!envOk || environment is null)
|
||||
{
|
||||
_out.WriteLine($" Environment 0x{envCell.EnvironmentId:X8} NOT loadable");
|
||||
return;
|
||||
}
|
||||
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
||||
{
|
||||
_out.WriteLine($" Environment.Cells[0x{envCell.CellStructure:X8}] NOT present");
|
||||
return;
|
||||
}
|
||||
_out.WriteLine($" CellStruct verts = {cellStruct.VertexArray?.Vertices.Count ?? 0}");
|
||||
_out.WriteLine($" CellStruct polygons = {cellStruct.Polygons?.Count ?? 0} (visible)");
|
||||
_out.WriteLine($" CellStruct physicsPolys = {cellStruct.PhysicsPolygons?.Count ?? 0}");
|
||||
|
||||
// Dump the plane of each portal polygon (the planes
|
||||
// FindTransitCellsSphere tests the sphere center against).
|
||||
for (int i = 0; i < envCell.CellPortals.Count; i++)
|
||||
{
|
||||
var p = envCell.CellPortals[i];
|
||||
if (cellStruct.Polygons is null
|
||||
|| !cellStruct.Polygons.TryGetValue(p.PolygonId, out var poly))
|
||||
{
|
||||
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] NOT present in CellStruct.Polygons");
|
||||
continue;
|
||||
}
|
||||
// Resolve vertex positions.
|
||||
var vs = poly.VertexIds.Select(vid => cellStruct.VertexArray.Vertices[(ushort)vid].Origin).ToList();
|
||||
string vlist = string.Join(",", vs.Select(v => $"({v.X:F2},{v.Y:F2},{v.Z:F2})"));
|
||||
|
||||
// Compute the plane using the first three verts (normal = (v1-v0) x (v2-v0), d = -dot(n, v0)).
|
||||
if (vs.Count >= 3)
|
||||
{
|
||||
var n = System.Numerics.Vector3.Normalize(System.Numerics.Vector3.Cross(vs[1] - vs[0], vs[2] - vs[0]));
|
||||
float d = -System.Numerics.Vector3.Dot(n, vs[0]);
|
||||
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] n_local=({n.X:F4},{n.Y:F4},{n.Z:F4}) d_local={d:F4} verts={vlist}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] <3 verts, plane n/a verts={vlist}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue