acdream/tests/AcDream.Core.Tests/Conformance/Issue119UpNullGfxObjDumpTests.cs
Erik b0758d772b refactor(pipeline): MP1a cleanup - narrow public surface, csproj parity, move residue
Coordinator-directed final cleanup before the user gate; none behavioral:

1. MeshExtractor public surface narrowed to the cross-assembly entry
   points App actually calls (PrepareMeshData, PrepareCellStructMeshData,
   CollectParts, ComputeBounds); PrepareSetupMeshData,
   CollectEmittersFromScript, PrepareGfxObjMeshData,
   PrepareEnvCellMeshData, PrepareCellStructEdgeLineData back to private
   (internal dispatch, only reached via PrepareMeshData).
2. sideStagedSink constructor parameter is now REQUIRED (no default;
   type stays nullable for a conscious null): a bake tool that forgot
   the sink would silently lose particle-preload meshes.
3. AcDream.Content.csproj gains TreatWarningsAsErrors + LangVersion
   latest (parity with AcDream.Core.csproj). Surfaced zero warnings.
4. Dead usings removed from ObjectMeshManager.cs (BCnEncoder.*,
   SixLabors.*) — the inline decode moved out in Task 4.
5. Doc fixes: ObjectMeshData.cs cross-assembly <see cref> ->
   plain text (Content can't resolve App types); IDatReaderWriter.cs
   stale Phase O-T7 'both in this namespace' sentence rewritten.
6. Stale test doc comments updated to MeshExtractor.PrepareGfxObjMeshData
   (StipplingSurfaceEquivalenceTests, Issue119UpNullGfxObjDumpTests) —
   comments only, no code/assertion changes.

dotnet build green (0 warnings in Content under warnings-as-errors);
full test suite 4059 passed / 0 failed / 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:52:01 +02:00

141 lines
7.1 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.UploadGfxObjMeshData's empty-vertices guard), so the
/// upload is innocent — some extraction
/// gate dropped every polygon. This dump prints the raw dat facts per polygon
/// and replicates MeshExtractor.PrepareGfxObjMeshData's gates (moved from
/// ObjectMeshManager in MP1a)
/// 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");
}
}