From 1b8c9f1f50de5b7e871c009be1ffbc993d16a6cb Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 11 Jun 2026 17:59:52 +0200 Subject: [PATCH] #119: tower decoded - the "missing stairs" are Setup 0x020003F2 (43-part spiral staircase); the "barrel" is a legit dat static orphaned by it The user stood in the tower and logged out; the next login [snap] pinned it: cell 0xAAB30107, AAB3 building[1] (model 0x01001117, NOT the #113 meeting hall 0x010014C3). Issue119TowerDumpTests decodes the dat truth: - The tower interior cell 0x0107 has ZERO ramp polys - the stairs are NOT cell geometry. They are cell STATICS: Setup 0x020003F2 at the exact tower center = a 43-part spiral staircase (5 corner platforms 0x01000E2A + 38 steps 0x01000E2B/2C/2D/2F/31/32, placement frames spiraling z 0.35..15.15, all parts fully drawable - 0 NoPos polys). - The four 0x020005D8 statics (1 part, 0x01001774, 24 polys - barrel- shaped) sit along the wall at ascending heights: legitimate dat barrels on the stair landings. With the staircase missing, the bottom one reads as "a barrel in the middle where it's not supposed to be" - the barrel is NOT extraneous, the stairs around it are gone. Pipeline reads (all correct by read, no errors logged): BuildInteriorEntitiesForStreaming flattens Setup statics per part (SetupMesh.Flatten -> 43 MeshRefs with placement transforms); LandblockSpawnAdapter registers per MeshRef GfxObj id; the dispatcher walks per MeshRef and composes PartTransform * entityWorld; the ConsoleErrorLogger (wb-error) is wired and silent. Remaining suspects are runtime-state: the #55 meshMissing population (parts never finishing PrepareMeshDataAsync) or a draw-level drop - the saved character now spawns INSIDE the tower, so a WB_DIAG launch at spawn reads the dispatcher counters directly on the exact content. Co-Authored-By: Claude Fable 5 --- .../Conformance/Issue119TowerDumpTests.cs | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs diff --git a/tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs b/tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs new file mode 100644 index 00000000..6f011285 --- /dev/null +++ b/tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs @@ -0,0 +1,184 @@ +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}"); + } + } +}