using System; using System.Buffers.Binary; using System.IO; namespace AcDream.Content.Pak; /// /// Pak format constants shared by writer and reader. /// public static class PakFormat { /// /// The one format version this build writes and reads. PakWriter stamps /// it unconditionally (callers cannot override it via the header /// template); PakReader refuses to open any other version. Bump ONLY /// with an accompanying reader migration path. /// public const uint CurrentFormatVersion = 2; /// /// Identity of the bake algorithm that produced the payloads. Version 2 /// introduced content-identity EnvCell blobs with ordinary TOC aliases; /// version 3 embeds exact render-pass translucency in each texture batch /// so production never rebuilds surface metadata from live DAT. Version 4 /// adds complete immutable flat collision and EnvCell-topology payloads. /// Version 5 (#426, 2026-08-23) extracts untextured (solid-colour) /// positive faces that versions <=4 dropped — every GfxObj polygon /// whose Stippling carries NoPos (NO_POS_UVS) previously extracted to /// zero vertices on its positive side, so any pak baked by an older tool /// is missing those faces (e.g. the Holtburg windmill axle 0x010010CE, /// 8 polygons all NoPos + Base1Solid, extracted to a 0-vertex mesh). The /// binary format remains version 1. Version 6 introduces pak format 2: /// globally shared texture payloads plus independently Brotli-compressed /// blobs with raw fallback. Mesh geometry and source texture bytes remain /// exact; unedited DXT surfaces now retain their native BC blocks. Version /// 7 replaces the synthetic vertex-AABB GfxObj view sphere with retail's /// authored DrawingBSP root sphere. The binary format remains version 2, /// but every prepared GfxObj render record must be regenerated because /// the sphere participates in portal-view admission. Version 8 (OH2/S1, /// docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md) /// replaces CellStruct/EnvCell extraction with retail's exact /// surface-array-index subset construction: side candidates come only /// from sides_type (not NoPos/NoNeg), the subset/material owner is the /// source surface-array index rather than TextureKey/stippling/sides, /// and built-EnvCell admission is /// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after /// surface resolution. Version 9 appends authored surface opacity to /// every prepared mesh texture batch. The binary format remains version /// 2, but every prepared render record must be regenerated. /// public const uint CurrentBakeToolVersion = 9; } /// /// Fixed 64-byte pak file header. Layout (all integers little-endian): /// /// offset size field /// 0 4 magic 'ACPK' (0x4B504341) /// 4 4 formatVersion = 2 /// 8 4 portalIteration (DatCollection.Portal.Iteration) /// 12 4 cellIteration /// 16 4 highResIteration /// 20 4 languageIteration /// 24 8 tocOffset (u64) /// 32 4 tocCount (u32) /// 36 4 bakeToolVersion /// 40 24 reserved (zero) /// /// Format 1 is specified by docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md. /// Format 2 retains this header and adds the TOC compression flag plus global /// texture payload entries documented in docs/plans/2026-08-27-pak-v2-resource-campaign.md. /// public struct PakHeader { public const int Size = 64; public const uint MagicValue = 0x4B504341u; // 'ACPK' little-endian /// Always after ; not settable by callers building a header to write. public uint Magic { get; private set; } = MagicValue; public uint FormatVersion; public uint PortalIteration; public uint CellIteration; public uint HighResIteration; public uint LanguageIteration; public ulong TocOffset; public uint TocCount; public uint BakeToolVersion; public PakHeader() { } public void WriteTo(Span dest) { if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest)); BinaryPrimitives.WriteUInt32LittleEndian(dest[0..4], MagicValue); BinaryPrimitives.WriteUInt32LittleEndian(dest[4..8], FormatVersion); BinaryPrimitives.WriteUInt32LittleEndian(dest[8..12], PortalIteration); BinaryPrimitives.WriteUInt32LittleEndian(dest[12..16], CellIteration); BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], HighResIteration); BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], LanguageIteration); BinaryPrimitives.WriteUInt64LittleEndian(dest[24..32], TocOffset); BinaryPrimitives.WriteUInt32LittleEndian(dest[32..36], TocCount); BinaryPrimitives.WriteUInt32LittleEndian(dest[36..40], BakeToolVersion); dest[40..64].Clear(); // reserved, zero } public void WriteTo(Stream stream) { Span buf = stackalloc byte[Size]; WriteTo(buf); stream.Write(buf); } public static PakHeader ReadFrom(ReadOnlySpan src) { if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src)); var magic = BinaryPrimitives.ReadUInt32LittleEndian(src[0..4]); if (magic != MagicValue) { throw new InvalidDataException($"pak header magic mismatch: expected 0x{MagicValue:X8}, got 0x{magic:X8}"); } return new PakHeader { FormatVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[4..8]), PortalIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[8..12]), CellIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[12..16]), HighResIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]), LanguageIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]), TocOffset = BinaryPrimitives.ReadUInt64LittleEndian(src[24..32]), TocCount = BinaryPrimitives.ReadUInt32LittleEndian(src[32..36]), BakeToolVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[36..40]), }; } public static PakHeader ReadFrom(Stream stream) { Span buf = stackalloc byte[Size]; stream.ReadExactly(buf); return ReadFrom((ReadOnlySpan)buf); } } /// /// One 24-byte TOC entry: key u64, offset u64, lengthAndFlags u32, crc32 u32. /// Entries in a pak's TOC are sorted ascending by to allow /// binary-search lookup. is a corruption tripwire computed /// over the blob bytes; the reader verifies lazily on first access. /// public struct PakTocEntry { public const int Size = 24; public const uint CompressionFlag = 0x8000_0000u; public const uint StoredLengthMask = 0x7FFF_FFFFu; public ulong Key; public ulong Offset; public uint Length; public uint Crc32; /// /// Format-2 entries use the high bit of to mark an /// independently Brotli-compressed blob. The remaining 31 bits are the /// exact bytes stored in the file (including the compressed blob's /// four-byte decoded-length prefix). Raw entries retain their historical /// length representation and require no second copy on read. /// public readonly bool IsCompressed => (Length & CompressionFlag) != 0; public readonly uint StoredLength => Length & StoredLengthMask; public void WriteTo(Span dest) { if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest)); BinaryPrimitives.WriteUInt64LittleEndian(dest[0..8], Key); BinaryPrimitives.WriteUInt64LittleEndian(dest[8..16], Offset); BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], Length); BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], Crc32); } public void WriteTo(Stream stream) { Span buf = stackalloc byte[Size]; WriteTo(buf); stream.Write(buf); } public static PakTocEntry ReadFrom(ReadOnlySpan src) { if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src)); return new PakTocEntry { Key = BinaryPrimitives.ReadUInt64LittleEndian(src[0..8]), Offset = BinaryPrimitives.ReadUInt64LittleEndian(src[8..16]), Length = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]), Crc32 = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]), }; } }