using System.Collections.Generic;
using System.IO;
using System.Linq;
using AcDream.Content.Pak;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
namespace AcDream.Content.Tests;
///
/// MP1b Task 6: dat-gated live-vs-pak equivalence suite. Runs MeshExtractor
/// LIVE and bakes the SAME ids to a temp pak, reads the pak back, and deep-
/// compares field-by-field via the Task 3 comparator. Because the bake tool
/// and the live client drive the identical MeshExtractor code (MP1a), this
/// test is proving the pak ROUND-TRIP (serialize/deserialize) preserves
/// what extraction actually produces on real content — not re-verifying the
/// extraction algorithm itself (that's the existing Conformance suite's job).
///
/// Skips cleanly when the real dats are absent (CI), matching
/// DatConcurrencyStressTests' convention.
///
public sealed class PakEquivalenceTests {
// Known-tricky GfxObj ids reused from existing conformance fixtures:
// 0x010002B4 / 0x010008A8 — #119 "[up-null] upload returned null" dump
// targets (Issue119UpNullGfxObjDumpTests).
// 0x010014C3 — the #113-saga Holtburg meeting-hall shell
// (Issue119UpNullGfxObjDumpTests.ShellModel_NoTexturedPolyIsDropped).
private static readonly uint[] KnownTrickyGfxObjIds = { 0x010002B4u, 0x010008A8u, 0x010014C3u };
// Setup ids reused from existing physics/conformance fixtures:
// 0x020019FF — the door setup (DoorBugTrajectoryReplayTests,
// DoorSetupGfxObjInspectionTests, DoorCollisionApparatusTests).
// 0x020005D8 / 0x020003F2 — Issue119TowerDumpTests fixtures.
private static readonly uint[] SetupIds = { 0x020019FFu, 0x020005D8u, 0x020003F2u };
private const uint HoltburgLandblock = 0xA9B40000u; // ConformanceDats.HoltburgLandblock
[Fact]
public void LiveExtraction_MatchesPakRoundTrip_OnFixtureIdSet() {
var datDir = ContentConformanceDats.ResolveDatDir();
if (datDir is null) return; // dats absent (CI) — skip, matching suite convention
using var dats = new DatCollection(datDir, DatAccessType.Read);
// The unified AcDream.Content.DatCollectionAdapter — same class the
// client and the bake tool use (MP1b review finding 7).
using var datReaderWriter = new DatCollectionAdapter(dats);
var logger = new TestConsoleLogger();
var sideStaged = new List();
var extractor = new MeshExtractor(datReaderWriter, logger, data => sideStaged.Add(data));
var gfxObjIds = BuildGfxObjIdSet(dats);
var setupIds = SetupIds.ToList();
var envCellIds = BuildEnvCellIdSet(dats, HoltburgLandblock, minCount: 5);
Assert.True(gfxObjIds.Count >= 10, $"fixture GfxObj id set unexpectedly small ({gfxObjIds.Count})");
Assert.True(setupIds.Count >= 3, $"fixture Setup id set unexpectedly small ({setupIds.Count})");
Assert.True(envCellIds.Count >= 5, $"fixture EnvCell id set unexpectedly small ({envCellIds.Count})");
var work = new List<(PakAssetType Type, uint FileId, ulong ExtractorId, bool IsSetup)>();
work.AddRange(gfxObjIds.Select(id => (PakAssetType.GfxObjMesh, id, (ulong)id, false)));
work.AddRange(setupIds.Select(id => (PakAssetType.SetupMesh, id, (ulong)id, true)));
// isSetup: false for EnvCell — matches the runtime's own request sites
// (WbMeshAdapter.IncrementRefCount/EnsureLoaded pass isSetup: false for
// every MeshRef id, incl. cell-geometry ids) and BakeRunner's call site.
// (Review finding 10 — the two call sites previously disagreed.)
work.AddRange(envCellIds.Select(id => (PakAssetType.EnvCellMesh, id, id | 0x1_0000_0000UL, false)));
// ---- LIVE extraction (golden) ----
var golden = new Dictionary<(PakAssetType, uint), ObjectMeshData>();
var extractionFailures = new List();
foreach (var (type, fileId, extractorId, isSetup) in work) {
var data = extractor.PrepareMeshData(extractorId, isSetup);
if (data is null) {
extractionFailures.Add($"{type} 0x{fileId:X8}: live extraction returned null");
continue;
}
golden[(type, fileId)] = data;
}
Assert.True(extractionFailures.Count == 0,
$"{extractionFailures.Count} fixture ids failed LIVE extraction (fixture assumption broke): " +
string.Join(" | ", extractionFailures));
// ---- bake the SAME ids to a temp pak ----
var pakPath = Path.Combine(Path.GetTempPath(), $"acdream-equivtest-{System.Guid.NewGuid():N}.pak");
try {
var header = new PakHeader {
FormatVersion = 1,
PortalIteration = (uint)dats.Portal.Iteration!.CurrentIteration,
CellIteration = (uint)dats.Cell.Iteration!.CurrentIteration,
HighResIteration = (uint)dats.HighRes.Iteration!.CurrentIteration,
LanguageIteration = (uint)dats.Local.Iteration!.CurrentIteration,
BakeToolVersion = 1,
};
using (var writer = new PakWriter(pakPath, header)) {
foreach (var ((type, fileId), data) in golden) {
writer.AddBlob(PakKey.Compose(type, fileId), data);
}
writer.Finish();
}
// ---- read back and deep-compare ----
using var reader = new PakReader(pakPath);
var mismatches = new List();
foreach (var ((type, fileId), expected) in golden) {
var key = PakKey.Compose(type, fileId);
if (!reader.TryReadObjectMeshData(key, out var actual)) {
mismatches.Add($"{type} 0x{fileId:X8}: pak read failed (missing or CRC mismatch)");
continue;
}
try {
ObjectMeshDataEquality.AssertEqual(expected, actual);
}
catch (Xunit.Sdk.XunitException ex) {
mismatches.Add($"{type} 0x{fileId:X8}: {ex.Message}");
}
}
Assert.True(mismatches.Count == 0,
$"{mismatches.Count}/{golden.Count} fixture ids mismatched between live extraction and pak round-trip:\n" +
string.Join("\n", mismatches));
}
finally {
if (File.Exists(pakPath)) File.Delete(pakPath);
}
}
///
/// Known-tricky ids (#119/#113 dump fixtures) plus enough additional
/// GfxObjs (deterministically, the first N ids in dat order after the
/// tricky set) to reach at least 10 total.
///
private static List BuildGfxObjIdSet(DatCollection dats) {
var ids = new List(KnownTrickyGfxObjIds);
var seen = new HashSet(ids);
foreach (var id in dats.GetAllIdsOfType()) {
if (ids.Count >= 10) break;
if (seen.Add(id)) ids.Add(id);
}
return ids;
}
/// Walks the Holtburg landblock's LandBlockInfo.NumCells range (mirrors StipplingSurfaceEquivalenceTests' enumeration) to get at least real EnvCell ids.
private static List BuildEnvCellIdSet(DatCollection dats, uint landblockId, int minCount) {
var ids = new List();
var lbInfo = dats.Get(landblockId | 0xFFFEu);
Assert.True(lbInfo is not null, $"LandBlockInfo for landblock 0x{landblockId:X8} not found — fixture assumption broke");
Assert.True(lbInfo!.NumCells >= minCount,
$"landblock 0x{landblockId:X8} has only {lbInfo.NumCells} cells — fixture assumption broke (need >= {minCount})");
uint firstCellId = landblockId | 0x0100u;
for (uint offset = 0; offset < lbInfo.NumCells && ids.Count < minCount; offset++) {
uint envCellId = firstCellId + offset;
if (dats.Get(envCellId) is not null) ids.Add(envCellId);
}
return ids;
}
}