#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>
This commit is contained in:
parent
c4464739d2
commit
1b8c9f1f50
1 changed files with 184 additions and 0 deletions
184
tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs
Normal file
184
tests/AcDream.Core.Tests/Conformance/Issue119TowerDumpTests.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue