test(phys): A6.P4 door inside-out — locate cottage wall, identify corner-slide hypothesis

Followed up the geometry-gap diagnosis with a wider polygon search.
Result: the cottage's north exterior wall east of doorway DOES exist
in cottage GfxObj 0x01000A2B (polys 0x0032, 0x0033) at
world X=[133.5, 136.3], Y=17.10, Z=[94, 97], normal +Y. Symmetric
polys cover the west side and above the doorway lintel.

The wall SHOULD block sphere at X=133.655 (sphere west edge at 133.175
overlaps wall X range; sphere south edge at 17.11 aligns with wall
at Y=17.10).

New hypothesis: the bug is sphere-vs-corner collision at the meeting
point of cell 0x0150's east wall (X=133.5, Y=[16.5, 17.1]) and the
cottage's north exterior wall (X=[133.5, 136.3], Y=17.10). Cell
transit data shows sphere going from X=132.859 entering alcove to
X=134.022 leaving alcove — sphere reached X=134.022 INSIDE cottage
geometry somehow. The sliding along the slab east face (cn=(+1,0,0)
in captured tick 3254) gradually pushes sphere east. Eventually it
shifts past X=133.5 — the corner where alcove east wall meets cottage
north wall. The corner-handling in our BSP collision may incorrectly
let the sphere slide past, or the alcove cell's east wall and cottage
GfxObj's north wall don't compose correctly at the corner.

Diagnostic apparatus extensions:
- HoltburgLandblockStatics_DatInspection: dumps LandBlockInfo for
  landblock 0xA9B4. Shows 114 stabs + 12 buildings. The cottage IS
  Building[6] with modelId=0x01000A2B (the GfxObj we already loaded).
- Diagnostic_CottagePolys_NearWalkthroughPosition: widened search
  reveals the cottage's full north exterior wall geometry.
- HoltburgCottage_CellPortals_DatInspection: extended with cell
  PhysicsPolygon world-frame dump (already in prior commit).

Full updated handoff: docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md

