using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using AcDream.Content.Pak; namespace AcDream.Content.Tests; public class PakRoundTripTests : IDisposable { private readonly List _tempFiles = new(); private string NewTempPakPath() { var path = Path.Combine(Path.GetTempPath(), $"acdream-paktest-{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 ObjectMeshData MakeSyntheticData(uint fileId, int vertexCount) { var vertices = new VertexPositionNormalTexture[vertexCount]; for (int i = 0; i < vertexCount; i++) { vertices[i] = new VertexPositionNormalTexture( new Vector3(i, i * 2, i * 3), new Vector3(0, 0, 1), new Vector2(i * 0.1f, i * 0.2f)); } return new ObjectMeshData { ObjectId = fileId, IsSetup = false, Vertices = vertices, DIDDegrade = fileId + 1, }; } private static (PakAssetType Type, uint FileId, ObjectMeshData Data)[] MakeBlobSet(int n) { var result = new (PakAssetType, uint, ObjectMeshData)[n]; for (int i = 0; i < n; i++) { var type = (PakAssetType)((i % 3) + 1); uint fileId = 0x0100_0000u + (uint)i; result[i] = (type, fileId, MakeSyntheticData(fileId, i + 1)); } return result; } private static PakHeader WritePak(string path, (PakAssetType Type, uint FileId, ObjectMeshData Data)[] blobs) { var header = new PakHeader { FormatVersion = 1, PortalIteration = 10, CellIteration = 20, HighResIteration = 30, LanguageIteration = 40, BakeToolVersion = 1, }; using var writer = new PakWriter(path, header); foreach (var (type, fileId, data) in blobs) { writer.AddBlob(PakKey.Compose(type, fileId), data); } writer.Finish(); return header; } [Fact] public void WriteThenOpen_HeaderFieldsMatch() { var path = NewTempPakPath(); var blobs = MakeBlobSet(5); var written = WritePak(path, blobs); using var reader = new PakReader(path); Assert.Equal(written.FormatVersion, reader.Header.FormatVersion); Assert.Equal(written.PortalIteration, reader.Header.PortalIteration); Assert.Equal(written.CellIteration, reader.Header.CellIteration); Assert.Equal(written.HighResIteration, reader.Header.HighResIteration); Assert.Equal(written.LanguageIteration, reader.Header.LanguageIteration); Assert.Equal(written.BakeToolVersion, reader.Header.BakeToolVersion); Assert.Equal((uint)blobs.Length, reader.Header.TocCount); } [Fact] public void WriteThenOpen_EveryKeyFound() { var path = NewTempPakPath(); var blobs = MakeBlobSet(20); WritePak(path, blobs); using var reader = new PakReader(path); foreach (var (type, fileId, _) in blobs) { Assert.True(reader.ContainsKey(PakKey.Compose(type, fileId)), $"key for type={type} fileId=0x{fileId:X8} should be found"); } } [Fact] public void WriteThenOpen_BlobsDeserializeToDeepEqualObjects() { var path = NewTempPakPath(); var blobs = MakeBlobSet(8); WritePak(path, blobs); using var reader = new PakReader(path); foreach (var (type, fileId, expected) in blobs) { var key = PakKey.Compose(type, fileId); Assert.True(reader.TryReadObjectMeshData(key, out var actual), $"expected key 0x{key:X16} to read successfully"); ObjectMeshDataEquality.AssertEqual(expected, actual); } } [Fact] public void MissingKey_ReturnsFalse() { var path = NewTempPakPath(); var blobs = MakeBlobSet(3); WritePak(path, blobs); using var reader = new PakReader(path); var missingKey = PakKey.Compose(PakAssetType.GfxObjMesh, 0xFFFF_FFFEu); Assert.False(reader.ContainsKey(missingKey)); Assert.False(reader.TryReadObjectMeshData(missingKey, out var data)); Assert.Null(data); } [Fact] public void CorruptedBlob_CrcMismatch_TreatedAsMissing() { var path = NewTempPakPath(); var blobs = MakeBlobSet(4); WritePak(path, blobs); // Flip one byte inside the FIRST blob's region (right after the 64-byte header). using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) { fs.Position = PakHeader.Size + 4; // a few bytes into the first blob's payload int b = fs.ReadByte(); fs.Position = PakHeader.Size + 4; fs.WriteByte((byte)(b ^ 0xFF)); } using var reader = new PakReader(path); var key = PakKey.Compose(blobs[0].Type, blobs[0].FileId); Assert.False(reader.TryReadObjectMeshData(key, out var data), "a CRC-mismatched blob must be treated as missing"); Assert.Null(data); } [Fact] public void CorruptedBlob_DoesNotAffectOtherBlobs() { var path = NewTempPakPath(); var blobs = MakeBlobSet(4); WritePak(path, blobs); using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) { fs.Position = PakHeader.Size + 4; int b = fs.ReadByte(); fs.Position = PakHeader.Size + 4; fs.WriteByte((byte)(b ^ 0xFF)); } using var reader = new PakReader(path); // blobs[1..] should still read fine. for (int i = 1; i < blobs.Length; i++) { var key = PakKey.Compose(blobs[i].Type, blobs[i].FileId); Assert.True(reader.TryReadObjectMeshData(key, out var actual), $"blob {i} should be unaffected by corruption in blob 0"); ObjectMeshDataEquality.AssertEqual(blobs[i].Data, actual); } } [Fact] public void Blobs_Are64ByteAligned() { var path = NewTempPakPath(); var blobs = MakeBlobSet(6); WritePak(path, blobs); using var reader = new PakReader(path); foreach (var (type, fileId, _) in blobs) { var offset = reader.GetBlobOffsetForTest(PakKey.Compose(type, fileId)); Assert.True(offset % 64 == 0, $"blob offset {offset} for type={type} fileId=0x{fileId:X8} must be 64-byte aligned"); } } [Fact] public void TocBinarySearch_MatchesLinearScan() { var path = NewTempPakPath(); var blobs = MakeBlobSet(50); WritePak(path, blobs); using var reader = new PakReader(path); var allKeys = blobs.Select(b => PakKey.Compose(b.Type, b.FileId)).ToArray(); foreach (var key in allKeys) { bool binarySearchFound = reader.ContainsKey(key); bool linearScanFound = reader.DebugLinearScanContainsKey(key); Assert.Equal(linearScanFound, binarySearchFound); } // Also verify a handful of keys NOT present agree between both paths. var absentKeys = new[] { PakKey.Compose(PakAssetType.GfxObjMesh, 0xDEAD_0000u), PakKey.Compose(PakAssetType.SetupMesh, 0xDEAD_0001u), PakKey.Compose(PakAssetType.EnvCellMesh, 0xDEAD_0002u), }; foreach (var key in absentKeys) { Assert.Equal(reader.DebugLinearScanContainsKey(key), reader.ContainsKey(key)); Assert.False(reader.ContainsKey(key)); } } [Fact] public void EmptyPak_OpensCleanly_NoKeysFound() { var path = NewTempPakPath(); WritePak(path, Array.Empty<(PakAssetType, uint, ObjectMeshData)>()); using var reader = new PakReader(path); Assert.Equal(0u, reader.Header.TocCount); Assert.False(reader.ContainsKey(PakKey.Compose(PakAssetType.GfxObjMesh, 1))); } }