acdream/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs
Erik 8d93665053 #119: the [up-null] lead is EXONERATED (dat-proven) - both GfxObjs are legitimately no-draw models
Issue119UpNullGfxObjDumpTests pins the dat truth: 0x010002B4 = 9 polys,
ALL NoPos, all surfaces Base1Solid; 0x010008A8 = 1 poly, NoPos,
Base1Solid|Translucent. Retail's skipNoTexture never draws either model
(the BR-1 build-time-skip <=> draw-time-skip equivalence), so
ObjectMeshManager's empty render-data cache is the CORRECT terminal state
- the only defect was the alarming "permanently invisible" log line,
reworded into an honest tripwire pointing at the dump test.

Second fact, same test (ShellModel_NoTexturedPolyIsDropped): on the
hall/tower shell 0x010014C3, ZERO textured polys are dropped by the
extraction gates (137/149 draw; the 12 dropped are the known #113
no-draw orphans) - the per-poly GfxObj extraction is exonerated for
building shells, kept green as a regression pin.

Net for #119: the missing tower-stair parts are NOT the up-null pair and
NOT a per-poly extraction drop. Remaining hypothesis space (interior
stair-cell flood admission, or a different model than assumed) needs the
re-gate to identify the exact tower; then the cell set + flood replay
headlessly like #118. ISSUES.md updated.

Suites: App 232, Core 1419+2skip (1416+3 new), UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:55:45 +02:00

