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); using var datReaderWriter = new ContentTestDatCollectionAdapter(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))); work.AddRange(envCellIds.Select(id => (PakAssetType.EnvCellMesh, id, id | 0x1_0000_0000UL, true))); // ---- 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; } }