using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Conformance; /// /// #112 (2026-06-10): the A9B3 hill cottage has a real containment GAP inside /// the house (world (184.9, −109.5, 116) / A9B3-local (184.9, 82.5) is in NO /// interior cell while points ~0.7 m away are inside 0x100/0x103). The /// 6dbbf95 escape hatch demoted the walker to the outdoor column there, /// stranding them outdoor-classified deep indoors (no containment-based /// re-promotion) → the outdoor flood rendered the interior transparent. /// Retail find_cell_list KEEPS curr_cell when nothing contains the centre /// (pc:308788-308825). These tests pin the replacement semantics against the /// real dats. /// public sealed class Issue112MembershipTests { private readonly ITestOutputHelper _out; public Issue112MembershipTests(ITestOutputHelper output) => _out = output; private const float FootRadius = 0.48f; private static PhysicsDataCache LoadLandblockInteriors(DatCollection dats, uint lbPrefix) { var cache = new PhysicsDataCache(); for (uint low = 0x0100; low <= 0x01FF; low++) { try { ConformanceDats.LoadEnvCell(dats, cache, lbPrefix | low); } catch { } } return cache; } [Fact] public void A9B3CottageGap_AtDoorway_StraddlesExitPlane_DemotesRetailFaithfully() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); // The gap point (A9B3-local frame, footcenter height): contained by NO // interior cell (dat-scan fact from the live capture issue111-verify7). var gap = new Vector3(184.915f, 82.464f, 116.48f); uint picked = CellTransit.FindCellList(cache, gap, FootRadius, 0xA9B30104u); _out.WriteLine($"pick(seed 0x104) at gap -> 0x{picked:X8}"); // RESOLVED 2026-06-10 (#112 rider, live-binary oracle): retail's // CEnvCell::find_transit_cells admits outdoor cells IFF a path sphere // STRADDLES an exterior portal's plane (|dist| < radius + F_EPSILON; // acclient.exe 0052c8e5-0052c9f0). The gap point sits 0.23 m from // 0x104's exit-door plane (x=184.684) with foot radius 0.48 — it // STRADDLES, so retail admits the outdoor column and demotes here too. // This at-doorway demote is RETAIL-FAITHFUL, not a divergence; it // self-heals one step inward via doorway re-promotion. The former // "DocumentsResidual" framing is closed — see the deep-gap test below // for the behavior that DID change with the straddle gate. Assert.Equal(0xA9B3003Cu, picked); } [Fact] public void A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell_RetailGate() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); // A no-cell point past the straddle window: 0.66 m beyond 0x104's // exit-door plane (x=184.684 + 0.48 + 0.18), still in no interior cell // (inside the shell-wall band). Pre-gate, the A6.P5 topology widening // let the outdoor column WIN the pick here → outdoor demote deep in a // containment gap (#112's transparent-interior shape). Retail keeps // curr_cell: no sphere straddles any exterior portal plane, so the // outdoor cells never become pick candidates (live-binary verified). var deepGap = new Vector3(185.345f, 82.464f, 116.48f); uint picked = CellTransit.FindCellList(cache, deepGap, FootRadius, 0xA9B30104u); _out.WriteLine($"pick(seed 0x104) at deep gap -> 0x{picked:X8}"); Assert.Equal(0xA9B30104u, picked); } [Fact] public void FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); // Cottage north room 0x102 has an exterior door at x=186 (portal poly // plane n=(±1,0,0), |d|=186 — dat dump 2026-06-10). Function-level pin // of the retail gate semantics on real dat geometry: var cell102 = cache.GetCellStruct(0xA9B30102u)!; // (a) Deep inside the room, 3 m from the door plane: the cell HAS an // exterior portal but no straddle → no outdoor admission flag // (retail: var_44 stays 0, add_all_outside skipped). (BR-7 C4 // deleted the non-retail hasExitPortal topology output — the // straddle flag is the only outdoor-admission signal, like // retail.) var farCandidates = new List(); CellTransit.FindTransitCellsSphere( cache, cell102, 0xA9B30102u, new Vector3(183.0f, 86.5f, 117.0f), FootRadius, farCandidates, out bool farStraddle); Assert.False(farStraddle); // (b) At the door plane (0.30 m away < 0.48 radius): straddle fires. var nearCandidates = new List(); CellTransit.FindTransitCellsSphere( cache, cell102, 0xA9B30102u, new Vector3(185.70f, 85.5f, 117.0f), FootRadius, nearCandidates, out bool nearStraddle); Assert.True(nearStraddle); } /// /// Register the A9B3 landblock's buildings into the cache exactly as /// production does (GameWindow streaming drain ~6080: portals → /// BldPortalInfo with sign-extended OtherPortalId; landcell id from the /// building Frame.Origin via the retail row-major grid formula). The /// fixture frame is block-local (ConformanceDats.LoadEnvCell uses /// EnvCell.Position verbatim), so Frame.Origin needs no offset. /// private static void RegisterBuildings(DatCollection dats, PhysicsDataCache cache, uint lbPrefix) { var lbInfo = dats.Get(lbPrefix | 0xFFFEu); Assert.NotNull(lbInfo); foreach (var building in lbInfo!.Buildings) { if (building.Portals.Count == 0) continue; var portals = new List(building.Portals.Count); foreach (var bp in building.Portals) portals.Add(new BldPortalInfo( otherCellId: lbPrefix | (uint)bp.OtherCellId, otherPortalId: unchecked((short)bp.OtherPortalId), flags: (ushort)bp.Flags)); var transform = Matrix4x4.CreateFromQuaternion(building.Frame.Orientation) * Matrix4x4.CreateTranslation(building.Frame.Origin); int gridX = (int)(building.Frame.Origin.X / 24f); int gridY = (int)(building.Frame.Origin.Y / 24f); uint landcellLow = (uint)(gridX * 8 + gridY + 1); cache.CacheBuilding(lbPrefix | landcellLow, portals, transform); } } [Fact] public void A9B3Cottage_OutdoorSeed_TickSkippedThreshold_RecoversViaGrowingWalk() { // #112 ROOT CAUSE (cottage-112-capture1.log, 2026-06-12): the cottage's // entry cell 0x104 is a 0.22 m-wide THRESHOLD band (x 184.68→184.46 at // y≈82.2). A player RUNNING crosses it in under one physics tick, so // the tick where the centre is inside 0x104 can simply never happen — // the next tick's centre is already in 0x100, a DEEP room (not // building-portal-adjacent). Retail recovers on that very tick: its // find_cell_list runs ONE growing-array walk for EVERY seed // (0052b576-0052b5ab, cells[i]->find_transit_cells vtable dispatch over // the GROWING array) — the landcell's building bridge admits 0x104 // (the sphere still overlaps the band) and the walk EXPANDS 0x104's // portals to 0x100, where containment wins. Our outdoor branch used to // stop at the bridge cells (no walk): the miss became ABSORBING — the // pick kept the outdoor landcell while the player walked the whole // interior (the transparent cottage), promoting only on touching a // portal-adjacent cell's own volume (the captured late 0x102 flip). var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); RegisterBuildings(dats, cache, 0xA9B30000u); // One tick past the skipped threshold: centre inside 0x100 (deep), foot // sphere still overlapping the 0x104 band (184.3 + 0.48 > 184.46). var pastThreshold = new Vector3(184.30f, 82.20f, 116.48f); uint picked = CellTransit.FindCellList(cache, pastThreshold, FootRadius, 0xA9B3003Cu); _out.WriteLine($"pick(seed 0x3C) one tick past the threshold -> 0x{picked:X8}"); Assert.Equal(0xA9B30100u, picked); } [Fact] public void A9B3Cottage_RunSpeedEntryReplay_NeverStrandsOutdoor() { // The live mechanism end-to-end at run speed: replay the captured entry // line in 16 cm ticks (≈5 m/s at 30 Hz physics). Whatever individual // tick lands or skips the 0.22 m threshold band, the seed-evolving pick // must end INDOOR by the time the player is inside — never the // absorbing outdoor-deep-inside state the user sat in (transparent // cottage until randomly touching a portal-adjacent room). var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); RegisterBuildings(dats, cache, 0xA9B30000u); // Sweep several tick phases so the threshold band is sometimes landed // on, sometimes skipped — every phase must end indoor. for (float phase = 0f; phase < 0.16f; phase += 0.04f) { uint curr = 0xA9B3003Cu; for (float x = 191.5f - phase; x > 181.0f; x -= 0.16f) { float t = (191.5f - x) / 10.5f; var p = new Vector3(x, 82.7f - t * 0.7f, 116.48f); curr = CellTransit.FindCellList(cache, p, FootRadius, curr); } _out.WriteLine($"phase {phase:F2}: final curr=0x{curr & 0xFFFFu:X4}"); Assert.True((curr & 0xFFFFu) >= 0x0100u, $"run-speed entry (phase {phase:F2}) stranded OUTDOOR (0x{curr:X8}) inside the cottage — the #112 absorbing state"); } } [Fact] public void Diagnostic_ReplayCapturedEntryWalk() { // Replays the cottage-112-capture1.log entry walk (world → A9B3-local = // world + (0, 192)): west along y≈82 through the doorway, the stand, // then the wander north to the observed 0x102 promotion point. Output // only — prints per step: containing cell (direct BSP scan), the // evolving pick, and which landcells had buildings. Names the first // step where retail would promote vs where ours does. var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); RegisterBuildings(dats, cache, 0xA9B30000u); _out.WriteLine($"buildings on landcells: {string.Join(", ", System.Linq.Enumerable.Select(cache.BuildingIds, b => $"0x{b:X8}"))}"); uint curr = 0xA9B3003Cu; uint lastPrinted = 0; void Step(Vector3 p) { uint contains = 0; for (uint low = 0x0100; low <= 0x010F; low++) { var cand = cache.GetCellStruct(0xA9B30000u | low); if (cand?.CellBSP?.Root is null) continue; var local = Vector3.Transform(p, cand.InverseWorldTransform); if (BSPQuery.PointInsideCellBsp(cand.CellBSP.Root, local)) { contains = 0xA9B30000u | low; break; } } uint picked = CellTransit.FindCellList(cache, p, FootRadius, curr); if (picked != curr || contains != lastPrinted) _out.WriteLine($" ({p.X:F2},{p.Y:F2}) contains=0x{contains & 0xFFFFu:X4} pick: 0x{curr & 0xFFFFu:X4} -> 0x{picked & 0xFFFFu:X4}"); lastPrinted = contains; curr = picked; } _out.WriteLine("— leg A: west along y 82.7→82.0, x 191.5→181.0 —"); for (float t = 0f; t <= 1f; t += 0.01f) Step(new Vector3(191.5f - t * 10.5f, 82.7f - t * 0.7f, 116.48f)); _out.WriteLine($"after leg A: curr=0x{curr & 0xFFFFu:X4}"); _out.WriteLine("— leg B: wander north (181.0,82.0) → (182.4,88.1) —"); for (float t = 0f; t <= 1f; t += 0.01f) Step(new Vector3(181.0f + t * 1.4f, 82.0f + t * 6.1f, 116.48f)); _out.WriteLine($"after leg B: curr=0x{curr & 0xFFFFu:X4}"); } [Fact] public void A9B3Cottage_OutdoorSeed_AtThresholdGap_KeepsOutdoor() { // Guard against over-fixing: the documented threshold gap is contained // by NO interior cell, so an outdoor-seeded pick there stays on the // outdoor column (retail: nothing contains the centre → the landcell // is the only container). The transparency at THIS exact spot is a // RENDER question (outdoor root at a doorway), not membership. var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B30000u); RegisterBuildings(dats, cache, 0xA9B30000u); var gap = new Vector3(184.915f, 82.464f, 116.48f); uint picked = CellTransit.FindCellList(cache, gap, FootRadius, 0xA9B3003Cu); _out.WriteLine($"pick(seed 0x3C) at gap -> 0x{picked:X8}"); Assert.Equal(0xA9B3003Cu, picked); } [Fact] public void ThresholdCottage_AdjacentClaim_LaterallyRecovers_ViaStabGraph() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = LoadLandblockInteriors(dats, 0xA9B40000u); // The #111 gate shape: claim 0x172 (adjacent room), position deep // inside 0x171 (the captured spawn). The sphere does not overlap // 0x172's volume from there → the new lateral recovery searches the // claim's stab list (retail find_visible_child_cell :311444) and // self-heals to 0x171 — instead of the old outdoor demote. var spawnFootCenter = new Vector3(155.390f, 11.020f, 94.0f + FootRadius); uint picked = CellTransit.FindCellList(cache, spawnFootCenter, FootRadius, 0xA9B40172u); _out.WriteLine($"pick(seed 0x172) at 0x171-interior -> 0x{picked:X8}"); Assert.Equal(0xA9B40171u, picked); } }