using System; using System.Collections.Generic; using System.IO; using SabreTools.Numerics.Extensions; // TODO: Remove when IO is updated namespace SabreTools.Wrappers { /// /// Encodes disc data into RVZ-Pack format by replacing predictable LFG /// (Lagged Fibonacci Generator) junk regions with compact seed descriptors. /// /// This is the exact inverse of and mirrors /// Dolphin's RVZPack() in WIABlob.cpp. /// /// Two-phase algorithm: /// /// Phase 1 (): walk the buffer, identify LFG /// junk regions, build a map keyed by end-offset. /// Phase 2 (): for each chunk, use the map to /// emit alternating real-data and junk-seed segments. /// /// internal static class RvzPackEncoder { /// /// 17 u32s × 4 bytes = 68 bytes — minimum size to record a seed /// private const int SeedSizeBytes = LaggedFibonacciGenerator.SEED_SIZE * 4; /// /// /// private sealed class JunkRegion { public long StartOffset; public uint[]? Seed; } /// /// Result of packing a single chunk: compressed payload and its logical size. /// internal struct ChunkResult { /// /// Packed payload, or null if the chunk contains no junk. /// public byte[]? Packed; /// /// Number of bytes the decompressor needs to consume from . /// public uint RvzPackedSize; } /// /// RVZ-pack a single chunk. /// Returns null if the chunk contains no junk (write raw instead). /// is the number of bytes actually needed /// by the decompressor (may be < packed.Length due to alignment). /// /// Source buffer /// Start of data within public static byte[]? Pack(byte[] data, int dataOffset, int size, long discDataOffset, out uint rvzPackedSize, FileSystemTableReader? fst = null) { rvzPackedSize = 0; if (size <= 0) return null; var junkInfo = ScanForJunk(data, dataOffset, size, discDataOffset, fst); if (junkInfo.Count == 0) return null; ChunkResult result = EmitChunk(data, dataOffset, 0L, size, junkInfo); rvzPackedSize = result.RvzPackedSize; return result.Packed; } /// /// RVZ-pack a multi-chunk buffer (e.g. a full 2 MiB Wii group). /// Performs one Phase-1 scan over the entire buffer, then calls /// per chunk. /// /// Source buffer /// Start of data within /// Total number of bytes to process /// Size of each individual chunk /// Number of chunks /// Disc-partition byte offset of the first byte /// Optional FST for file-boundary optimisation /// /// One per chunk; /// Packed == null means the chunk has no junk and should be written raw. /// public static ChunkResult[] PackGroup(byte[] data, int dataOffset, int totalSize, int bytesPerChunk, int numChunks, long discDataOffset, FileSystemTableReader? fst = null) { var junkInfo = ScanForJunk(data, dataOffset, totalSize, discDataOffset, fst); var result = new ChunkResult[numChunks]; for (int i = 0; i < numChunks; i++) { long chunkStart = (long)i * bytesPerChunk; long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize); result[i] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, junkInfo); } return result; } /// /// Scan buffer for junk regions /// /// Source buffer /// Start of data within /// Total number of bytes to process /// Disc-partition byte offset of the first byte /// Optional FST for file-boundary optimisation /// private static SortedDictionary ScanForJunk( byte[] data, int dataOffset, int totalSize, long discDataOffset, FileSystemTableReader? fst) { var junkInfo = new SortedDictionary(); long position = 0; long dataOff = discDataOffset; while (position < totalSize) { // Step 1: count and advance past leading zeros long zeroes = 0; while ((position + zeroes) < totalSize && data[dataOffset + position + zeroes] == 0) { zeroes++; } if (zeroes > SeedSizeBytes) { junkInfo[position + zeroes] = new JunkRegion { StartOffset = position, Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE] }; } position += zeroes; dataOff += zeroes; if (position >= totalSize) break; // Step 2: compute aligned read window (next 0x8000 boundary) long nextBoundary = AlignUp(dataOff + 1, 0x8000); long bytesToRead = Math.Min(nextBoundary - dataOff, totalSize - position); int dataOffMod = (int)(dataOff % 0x8000); // Step 3: ALWAYS call GetSeed unconditionally — no FST pre-check var seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]; int reconstructed = LaggedFibonacciGenerator.GetSeed(data, (int)(dataOffset + position), (int)bytesToRead, dataOffMod, seed); if (reconstructed > 0) { junkInfo[position + reconstructed] = new JunkRegion { StartOffset = position, Seed = seed }; } // Step 4: FST skip AFTER GetSeed if (fst is not null) { long queryOff = dataOff + reconstructed; FileSystemTableReader.FileEntry? fileInfo = fst.FindFileInfo(queryOff); if (fileInfo is not null) { long fileEnd = fileInfo.Value.FileEnd; if (fileEnd < (dataOff + bytesToRead)) { position += fileEnd - dataOff; dataOff = fileEnd; continue; } } } // Step 5: normal advance by block window position += bytesToRead; dataOff += bytesToRead; } return junkInfo; } /// /// Emit packed segments for a single chunk /// /// Source buffer /// Start of data within /// /// /// /// private static ChunkResult EmitChunk( byte[] data, int dataOffset, long chunkStart, long chunkEnd, SortedDictionary junkInfo) { long currentOffset = chunkStart; bool firstIteration = true; var output = new MemoryStream((int)(chunkEnd - chunkStart)); uint packedSize = 0; while (currentOffset < chunkEnd) { long remaining = chunkEnd - currentOffset; long nextJunkStart = chunkEnd; long nextJunkEnd = chunkEnd; uint[]? junkSeed = null; if (remaining > SeedSizeBytes) { foreach (var kvp in junkInfo) { // Dolphin Phase-2 condition: // key > currentOffset + SEED_SIZE_BYTES AND // startOffset + SEED_SIZE_BYTES < chunkEnd if ((kvp.Key > (currentOffset + SeedSizeBytes)) && ((kvp.Value.StartOffset + SeedSizeBytes) < chunkEnd)) { nextJunkStart = Math.Max(currentOffset, kvp.Value.StartOffset); nextJunkEnd = Math.Min(chunkEnd, kvp.Key); junkSeed = kvp.Value.Seed; break; } } } // On the first iteration, bail out if there is no junk in this chunk if (firstIteration) { if (nextJunkStart == chunkEnd) return new ChunkResult { Packed = null, RvzPackedSize = 0 }; firstIteration = false; } // Emit real-data segment before the junk region long nonJunkBytes = nextJunkStart - currentOffset; if (nonJunkBytes > 0) { output.WriteBigEndian((uint)nonJunkBytes); output.Write(data, (int)(dataOffset + currentOffset), (int)nonJunkBytes); packedSize += 4 + (uint)nonJunkBytes; currentOffset += nonJunkBytes; } // Emit junk-seed segment long junkBytes = nextJunkEnd - currentOffset; if (junkBytes > 0 && junkSeed is not null) { output.WriteBigEndian(0x80000000u | (uint)junkBytes); byte[] seedBytes = new byte[SeedSizeBytes]; Buffer.BlockCopy(junkSeed, 0, seedBytes, 0, SeedSizeBytes); output.Write(seedBytes, 0, SeedSizeBytes); packedSize += 4 + (uint)SeedSizeBytes; currentOffset += junkBytes; } if (junkSeed == null) break; } return new ChunkResult { Packed = output.ToArray(), RvzPackedSize = packedSize }; } #region Helpers /// /// Align a value to a boundary /// /// TODO: Figure out how to use buffer alignment helpers here private static long AlignUp(long value, long alignment) => (value + alignment - 1) & ~(alignment - 1); #endregion } }