139 lines
7 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 DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.Core.Tests.Conformance;
/// <summary>
/// #119 diagnostic dump (2026-06-11): GfxObjs 0x010002B4 and 0x010008A8 hit
/// `[up-null] upload returned null — caching EMPTY render data (permanently
/// invisible)` at startup (t5-gate-launch.log:33-34); the old tower shows
/// missing stair parts (visible in retail — user axiom). UploadGfxObjMeshData
/// returns null only when the PREPARE phase produced ZERO vertices
/// (ObjectMeshManager.cs:1780), so the upload is innocent — some extraction
/// gate dropped every polygon. This dump prints the raw dat facts per polygon
/// and replicates PrepareGfxObjMeshData's gates (ObjectMeshManager.cs:1040-1058)
/// so the zeroing gate reads directly off the output.
/// </summary>
public sealed class Issue119UpNullGfxObjDumpTests
{
private readonly ITestOutputHelper _out;
public Issue119UpNullGfxObjDumpTests(ITestOutputHelper output) => _out = output;
public static readonly TheoryData<uint> UpNullIds = new() { 0x010002B4u, 0x010008A8u };
[Theory]
[MemberData(nameof(UpNullIds))]
public void DumpUpNullGfxObj(uint id)
{
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<GfxObj>(id, out var gfx) && gfx is not null,
$"GfxObj 0x{id:X8} not in portal dat");
_out.WriteLine($"=== GfxObj 0x{id:X8} ===");
_out.WriteLine($"Flags={gfx!.Flags} Polygons={gfx.Polygons.Count} Vertices={gfx.VertexArray.Vertices.Count} Surfaces={gfx.Surfaces.Count}");
_out.WriteLine($"DrawingBSP={(gfx.DrawingBSP?.Root is null ? "NONE" : "present")} PhysicsBSP={(gfx.PhysicsBSP?.Root is null ? "NONE" : "present")}");
for (int i = 0; i < gfx.Surfaces.Count; i++)
{
uint sid = gfx.Surfaces[i];
string stype = dats.Portal.TryGet<Surface>(sid, out var surf) && surf is not null
? surf.Type.ToString()
: "MISSING";
_out.WriteLine($" surface[{i}] = 0x{sid:X8} type={stype}");
}
// Replicate the extraction gates (PrepareGfxObjMeshData):
// pos added when !NoPos
// neg added when Negative || Both || (!NoNeg && SidesType==Clockwise)
// surface index must be in [0, Surfaces.Count)
int wouldAddPos = 0, wouldAddNeg = 0, degenerate = 0;
var gateHistogram = new Dictionary<string, int>();
foreach (var (pid, poly) in gfx.Polygons.OrderBy(kv => kv.Key))
{
string gate;
if (poly.VertexIds.Count < 3) { degenerate++; gate = "degenerate(<3 verts)"; }
else
{
bool pos = !poly.Stippling.HasFlag(StipplingType.NoPos)
&& poly.PosSurface >= 0 && poly.PosSurface < gfx.Surfaces.Count;
bool neg = (poly.Stippling.HasFlag(StipplingType.Negative)
|| poly.Stippling.HasFlag(StipplingType.Both)
|| (!poly.Stippling.HasFlag(StipplingType.NoNeg) && poly.SidesType == CullMode.Clockwise))
&& poly.NegSurface >= 0 && poly.NegSurface < gfx.Surfaces.Count;
if (pos) wouldAddPos++;
if (neg) wouldAddNeg++;
gate = pos || neg ? "DRAWS" : $"DROPPED stip={poly.Stippling} sides={poly.SidesType} posSurf={poly.PosSurface} negSurf={poly.NegSurface}";
}
gateHistogram.TryGetValue(gate, out int c);
gateHistogram[gate] = c + 1;
_out.WriteLine(FormattableString.Invariant(
$" poly[{pid}] verts={poly.VertexIds.Count} stip={poly.Stippling} sides={poly.SidesType} posSurf={poly.PosSurface} negSurf={poly.NegSurface} gate={gate}"));
}
_out.WriteLine($"--- summary: wouldAddPos={wouldAddPos} wouldAddNeg={wouldAddNeg} degenerate={degenerate} of {gfx.Polygons.Count} polys ---");
foreach (var (gate, count) in gateHistogram.OrderByDescending(kv => kv.Value))
_out.WriteLine($" {count,3} × {gate}");
}
/// <summary>
/// #119 second fact: does the extraction drop any polys that retail WOULD
/// draw (textured, non-solid surface) on a building-shell model? Run on the
/// Holtburg meeting-hall shell 0x010014C3 (the #113-saga tower whose stairs
/// are "regular shell polys" — render digest user axiom). A non-zero
/// "DROPPED but textured" count names the extraction as the stairs-miss
/// mechanism; zero exonerates the per-poly gates.
/// </summary>
[Theory]
[InlineData(0x010014C3u)]
public void ShellModel_NoTexturedPolyIsDropped(uint id)
{
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<GfxObj>(id, out var gfx) && gfx is not null,
$"GfxObj 0x{id:X8} not in portal dat");
bool SurfaceIsTextured(short idx)
{
if (idx < 0 || idx >= gfx!.Surfaces.Count) return false;
if (!dats.Portal.TryGet<Surface>(gfx.Surfaces[idx], out var surf) || surf is null) return false;
return !surf.Type.HasFlag(SurfaceType.Base1Solid);
}
int draws = 0;
var droppedTextured = new List<string>();
foreach (var (pid, poly) in gfx!.Polygons.OrderBy(kv => kv.Key))
{
if (poly.VertexIds.Count < 3) continue;
bool pos = !poly.Stippling.HasFlag(StipplingType.NoPos)
&& poly.PosSurface >= 0 && poly.PosSurface < gfx.Surfaces.Count;
bool neg = (poly.Stippling.HasFlag(StipplingType.Negative)
|| poly.Stippling.HasFlag(StipplingType.Both)
|| (!poly.Stippling.HasFlag(StipplingType.NoNeg) && poly.SidesType == CullMode.Clockwise))
&& poly.NegSurface >= 0 && poly.NegSurface < gfx.Surfaces.Count;
if (pos || neg) { draws++; continue; }
// dropped by the gates — would retail have drawn a textured side?
if (SurfaceIsTextured(poly.PosSurface) || SurfaceIsTextured(poly.NegSurface))
droppedTextured.Add(FormattableString.Invariant(
$"poly[{pid}] stip={poly.Stippling} sides={poly.SidesType} posSurf={poly.PosSurface} negSurf={poly.NegSurface}"));
}
_out.WriteLine($"GfxObj 0x{id:X8}: polys={gfx.Polygons.Count} drawnByGates={draws} droppedTextured={droppedTextured.Count}");
foreach (var line in droppedTextured)
_out.WriteLine($" {line}");
Assert.True(droppedTextured.Count == 0,
$"{droppedTextured.Count} textured polys are dropped by the extraction gates on 0x{id:X8} — see output");
}
}