Next-session move: add a "sphere walks +Y from inside alcove at
X near 133" test. If harness slides past the corner like production,
investigate BSPQuery's sphere-vs-edge case. If harness blocks at
corner, the bug is elsewhere (cell 0x0150 BSP not queried, or
cottage GfxObj BSP traversal misses the wall poly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-25 08:34:52 +02:00
parent da798b2071
commit fe29db5691
3 changed files with 190 additions and 2 deletions

View file

@ -79,7 +79,92 @@ walk there freely.
- The slab east face blocks WEST motion correctly. The sphere is FREE
to move north because no geometry covers (133.655, Y > 17.1).
## What's next
## UPDATE (2026-05-25 evening): the wall EXISTS, but isn't blocking
Continued investigation with a wider polygon search in
`Diagnostic_CottagePolys_NearWalkthroughPosition` revealed the cottage
DOES have the missing wall:
```
poly 0x0032 n=(0.00, +1.00, 0.00) X=[133.50, 136.30] Y=[17.10, 17.10] Z=[94.00, 97.00]
poly 0x0033 n=(0.00, +1.00, 0.00) X=[133.50, 136.30] Y=[17.10, 17.10] Z=[94.00, 97.00]
```
(Plus symmetric polys 0x0030, 0x0031, 0x0034, 0x0035 covering X<131.6,
0x0037, 0x0038, 0x003A, 0x003B above the doorway lintel.)
The cottage's north exterior wall east of doorway IS at world (X=[133.5,
136.3], Y=17.10, Z=[94, 97]), normal +Y. **This wall SHOULD block sphere
at X=133.655 (sphere west edge at 133.175 ≤ wall X range, sphere south
edge at 17.110 ≤ wall Y).**
The new question: WHY isn't the wall blocking in production?
Sphere at world (133.655, 17.59) at the captured failing tick:
- Sphere XY: X=[133.175, 134.135], Y=[17.110, 18.070]
- Sphere overlaps wall in X (133.175..134.135 vs 133.5..136.3) by 0.635m
- Sphere south edge at Y=17.110 ALIGNS with wall at Y=17.10 (0.010m past)
- Sphere CENTER at Y=17.59 is 0.49m north of wall
- Distance from sphere center to wall plane: 0.49m. Sphere radius 0.48m.
- |dist| (0.49) ≈ radius (0.48). Sphere is JUST grazing the wall plane.
At this exact tick the sphere CENTER is 0.49m north of wall; sphere
south edge is 0.01m north of wall. Sphere is BARELY past the wall.
So this tick isn't where the walkthrough happens. The walkthrough is
EARLIER — when sphere center Y went from 17.58 (just past wall by reach)
to 17.59. The crossing must have allowed the sphere through.
OR: the sphere never actually crossed the wall — it walked around it.
Cottage wall east of doorway is X=[133.5, 136.3]. Sphere at X=133.655
is barely in the wall's X range. If sphere came from X < 133.5 (where
no east wall exists) and shifted east while sliding along the slab,
it could end up at X > 133.5 having NEVER crossed the wall plane.
Cell transit data confirms: tick 1549 outdoor→indoor at X=132.859,
tick 2586 indoor→outdoor at X=134.022 (way past wall east edge).
**The sphere reached X=134.022 inside cottage geometry somehow.**
Sphere fitting through doorway opening requires center X in
[131.6+0.48, 133.5-0.48] = [132.08, 133.02]. Tight. The user's
off-center test (~50cm east) puts sphere at edge of opening or
past. Sphere is sliding against the slab east face (cn=(+1,0,0))
which gradually pushes it east. Eventually sphere center exceeds
X=133.5 — past the cottage east wall's start. From that position,
sphere can move north WITHOUT crossing the wall plane (sphere
center already north of Y=17.10 from prior sliding).
**This may be retail-faithful behavior** OR a bug in sphere-vs-corner
collision. The corner where alcove east wall (X=133.5, Y=[16.5,17.1])
meets cottage north wall (X=[133.5,136.3], Y=17.10) is a degenerate
edge. Sphere sliding along the alcove east wall (moving +Y) reaches
the corner at (133.5, 17.10) — should encounter the cottage wall
and be stopped. If our engine handles the corner transition
incorrectly, sphere slides past.
## What's next (revised)
**Investigate sphere-vs-corner collision behavior** at the alcove
east wall → cottage north wall meeting point at world (133.5, 17.10).
Apparatus to write:
- Load cottage GfxObj + cell 0x0150 BSP into harness
- Place sphere at (133.0, 16.8, 94) (inside alcove, near east wall)
- Walk sphere +Y in small increments
- Expected: sphere stops at Y=17.10-0.48 = 16.62 if east wall blocks
OR: sphere slides along corner staying in alcove
- Captured: sphere ends up at (133.655, 17.59) — sliding past corner
If harness reproduces "sphere slides past corner", the bug is in
the engine's corner-handling. Read BSPQuery for the sphere-vs-edge
case. Retail oracle: CTransition::find_obj_collisions corner
handling at acclient_2013_pseudo_c.txt.
If harness BLOCKS at corner (no sliding past), the bug is something
else — maybe cell 0x0150 BSP isn't being queried in production from
some sphere position.
## OLD (superseded) "what's next" candidates
**Identify which entity SHOULD own the cottage's north exterior wall
east of the doorway.** Three candidates:

View file

