using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; using DatEnvironment = DatReaderWriter.DBObjs.Environment; using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo; namespace AcDream.Core.Tests.Conformance; /// /// #119 diagnostic dump (2026-06-11): the user stood inside the "old tower" /// and logged out — the next login snap pinned it: cell 0xAAB30107, /// AAB3-local ≈ (107, 59, 112). Symptoms: stair parts INVISIBLE (visible in /// retail — axiom) + an extraneous water barrel "in the middle of it". /// This dump prints the dat truth for the tower's cell neighbourhood: /// per-cell environment/structure, position, SeenOutside, the stair /// signature (distinct flat-face heights + ramp polys — the Issue113 dump's /// discriminator), the portal topology, the VisibleCells list, and every /// static object (id + position) — the barrel candidate and the stair /// owners read directly off the output. /// public sealed class Issue119TowerDumpTests { private readonly ITestOutputHelper _out; public Issue119TowerDumpTests(ITestOutputHelper output) => _out = output; private const uint Landblock = 0xAAB30000u; private const uint EnvironmentFilePrefix = 0x0D000000u; private static readonly Vector3 TowerSpot = new(107.4f, 59.0f, 112.0f); // AAB3-local, from the [snap] [Fact] public void DumpTowerCellNeighbourhood() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var lbi = dats.Get(Landblock | 0xFFFEu); Assert.NotNull(lbi); _out.WriteLine($"=== LandBlockInfo 0x{Landblock | 0xFFFEu:X8}: NumCells={lbi!.NumCells}, Buildings={lbi.Buildings?.Count ?? 0} ==="); if (lbi.Buildings is not null) for (int b = 0; b < lbi.Buildings.Count; b++) { var bld = lbi.Buildings[b]; _out.WriteLine(FormattableString.Invariant( $" building[{b}] model=0x{bld.ModelId:X8} origin=({bld.Frame.Origin.X:F1},{bld.Frame.Origin.Y:F1},{bld.Frame.Origin.Z:F1}) portals={bld.Portals?.Count ?? 0}")); if (bld.Portals is not null) foreach (var bp in bld.Portals) _out.WriteLine(FormattableString.Invariant( $" bldPortal otherCell=0x{bp.OtherCellId:X4} stabs=[{string.Join(",", (bp.StabList ?? new List()).Select(s => $"0x{s:X4}"))}]")); } foreach (uint low in EnumerateCells(lbi.NumCells)) { uint id = Landblock | low; var dc = dats.Get(id); if (dc is null) continue; var env = dats.Get(EnvironmentFilePrefix | dc.EnvironmentId); if (env is null || !env.Cells.TryGetValue(dc.CellStructure, out var cs) || cs is null) { _out.WriteLine($"cell 0x{id:X8}: env 0x{dc.EnvironmentId:X} struct {dc.CellStructure} MISSING"); continue; } var world = Matrix4x4.CreateFromQuaternion(dc.Position.Orientation) * Matrix4x4.CreateTranslation(dc.Position.Origin); // bounds + distance to the user's spot var min = new Vector3(float.MaxValue); var max = new Vector3(float.MinValue); foreach (var kvp in cs.VertexArray.Vertices) { var p = Vector3.Transform(new Vector3(kvp.Value.Origin.X, kvp.Value.Origin.Y, kvp.Value.Origin.Z), world); min = Vector3.Min(min, p); max = Vector3.Max(max, p); } var center = (min + max) * 0.5f; float dist = Vector2.Distance(new Vector2(center.X, center.Y), new Vector2(TowerSpot.X, TowerSpot.Y)); bool near = dist < 18f; if (!near) continue; // only the tower neighbourhood // stair signature (Issue113 discriminator) var flatHeights = new HashSet(); int rampPolys = 0; foreach (var poly in cs.Polygons.Values) { if (poly.VertexIds.Count < 3) continue; var pts = new List(poly.VertexIds.Count); bool ok = true; foreach (var vid in poly.VertexIds) { if (!cs.VertexArray.Vertices.TryGetValue((ushort)vid, out var v)) { ok = false; break; } pts.Add(Vector3.Transform(new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z), world)); } if (!ok) continue; // Newell normal var n = Vector3.Zero; for (int i = 0; i < pts.Count; i++) { var a = pts[i]; var b = pts[(i + 1) % pts.Count]; n.X += (a.Y - b.Y) * (a.Z + b.Z); n.Y += (a.Z - b.Z) * (a.X + b.X); n.Z += (a.X - b.X) * (a.Y + b.Y); } if (n.LengthSquared() < 1e-12f) continue; n = Vector3.Normalize(n); if (MathF.Abs(n.Z) > 0.9f) flatHeights.Add((int)MathF.Round(pts.Average(p => p.Z) * 4f)); // 25 cm bins else if (MathF.Abs(n.Z) > 0.25f) rampPolys++; } bool seenOutside = dc.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside); _out.WriteLine(FormattableString.Invariant( $"cell 0x{id:X8} env=0x{dc.EnvironmentId:X4}/{dc.CellStructure} pos=({dc.Position.Origin.X:F1},{dc.Position.Origin.Y:F1},{dc.Position.Origin.Z:F1}) aabb=z[{min.Z:F1}..{max.Z:F1}] distXY={dist:F1} seenOutside={seenOutside} flatLevels={flatHeights.Count} rampPolys={rampPolys} polys={cs.Polygons.Count}")); _out.WriteLine($" portals: [{string.Join(" ", dc.CellPortals.Select(p => $"->0x{p.OtherCellId:X4}(poly{p.PolygonId},flags={(ushort)p.Flags})"))}]"); if (dc.VisibleCells is not null && dc.VisibleCells.Count > 0) _out.WriteLine($" visibleCells: [{string.Join(",", dc.VisibleCells.Select(v => $"0x{v:X4}"))}]"); if (dc.StaticObjects is not null) foreach (var so in dc.StaticObjects) _out.WriteLine(FormattableString.Invariant( $" static gfx=0x{so.Id:X8} at ({so.Frame.Origin.X:F2},{so.Frame.Origin.Y:F2},{so.Frame.Origin.Z:F2})")); } } private static IEnumerable EnumerateCells(uint numCells) { for (uint low = 0x0100u; low < 0x0100u + numCells; low++) yield return low; } /// /// The tower's statics are SETUPS: 0x020005D8 ×4 (spiraling up — the stair /// flights) + 0x020003F2 at the exact center (the user's "water barrel" /// spot). Dump their part lists — if the [up-null] no-draw GfxObjs /// (0x010002B4/0x010008A8) are parts here, the "missing stairs" are the /// VISIBLE sibling parts failing, or the whole-part placement is off. /// [Theory] [InlineData(0x020005D8u)] [InlineData(0x020003F2u)] public void DumpTowerStairSetups(uint setupId) { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); Assert.True(dats.Portal.TryGet(setupId, out var setup) && setup is not null, $"Setup 0x{setupId:X8} not in portal dat"); _out.WriteLine($"=== Setup 0x{setupId:X8}: parts={setup!.Parts.Count} ==="); for (int i = 0; i < setup.Parts.Count; i++) { uint gfxId = setup.Parts[i]; string polyInfo = "MISSING"; if (dats.Portal.TryGet(gfxId, out var gfx) && gfx is not null) { int noDraw = 0; foreach (var poly in gfx.Polygons.Values) if (poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos)) noDraw++; polyInfo = $"polys={gfx.Polygons.Count} noPos={noDraw} surfaces={gfx.Surfaces.Count}"; } string frame = ""; if (setup.PlacementFrames.Count > 0) { var pf = setup.PlacementFrames.Values.First(); if (i < pf.Frames.Count) { var f = pf.Frames[i]; frame = FormattableString.Invariant( $" frame0=({f.Origin.X:F2},{f.Origin.Y:F2},{f.Origin.Z:F2})"); } } _out.WriteLine($" part[{i}] gfx=0x{gfxId:X8} {polyInfo}{frame}"); } } }