acdream/tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs
Erik 1b8c9f1f50 #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 <noreply@anthropic.com>
2026-06-11 17:59:52 +02:00

184 lines
8.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// #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.
/// </summary>
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<DatLandBlockInfo>(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<ushort>()).Select(s => $"0x{s:X4}"))}]"));
}
foreach (uint low in EnumerateCells(lbi.NumCells))
{
uint id = Landblock | low;
var dc = dats.Get<DatEnvCell>(id);
if (dc is null) continue;
var env = dats.Get<DatEnvironment>(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>();
int rampPolys = 0;
foreach (var poly in cs.Polygons.Values)
{
if (poly.VertexIds.Count < 3) continue;
var pts = new List<Vector3>(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<uint> EnumerateCells(uint numCells)
{
for (uint low = 0x0100u; low < 0x0100u + numCells; low++)
yield return low;
}
/// <summary>
/// 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.
/// </summary>
[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<DatReaderWriter.DBObjs.Setup>(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<DatReaderWriter.DBObjs.GfxObj>(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}");
}
}
}