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;
}
}