Files
rr3-server/RR3CommunityServer/Controllers/AssetManagementController.cs
Daniel Elliott 0929f963c6 Add RR3 Asset Extraction & Management System
Cross-Platform Scripts:
- extract_z_asset.sh: Linux/Unix single file extraction
- batch_extract_z_assets.sh: Linux/Unix batch extraction
- pack_z_asset.sh: Linux/Unix asset packing
- extract_z_asset.ps1: Windows PowerShell extraction

Server Integration:
- AssetExtractionService.cs: C# service for ZLIB extraction/packing
- AssetManagementController.cs: API endpoints for asset management
  - POST /api/AssetManagement/extract
  - POST /api/AssetManagement/pack
  - POST /api/AssetManagement/batch-extract
  - GET /api/AssetManagement/list
- Registered AssetExtractionService in Program.cs

Features:
- Extracts .z files (ZLIB compressed textures/data)
- Packs files to .z format with ZLIB compression
- Batch processing support
- Cross-platform (Windows/Linux/macOS)
- Server-side API for remote asset management
- Path traversal protection

Documentation:
- ASSET_EXTRACTION_GUIDE.md: Complete integration guide
- Tools/README.md: CLI tool documentation

Based on: Tankonline/Real-Racing-3-Texture-Extraction-Tool
Converted to cross-platform bash/PowerShell scripts + C# service

Ready for .pak asset extraction when files arrive from community

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-18 10:06:58 -08:00

190 lines
6.2 KiB
C#

using Microsoft.AspNetCore.Mvc;
using RR3CommunityServer.Services;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AssetManagementController : ControllerBase
{
private readonly AssetExtractionService _assetExtraction;
private readonly ILogger<AssetManagementController> _logger;
private readonly string _assetBasePath;
public AssetManagementController(
AssetExtractionService assetExtraction,
ILogger<AssetManagementController> logger,
IConfiguration configuration)
{
_assetExtraction = assetExtraction;
_logger = logger;
_assetBasePath = configuration["AssetBasePath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Assets");
}
/// <summary>
/// Extract a single .z asset file
/// </summary>
[HttpPost("extract")]
public async Task<IActionResult> ExtractAsset([FromBody] ExtractRequest request)
{
try
{
var filePath = Path.Combine(_assetBasePath, request.FileName);
var outputPath = await _assetExtraction.ExtractZFileAsync(filePath, request.OutputPath);
return Ok(new
{
resultCode = 0,
message = "Success",
data = new
{
inputFile = filePath,
outputFile = outputPath,
size = new FileInfo(outputPath).Length
}
});
}
catch (FileNotFoundException ex)
{
return NotFound(new { resultCode = 404, message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting asset");
return StatusCode(500, new { resultCode = 500, message = ex.Message });
}
}
/// <summary>
/// Pack a file to .z format
/// </summary>
[HttpPost("pack")]
public async Task<IActionResult> PackAsset([FromBody] PackRequest request)
{
try
{
var filePath = Path.Combine(_assetBasePath, request.FileName);
var outputPath = await _assetExtraction.PackZFileAsync(filePath, request.OutputPath);
return Ok(new
{
resultCode = 0,
message = "Success",
data = new
{
inputFile = filePath,
outputFile = outputPath,
size = new FileInfo(outputPath).Length
}
});
}
catch (FileNotFoundException ex)
{
return NotFound(new { resultCode = 404, message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error packing asset");
return StatusCode(500, new { resultCode = 500, message = ex.Message });
}
}
/// <summary>
/// Batch extract all .z files in a directory
/// </summary>
[HttpPost("batch-extract")]
public async Task<IActionResult> BatchExtract([FromBody] BatchExtractRequest request)
{
try
{
var inputDir = Path.Combine(_assetBasePath, request.InputDirectory);
var outputDir = string.IsNullOrEmpty(request.OutputDirectory)
? null
: Path.Combine(_assetBasePath, request.OutputDirectory);
var results = await _assetExtraction.BatchExtractAsync(inputDir, outputDir);
var successful = results.Count(r => !r.Value.StartsWith("ERROR:"));
var failed = results.Count - successful;
return Ok(new
{
resultCode = 0,
message = "Success",
data = new
{
totalFiles = results.Count,
successful,
failed,
results = results.Select(r => new
{
inputFile = Path.GetFileName(r.Key),
outputFile = Path.GetFileName(r.Value),
status = r.Value.StartsWith("ERROR:") ? "failed" : "success",
error = r.Value.StartsWith("ERROR:") ? r.Value : null
})
}
});
}
catch (DirectoryNotFoundException ex)
{
return NotFound(new { resultCode = 404, message = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in batch extraction");
return StatusCode(500, new { resultCode = 500, message = ex.Message });
}
}
/// <summary>
/// List all available .z assets
/// </summary>
[HttpGet("list")]
public IActionResult ListAssets([FromQuery] string? directory = null)
{
try
{
var searchDir = string.IsNullOrEmpty(directory)
? _assetBasePath
: Path.Combine(_assetBasePath, directory);
if (!Directory.Exists(searchDir))
{
return NotFound(new { resultCode = 404, message = "Directory not found" });
}
var zFiles = Directory.GetFiles(searchDir, "*.z", SearchOption.AllDirectories)
.Select(f => new
{
fileName = Path.GetFileName(f),
relativePath = Path.GetRelativePath(_assetBasePath, f),
size = new FileInfo(f).Length,
modified = new FileInfo(f).LastWriteTimeUtc
})
.ToList();
return Ok(new
{
resultCode = 0,
message = "Success",
data = new
{
directory = searchDir,
fileCount = zFiles.Count,
files = zFiles
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error listing assets");
return StatusCode(500, new { resultCode = 500, message = ex.Message });
}
}
}
public record ExtractRequest(string FileName, string? OutputPath = null);
public record PackRequest(string FileName, string? OutputPath = null);
public record BatchExtractRequest(string InputDirectory, string? OutputDirectory = null);