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]),
};
}
}