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}");
}
}
}