using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using AcDream.App.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo; namespace AcDream.App.Tests.Rendering; /// /// #95 MEASUREMENT (2026-06-13): entering the 0x0007 dungeon (Town Network) explodes /// WB-DIAG to ~9.1M instances/frame. Suspected cause: /// floods the dungeon's portal graph WITHOUT the retail grab_visible_cells stab_list bounding /// (decomp:311878). A dungeon cell has seen_outside==0; retail's PVS for it is just the /// cell's stab_list () — typically a small bounded /// set. If our flood instead visits ~all cells of the landblock, that is the blowup. /// /// This is a DIAGNOSTIC, not a fix: it loads the real 0x0007 interior cells, runs the real /// production flood from representative dungeon-cell roots, and PRINTS the ground-truth numbers — /// flood visited-cell-set size () vs the /// root's stab_list size (), plus how many visited cells /// cross landblocks. The single assertion just guarantees the test ran; the VALUE is the output. /// public class Issue95DungeonFloodDiagnosticTests { private const uint TownNetwork = 0x00070000u; private readonly ITestOutputHelper _out; public Issue95DungeonFloodDiagnosticTests(ITestOutputHelper output) => _out = output; // Production-ish projection (mirrors the sibling harnesses): FovY ~1.2, 1280x720, // near 0.1, far 5000. The flood's clip is near-independent, so exactness is not // load-bearing for cell-count measurement. private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt) { var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ); var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f); return view * proj; } [Fact] public void Measure_DungeonFlood_VisibleCellCount() { var datDir = CornerFloodReplayTests.ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dat dir did not resolve (ACDREAM_DAT_DIR unset and " + "%USERPROFILE%\\Documents\\Asheron's Call absent). No numbers measured."); // Diagnostic test: do not hard-fail when dats are absent (matches sibling harnesses). return; } _out.WriteLine($"dat dir resolved: {datDir}"); using var dats = new DatCollection(datDir, DatAccessType.Read); // 1) LandBlockInfo header — NumCells for 0x0007. var lbi = dats.Get(TownNetwork | 0xFFFEu); if (lbi is null) { _out.WriteLine($"SKIP: LandBlockInfo 0x{TownNetwork | 0xFFFEu:X8} not found in the dat " + "(0x0007 may not exist in this client_cell_1.dat)."); return; } _out.WriteLine($"=== 0x0007 (Town Network) LandBlockInfo ==="); _out.WriteLine($"NumCells (DatLandBlockInfo.NumCells) = {lbi.NumCells}"); // 2) Load ALL interior cells (sparse ids tolerated — see LoadAllInteriorCells). var loaded = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, TownNetwork); _out.WriteLine($"cells actually loaded = {loaded.Count}"); Assert.True(loaded.Count > 0, "no interior cells loaded for 0x0007 — cannot measure"); Func lookup = id => loaded.TryGetValue(id, out var c) ? c : null; // 3) Per-cell stab_list (VisibleCells) distribution across ALL loaded cells. // This is the bounded retail PVS size we expect the flood to roughly match. var stabSizes = loaded.Values.Select(c => c.VisibleCells.Count).ToList(); int seenOutsideCount = loaded.Values.Count(c => c.SeenOutside); int interiorCount = loaded.Count - seenOutsideCount; _out.WriteLine(""); _out.WriteLine("=== stab_list (LoadedCell.VisibleCells) distribution over ALL loaded cells ==="); _out.WriteLine($"cells with SeenOutside==true (entrance/exterior-facing) = {seenOutsideCount}"); _out.WriteLine($"cells with SeenOutside==false (interior dungeon) = {interiorCount}"); if (stabSizes.Count > 0) _out.WriteLine(FormattableString.Invariant( $"VisibleCells.Count min={stabSizes.Min()} max={stabSizes.Max()} avg={stabSizes.Average():F1} sum={stabSizes.Sum()}")); int emptyStab = stabSizes.Count(s => s == 0); _out.WriteLine($"cells with EMPTY stab_list (no dat PVS) = {emptyStab}"); // 4) Pick representative DUNGEON roots: the first interior (SeenOutside==false) cells in // ascending id order. If none exist, fall back to 0x00070100 and report that. var interiorRoots = loaded .Where(kv => !kv.Value.SeenOutside) .OrderBy(kv => kv.Key) .Select(kv => kv.Value) .Take(5) .ToList(); if (interiorRoots.Count == 0) { _out.WriteLine(""); _out.WriteLine("NOTE: NO cell has SeenOutside==false (all cells see the exterior). " + "Falling back to root 0x00070100 for the flood measurement."); if (loaded.TryGetValue(TownNetwork | 0x0100u, out var fallback)) interiorRoots.Add(fallback); else { _out.WriteLine("WARN: 0x00070100 not loaded either; using the lowest-id loaded cell."); interiorRoots.Add(loaded.OrderBy(kv => kv.Key).First().Value); } } _out.WriteLine(""); _out.WriteLine("=== PER-ROOT FLOOD MEASUREMENT (PortalVisibilityBuilder.Build) ==="); _out.WriteLine("property read for the visited-cell set: PortalVisibilityFrame.OrderedVisibleCells"); _out.WriteLine("root | seenOut | stab(VisibleCells) | flood(OrderedVisibleCells) | crossLB | dir"); var floodSizes = new List(); foreach (var root in interiorRoots) { // Eye at the root cell's world origin, looking toward its first portal (or +X if none), // so the flood actually fires through an opening. Sweep all 6 axis directions and KEEP // the maximum visited-set — the blowup is a worst-case-over-orientation quantity. var eye = root.WorldPosition; int bestFlood = -1; string bestDir = "?"; int bestCrossLb = -1; List? bestVisited = null; // Direction candidates: toward each portal's polygon centroid (the natural look-through), // plus the 6 cardinal axes as a fallback sweep. var lookTargets = new List<(Vector3 target, string label)>(); for (int pi = 0; pi < root.Portals.Count && pi < root.PortalPolygons.Count; pi++) { var poly = root.PortalPolygons[pi]; if (poly is { Length: >= 1 }) { var cl = Vector3.Zero; foreach (var v in poly) cl += v; cl /= poly.Length; lookTargets.Add((Vector3.Transform(cl, root.WorldTransform), $"portal{pi}->0x{root.Portals[pi].OtherCellId:X4}")); } } foreach (var (d, lbl) in new (Vector3, string)[] { (Vector3.UnitX, "+X"), (-Vector3.UnitX, "-X"), (Vector3.UnitY, "+Y"), (-Vector3.UnitY, "-Y"), (Vector3.UnitZ, "+Z"), (-Vector3.UnitZ, "-Z"), }) lookTargets.Add((eye + d * 5f, lbl)); foreach (var (target, label) in lookTargets) { if (Vector3.DistanceSquared(target, eye) < 1e-6f) continue; var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, target)); int floodN = frame.OrderedVisibleCells.Count; if (floodN > bestFlood) { bestFlood = floodN; bestDir = label; bestVisited = frame.OrderedVisibleCells; bestCrossLb = frame.OrderedVisibleCells.Count(id => (id & 0xFFFF0000u) != TownNetwork); } } floodSizes.Add(bestFlood); _out.WriteLine(FormattableString.Invariant( $"0x{root.CellId:X8} | {(root.SeenOutside ? "Y" : "N"),5} | {root.VisibleCells.Count,18} | {bestFlood,26} | {bestCrossLb,7} | {bestDir}")); // For the FIRST root, also print the actual visited set + stab set for eyeballing. if (ReferenceEquals(root, interiorRoots[0]) && bestVisited is not null) { _out.WriteLine(" first-root visited (OrderedVisibleCells, low ids): " + string.Join(" ", bestVisited.Select(id => $"{id & 0xFFFFu:X4}"))); _out.WriteLine(" first-root stab_list (VisibleCells, low ids): " + string.Join(" ", root.VisibleCells.Select(id => $"{id & 0xFFFFu:X4}"))); } } // 5) Aggregate flood-size stats across the sampled roots — the headline numbers. _out.WriteLine(""); _out.WriteLine("=== AGGREGATE over sampled roots ==="); if (floodSizes.Count > 0) _out.WriteLine(FormattableString.Invariant( $"flood visited-set size (OrderedVisibleCells): min={floodSizes.Min()} max={floodSizes.Max()} avg={floodSizes.Average():F1} (NumCells={lbi.NumCells}, loaded={loaded.Count})")); var sampledStab = interiorRoots.Select(r => r.VisibleCells.Count).ToList(); if (sampledStab.Count > 0) _out.WriteLine(FormattableString.Invariant( $"sampled roots' stab_list size (VisibleCells): min={sampledStab.Min()} max={sampledStab.Max()} avg={sampledStab.Average():F1}")); _out.WriteLine(""); _out.WriteLine("INTERPRETATION: if flood max ~= loaded.Count (visits ~all cells) while stab " + "is small, that is the #95 blowup — the flood is unbounded by the retail stab_list PVS."); } }