@ -474,10 +474,39 @@ public class DoorBugTrajectoryReplayTests
Console.WriteLine($" Sphere Y=[{sphereCenterY-0.48f:F3}, {sphereCenterY+0.48f:F3}]");
Console.WriteLine($" Sphere Z=[94.000, 95.200]");
// Also dump ALL polys with vertices near sphere XY (loose: 3m window)
// so we can see what wall geometry the cottage HAS in the area.
Console.WriteLine("");
Console.WriteLine("=== Cottage polys with bbox extending into (X in [130,138], Y in [13,21]) ===");
int nearXYCount = 0;
foreach (var (polyId, poly) in physics.Resolved)
{
float wxMin = float.MaxValue, wxMax = float.MinValue;
float wyMin = float.MaxValue, wyMax = float.MinValue;
float wzMin = float.MaxValue, wzMax = float.MinValue;
foreach (var v in poly.Vertices)
{
var rotated = Vector3.Transform(v, cottageRot);
var world = cottagePos + rotated;
if (world.X < wxMin) wxMin = world.X; if (world.X > wxMax) wxMax = world.X;
if (world.Y < wyMin) wyMin = world.Y; if (world.Y > wyMax) wyMax = world.Y;
if (world.Z < wzMin) wzMin = world.Z; if (world.Z > wzMax) wzMax = world.Z;
}
// Wide search window.
if (wxMax < 130 || wxMin > 138) continue;
if (wyMax < 13 || wyMin > 21) continue;
nearXYCount++;
var nWorld = Vector3.Transform(poly.Plane.Normal, cottageRot);
Console.WriteLine(string.Format(System.Globalization.CultureInfo.InvariantCulture,
" poly 0x{0:X4} n=({1:F2},{2:F2},{3:F2}) X=[{4:F2},{5:F2}] Y=[{6:F2},{7:F2}] Z=[{8:F2},{9:F2}]",
polyId, nWorld.X, nWorld.Y, nWorld.Z, wxMin, wxMax, wyMin, wyMax, wzMin, wzMax));
}
Console.WriteLine($" Total: {nearXYCount}");
int matched = 0;
int matchedXY = 0;
Console.WriteLine("");
Console.WriteLine("=== All cottage polys with XY overlap (any Z) ===");
Console.WriteLine("=== Tight: All cottage polys with XY overlap of sphere AABB (any Z) ===");
foreach (var (polyId, poly) in physics.Resolved)
{
// Transform vertices to world space.

View file

@ -262,6 +262,80 @@ public class DoorSetupGfxObjInspectionTests
}
}
/// <summary>
/// A6.P4 door inside-out (2026-05-25 late) — inspects LandBlockInfo
/// 0xA9B4FFFE to identify all static entities at the Holtburg
/// cottage doorway area. The captured walkthrough has sphere at
/// world (133.655, 17.59) — no cottage GfxObj polys exist there.
/// Maybe a different entity (stab object, second cottage GfxObj,
/// building wall sub-piece) lives at that XY.
/// </summary>
[Fact]
public void HoltburgLandblockStatics_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);
// Landblock 0xA9B4 — the captured Holtburg cottage area.
// LandBlockInfo id = (landblockId & 0xFFFF0000) | 0xFFFE
var lbInfo = dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(0xA9B4FFFEu);
if (lbInfo is null)
{
_out.WriteLine("LandBlockInfo 0xA9B4FFFE: NULL");
return;
}
_out.WriteLine($"=== LandBlockInfo 0xA9B4FFFE ===");
_out.WriteLine($" NumCells = {lbInfo.NumCells}");
_out.WriteLine($" Objects = {lbInfo.Objects.Count} (landblock stabs)");
_out.WriteLine($" Buildings = {lbInfo.Buildings.Count}");
// The captured walkthrough sphere position.
const float SphereX = 133.655f;
const float SphereY = 17.59f;
const float Window = 4f; // search window
_out.WriteLine("");
_out.WriteLine($"=== Stabs (Objects) within {Window} m of sphere XY ({SphereX:F2}, {SphereY:F2}) ===");
int stabHits = 0;
for (int i = 0; i < lbInfo.Objects.Count; i++)
{
var stab = lbInfo.Objects[i];
float dx = stab.Frame.Origin.X - SphereX;
float dy = stab.Frame.Origin.Y - SphereY;
float dist = MathF.Sqrt(dx * dx + dy * dy);
if (dist > Window) continue;
stabHits++;
_out.WriteLine($" [{i}] id=0x{stab.Id:X8} pos=({stab.Frame.Origin.X:F3},{stab.Frame.Origin.Y:F3},{stab.Frame.Origin.Z:F3}) dist={dist:F3}");
}
_out.WriteLine($" Total stab hits: {stabHits}");
_out.WriteLine("");
_out.WriteLine($"=== ALL Buildings (sorted by distance to sphere) ===");
var buildings = lbInfo.Buildings
.Select((b, i) => new {
Idx = i, B = b,
Dist = MathF.Sqrt(
(b.Frame.Origin.X - SphereX) * (b.Frame.Origin.X - SphereX) +
(b.Frame.Origin.Y - SphereY) * (b.Frame.Origin.Y - SphereY))
})
.OrderBy(x => x.Dist)
.Take(6)
.ToList();
foreach (var x in buildings)
{
_out.WriteLine($" [{x.Idx}] modelId=0x{x.B.ModelId:X8} pos=({x.B.Frame.Origin.X:F3},{x.B.Frame.Origin.Y:F3},{x.B.Frame.Origin.Z:F3}) dist={x.Dist:F3} portals={x.B.Portals.Count} numLeaves={x.B.NumLeaves}");
}
}
private void InspectCell(DatCollection dats, uint cellId)
{
string typeLabel = (cellId & 0xFFFFu) >= 0x0100u ? "indoor" : "outdoor";