using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace AcDream.Content.Pak; /// /// Streams pak blobs to disk: header placeholder -> 64-byte-aligned blobs -> /// TOC (sorted by key) -> seek back and finalize the header once /// tocOffset/tocCount are known. This lets the writer stream blobs without /// knowing the final count up front (plan: "TOC last"). /// public sealed class PakWriter : IDisposable { private const int Alignment = 64; private readonly FileStream _stream; private readonly PakHeader _headerTemplate; private readonly List _tocEntries = new(); private readonly HashSet _seenKeys = new(); private readonly Dictionary _receiptByKey = new(); private bool _finished; public PakWriter(string path, PakHeader headerTemplate) { _stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None); _headerTemplate = headerTemplate; // The format version is the WRITER's identity, not caller input — // stamp it unconditionally so a caller-populated header template can // never produce a pak claiming a version this code doesn't write // (the default-0 footgun; PakReader rejects any version but // PakFormat.CurrentFormatVersion). _headerTemplate.FormatVersion = PakFormat.CurrentFormatVersion; _headerTemplate.BakeToolVersion = PakFormat.CurrentBakeToolVersion; // Write a header placeholder now; finalized in Finish() once TocOffset/TocCount are known. _headerTemplate.WriteTo(_stream); PadToAlignment(); } /// /// Adds one blob (an serialized via /// ) under . /// Blobs are written immediately at the current (64-byte-aligned) stream /// position; the writer pads after each blob so the NEXT blob also /// starts aligned. /// public void AddBlob(ulong key, ObjectMeshData data) { ThrowIfFinished(); ReserveKey(key); using var ms = new MemoryStream(); ObjectMeshDataSerializer.Write(data, ms); var bytes = ms.ToArray(); AddReservedBlob(key, bytes); } /// /// Adds an already serialized immutable payload. This is the package seam /// used by typed non-render assets such as flat collision records. /// public void AddBlob(ulong key, ReadOnlySpan bytes) { ThrowIfFinished(); ReserveKey(key); AddReservedBlob(key, bytes); } private void AddReservedBlob(ulong key, ReadOnlySpan bytes) { if ((ulong)bytes.Length > uint.MaxValue) { throw new ArgumentException( "one pak payload cannot exceed UInt32.MaxValue bytes", nameof(bytes)); } long offset = _stream.Position; _stream.Write(bytes); PadToAlignment(); var receipt = new PakTocEntry { Key = key, Offset = (ulong)offset, Length = checked((uint)bytes.Length), Crc32 = Content.Pak.Crc32.Compute(bytes), }; _tocEntries.Add(receipt); _receiptByKey.Add(key, receipt); } private void ReserveKey(ulong key) { if (!_seenKeys.Add(key)) { throw new ArgumentException($"duplicate pak key 0x{key:X16}", nameof(key)); } } /// /// Adds a second independently addressable key for an existing physical /// blob. The alias receives a normal TOC row with the source blob's exact /// offset, length, and CRC; no payload bytes are written again. /// public void AddAlias(ulong aliasKey, ulong existingKey) { ThrowIfFinished(); if (aliasKey == existingKey) { throw new ArgumentException("an alias key must differ from its source key", nameof(aliasKey)); } if (!_receiptByKey.TryGetValue(existingKey, out var source)) { throw new KeyNotFoundException($"source pak key 0x{existingKey:X16} has not been written"); } if (!_seenKeys.Add(aliasKey)) { throw new ArgumentException($"duplicate pak key 0x{aliasKey:X16}", nameof(aliasKey)); } var alias = source; alias.Key = aliasKey; _tocEntries.Add(alias); _receiptByKey.Add(aliasKey, alias); } /// Writes the sorted TOC and finalizes the header. Must be called exactly once. public void Finish() { ThrowIfFinished(); _finished = true; var sorted = _tocEntries.OrderBy(e => e.Key).ToList(); ulong tocOffset = (ulong)_stream.Position; foreach (var entry in sorted) { entry.WriteTo(_stream); } var finalHeader = _headerTemplate; finalHeader.TocOffset = tocOffset; finalHeader.TocCount = (uint)sorted.Count; _stream.Position = 0; finalHeader.WriteTo(_stream); _stream.Flush(); } private void PadToAlignment() { long pos = _stream.Position; long remainder = pos % Alignment; if (remainder == 0) return; long pad = Alignment - remainder; Span zeros = stackalloc byte[(int)pad]; zeros.Clear(); _stream.Write(zeros); } private void ThrowIfFinished() { if (_finished) throw new InvalidOperationException("PakWriter.Finish() has already been called."); } public void Dispose() { // Best-effort finalize so a forgotten Finish() call doesn't leave a // half-written file with a placeholder header claiming 0 entries // while blobs are on disk. try/finally, NOT a bare sequence: Finish() // performs real I/O (TOC write, header seek-back, flush) and can // throw for environmental reasons (disk full, I/O error) — the // stream must ALWAYS be closed so the file handle is never leaked // mid-unwind (review finding 5 on the a5926ebc cleanup, which had // removed exactly this guarantee). try { if (!_finished) { Finish(); } } finally { _stream.Dispose(); } } }