using System; using System.Collections.Generic; using System.IO; using AcDream.Bake; using AcDream.Content.Pak; namespace AcDream.Bake.Tests; /// /// Dat-gated byte-reproducibility gate for the bake pipeline (review finding /// 1): the plan's "bakes must be byte-reproducible run-to-run" is a whole- /// FILE property, so it must be tested through the REAL BakeRunner — the /// serializer-level determinism tests in Content.Tests can't see thread- /// completion-order effects in the blob region. Skips cleanly when the dats /// are absent (CI), same convention as /// tests/AcDream.Core.Tests/Conformance/DatConcurrencyStressTests. /// public sealed class BakeDeterminismTests : IDisposable { private readonly List _tempFiles = new(); private string NewTempPakPath() { var path = Path.Combine(Path.GetTempPath(), $"acdream-baketest-{Guid.NewGuid():N}.pak"); _tempFiles.Add(path); return path; } public void Dispose() { foreach (var f in _tempFiles) { try { if (File.Exists(f)) File.Delete(f); } catch { /* best effort cleanup */ } } } private static string? ResolveDatDir() { // Mirrors ConformanceDats.ResolveDatDir (env var, then the well-known // Documents fallback); duplicated because test projects can't // reference each other's helpers. var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv; var def = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(def) ? def : null; } [Fact] public void Bake_SameIdSet_DifferentThreadCounts_ByteIdenticalPaks() { var datDir = ResolveDatDir(); if (datDir is null) return; // dats absent (CI) — skip, matching suite convention // Small mixed fixture: GfxObjs (incl. the #119 tricky ids), Setups // (door setup carries emitters -> exercises the side-staged preload // path), and a Holtburg EnvCell. Big enough to span multiple asset // types; small enough to run in seconds. var ids = new HashSet { 0x01000001u, 0x010002B4u, 0x010008A8u, 0x010014C3u, 0x02000001u, 0x020019FFu, 0x020005D8u, 0xA9B40100u, 0xA9B40101u, }; var pathA = NewTempPakPath(); var pathB = NewTempPakPath(); // DIFFERENT thread counts on purpose: thread-completion order was the // nondeterminism source the sorted-batching fix removes — identical // bytes must hold regardless of parallelism. int rcA = BakeRunner.Run(new BakeOptions { DatDir = datDir, OutPath = pathA, IdFilter = ids, Threads = 8 }); int rcB = BakeRunner.Run(new BakeOptions { DatDir = datDir, OutPath = pathB, IdFilter = ids, Threads = 3 }); Assert.Equal(0, rcA); Assert.Equal(0, rcB); var bytesA = File.ReadAllBytes(pathA); var bytesB = File.ReadAllBytes(pathB); Assert.True(bytesA.Length == bytesB.Length, $"pak sizes differ between runs: {bytesA.Length} vs {bytesB.Length} bytes"); Assert.True(bytesA.AsSpan().SequenceEqual(bytesB), "two bakes of the same id set produced different bytes — the sorted-batching determinism guarantee is broken"); } [Fact] public void Bake_RealDuplicateCells_ShareOneBlobAcrossLandblocksAndThreads() { var datDir = ResolveDatDir(); if (datDir is null) return; // Full-bake discovery fixture: these eight cells span multiple // landblocks but have the same complete environment/structure/surface // tuple. They must remain eight addressable keys backed by one blob. var ids = new HashSet { 0x00A90230u, 0x00B901A8u, 0x00E50602u, 0x00E50610u, 0x00E5061Fu, 0x00F90784u, 0x00F9078Eu, 0x00FA073Eu, }; var pathA = NewTempPakPath(); var pathB = NewTempPakPath(); var reportA = BakeRunner.RunDetailed( new BakeOptions { DatDir = datDir, OutPath = pathA, IdFilter = ids, Threads = 1, }); var reportB = BakeRunner.RunDetailed( new BakeOptions { DatDir = datDir, OutPath = pathB, IdFilter = ids, Threads = 8, }); Assert.Equal(8, reportA.EnvCellKeys); Assert.Equal(1, reportA.UniqueEnvCellGeometries); Assert.Equal(7, reportA.EnvCellAliases); Assert.Equal(1, reportA.PhysicalBlobs); Assert.Equal(reportA.TotalKeys, reportB.TotalKeys); Assert.True(File.ReadAllBytes(pathA).AsSpan().SequenceEqual(File.ReadAllBytes(pathB))); using var reader = new PakReader(pathA); var receipts = ids .Select(id => reader.GetTocEntryForTest( PakKey.Compose(PakAssetType.EnvCellMesh, id))) .ToArray(); Assert.All(receipts, receipt => Assert.Equal(receipts[0].Offset, receipt.Offset)); Assert.All(receipts, receipt => Assert.Equal(receipts[0].Length, receipt.Length)); Assert.All(receipts, receipt => Assert.Equal(receipts[0].Crc32, receipt.Crc32)); Assert.True( reader.TryReadObjectMeshData( PakKey.Compose(PakAssetType.EnvCellMesh, ids.Min()), out var geometry)); Assert.NotNull(geometry); Assert.Equal(0xE000_0000_0000_0000UL, geometry.ObjectId & 0xF000_0000_0000_0000UL); } }