PakEquivalenceTests runs MeshExtractor LIVE and bakes the SAME fixture ids to a temp pak, reads it back via PakReader, and deep-compares every field via the Task 3 ObjectMeshDataEquality comparator. Since the bake tool and the live client drive the identical MeshExtractor code (MP1a), this proves the pak ROUND-TRIP preserves what extraction actually produces on real content — the serializer's job, not a re-verification of the extraction algorithm itself (the existing Conformance suite owns that). Fixture set (>= the plan's minimums): 10 GfxObjs (3 known-tricky ids reused from Issue119UpNullGfxObjDumpTests - 0x010002B4, 0x010008A8, 0x010014C3 - plus 7 more from dat order), 3 Setups reused from door/ tower conformance fixtures (0x020019FF, 0x020005D8, 0x020003F2), 5 EnvCells walked from the Holtburg landblock 0xA9B40000's LandBlockInfo.NumCells range (same idiom as StipplingSurfaceEquivalenceTests). Skips cleanly when dats are absent via ContentConformanceDats. ResolveDatDir(), a deliberate small duplicate of ConformanceDats.ResolveDatDir()'s exact pattern (env var then Documents/Asheron's Call fallback) since Content.Tests cannot reference the AcDream.Core.Tests project. ContentTestDatCollectionAdapter is a third small copy of the IDatReaderWriter dat-access glue (alongside AcDream.App's internal original and AcDream.Bake's copy) for the same layering reason (structure rule 6: tests live in the project matching the layer under test). Ran for real against the dats on this machine (not just CI-skip path): 1 test, all fixture ids extracted live with zero failures, baked, read back, and field-for-field identical. Full solution dotnet test: 4106 tests total (43 Content.Tests + 385 Core.Net.Tests + 425 UI.Abstractions.Tests + 722 App.Tests incl. 2 pre-existing skips + 2531 Core.Tests incl. 2 pre-existing skips) — 0 failures.
155 lines
7.5 KiB
C#
155 lines
7.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<ObjectMeshData>();
|
|
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<string>();
|
|
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<string>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private static List<uint> BuildGfxObjIdSet(DatCollection dats) {
|
|
var ids = new List<uint>(KnownTrickyGfxObjIds);
|
|
var seen = new HashSet<uint>(ids);
|
|
|
|
foreach (var id in dats.GetAllIdsOfType<GfxObj>()) {
|
|
if (ids.Count >= 10) break;
|
|
if (seen.Add(id)) ids.Add(id);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
/// <summary>Walks the Holtburg landblock's LandBlockInfo.NumCells range (mirrors StipplingSurfaceEquivalenceTests' enumeration) to get at least <paramref name="minCount"/> real EnvCell ids.</summary>
|
|
private static List<uint> BuildEnvCellIdSet(DatCollection dats, uint landblockId, int minCount) {
|
|
var ids = new List<uint>();
|
|
var lbInfo = dats.Get<LandBlockInfo>(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<EnvCell>(envCellId) is not null) ids.Add(envCellId);
|
|
}
|
|
return ids;
|
|
}
|
|
}
|