using System.IO.Compression; namespace RR3CommunityServer.Services; /// /// Service for extracting and packing RR3 .z (ZLIB compressed) asset files /// public class AssetExtractionService { private readonly ILogger _logger; public AssetExtractionService(ILogger logger) { _logger = logger; } /// /// Extracts a .z file (ZLIB compressed) to its original format /// public async Task ExtractZFileAsync(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException($"File not found: {filePath}"); } _logger.LogInformation($"Extracting .z file: {filePath}"); var data = await File.ReadAllBytesAsync(filePath); var output = new List(); int i = 0; int blocksFound = 0; // Scan for ZLIB magic bytes (0x78 followed by 0x9C, 0xDA, or 0x01) while (i < data.Length - 2) { if (data[i] == 0x78 && (data[i + 1] == 0x9C || data[i + 1] == 0xDA || data[i + 1] == 0x01)) { try { // Try to decompress from this position using var compressed = new MemoryStream(data, i + 2, data.Length - i - 2); using var deflate = new DeflateStream(compressed, CompressionMode.Decompress); using var decompressed = new MemoryStream(); await deflate.CopyToAsync(decompressed); var block = decompressed.ToArray(); if (block.Length > 0) { output.AddRange(block); _logger.LogDebug($"Block {blocksFound} found at offset 0x{i:X}"); blocksFound++; i += block.Length; continue; } } catch { // Not a valid ZLIB block, continue scanning } } i++; } if (blocksFound == 0) { throw new InvalidDataException("No valid ZLIB blocks found in file"); } _logger.LogInformation($"Extracted {blocksFound} blocks, total size: {output.Count:N0} bytes"); return output.ToArray(); } /// /// Extracts a .z file and saves the result /// public async Task ExtractZFileAsync(string inputPath, string? outputPath = null) { var extracted = await ExtractZFileAsync(inputPath); // Default output path: remove .z extension if (string.IsNullOrEmpty(outputPath)) { outputPath = inputPath.EndsWith(".z", StringComparison.OrdinalIgnoreCase) ? inputPath[..^2] : $"{inputPath}.extracted"; } // Backup existing file if (File.Exists(outputPath)) { var backupPath = $"{outputPath}.bak"; if (!File.Exists(backupPath)) { File.Move(outputPath, backupPath); _logger.LogInformation($"Created backup: {backupPath}"); } } await File.WriteAllBytesAsync(outputPath, extracted); _logger.LogInformation($"Saved extracted file: {outputPath}"); return outputPath; } /// /// Packs a file with ZLIB compression to create .z format /// public async Task PackZFileAsync(byte[] data) { using var output = new MemoryStream(); using (var deflate = new DeflateStream(output, CompressionLevel.Optimal)) { // Write ZLIB header (0x78 0x9C for default compression) output.WriteByte(0x78); output.WriteByte(0x9C); await deflate.WriteAsync(data, 0, data.Length); } var compressed = output.ToArray(); var ratio = (1.0 - (double)compressed.Length / data.Length) * 100; _logger.LogInformation($"Packed {data.Length:N0} bytes → {compressed.Length:N0} bytes (compression: {ratio:F1}%)"); return compressed; } /// /// Packs a file with ZLIB compression /// public async Task PackZFileAsync(string inputPath, string? outputPath = null) { if (!File.Exists(inputPath)) { throw new FileNotFoundException($"File not found: {inputPath}"); } _logger.LogInformation($"Packing file: {inputPath}"); var data = await File.ReadAllBytesAsync(inputPath); var packed = await PackZFileAsync(data); // Default output: add .z extension outputPath ??= $"{inputPath}.z"; await File.WriteAllBytesAsync(outputPath, packed); _logger.LogInformation($"Saved packed file: {outputPath}"); return outputPath; } /// /// Batch extract multiple .z files from a directory /// public async Task> BatchExtractAsync(string inputDirectory, string? outputDirectory = null) { if (!Directory.Exists(inputDirectory)) { throw new DirectoryNotFoundException($"Directory not found: {inputDirectory}"); } outputDirectory ??= Path.Combine(inputDirectory, "extracted"); Directory.CreateDirectory(outputDirectory); var zFiles = Directory.GetFiles(inputDirectory, "*.z", SearchOption.AllDirectories); _logger.LogInformation($"Found {zFiles.Length} .z files in {inputDirectory}"); var results = new Dictionary(); int success = 0; int failed = 0; foreach (var zFile in zFiles) { try { var fileName = Path.GetFileNameWithoutExtension(zFile); var outputPath = Path.Combine(outputDirectory, fileName); await ExtractZFileAsync(zFile, outputPath); results[zFile] = outputPath; success++; _logger.LogInformation($"[{success + failed}/{zFiles.Length}] ✅ Extracted: {Path.GetFileName(zFile)}"); } catch (Exception ex) { failed++; _logger.LogError(ex, $"[{success + failed}/{zFiles.Length}] ❌ Failed: {Path.GetFileName(zFile)}"); results[zFile] = $"ERROR: {ex.Message}"; } } _logger.LogInformation($"Batch extraction complete: {success} success, {failed} failed"); return results; } }