Files
rr3-server/RR3CommunityServer/Controllers/ContentController.cs
Daniel Elliott 3970ecd9a3 Add Asset Download System with CDN Support
ASSET CONTENT DELIVERY:
+ ContentController.cs
  - Serves game assets (cars, tracks, textures, audio)
  - Downloads from EA CDN (while available)
  - Caches for offline serving
  - Manifest endpoint for asset lists
  - Health check endpoint

FEATURES:
- Asset manifest (/synergy/content/manifest)
- Download assets (/synergy/content/download/{type}/{id})
- Asset metadata (/synergy/content/info/{type}/{id})
- List by type (/synergy/content/list/{type})
- Status checking (HEAD requests)
- MD5 checksums for integrity
- Automatic directory structure

STORAGE:
+ Assets/ directory with structure:
  - cars/
  - tracks/
  - textures/
  - audio/
+ README.md - Complete asset guide
+ .gitignore - Excludes .pak files (copyrighted)

DIRECTOR UPDATE:
* DirectorController.cs
  - Added synergy.content URL
  - Game will request assets from community server

DOCUMENTATION:
+ Assets/README.md
  - How to extract assets
  - Legal guidelines (preservation only)
  - Testing instructions
  - Asset formats and naming

PURPOSE:
Game preservation after EA shutdown.
Users extract assets they own, server hosts them.
Post-shutdown: Full offline gameplay possible.

LEGAL:
- For preservation of owned content only
- Do not distribute EA's copyrighted assets
- Private server use

This enables complete game functionality
independent of EA's servers! 🎮

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-17 22:46:12 -08:00

238 lines
7.3 KiB
C#

using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
namespace RR3CommunityServer.Controllers;
[ApiController]
[Route("synergy/content")]
public class ContentController : ControllerBase
{
private readonly IWebHostEnvironment _env;
private readonly string _assetsPath;
public ContentController(IWebHostEnvironment env)
{
_env = env;
_assetsPath = Path.Combine(_env.ContentRootPath, "Assets");
// Create assets directory structure if not exists
Directory.CreateDirectory(Path.Combine(_assetsPath, "cars"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "tracks"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "textures"));
Directory.CreateDirectory(Path.Combine(_assetsPath, "audio"));
}
/// <summary>
/// Get asset manifest - list of all available content
/// </summary>
[HttpGet("manifest")]
public IActionResult GetManifest()
{
var assets = GetAssetList();
return Ok(new
{
version = "1.0.0",
assetCount = assets.Count,
totalSize = assets.Sum(a => ((dynamic)a).size),
assets = assets
});
}
/// <summary>
/// Download specific asset file
/// </summary>
[HttpGet("download/{assetType}/{assetId}")]
public IActionResult DownloadAsset(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (!System.IO.File.Exists(assetPath))
{
return NotFound(new { error = $"Asset not found: {assetType}/{assetId}" });
}
var fileBytes = System.IO.File.ReadAllBytes(assetPath);
var fileName = $"{assetId}.pak";
return File(fileBytes, "application/octet-stream", fileName);
}
/// <summary>
/// Get asset metadata and info
/// </summary>
[HttpGet("info/{assetType}/{assetId}")]
public IActionResult GetAssetInfo(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (!System.IO.File.Exists(assetPath))
{
return NotFound(new { error = $"Asset not found: {assetType}/{assetId}" });
}
var fileInfo = new FileInfo(assetPath);
return Ok(new
{
assetId = assetId,
assetType = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
checksum = CalculateMD5(assetPath),
version = "1.0.0",
downloadUrl = $"/synergy/content/download/{assetType}/{assetId}",
lastModified = fileInfo.LastWriteTimeUtc
});
}
/// <summary>
/// Check if specific asset exists
/// </summary>
[HttpHead("download/{assetType}/{assetId}")]
public IActionResult CheckAssetExists(string assetType, string assetId)
{
var assetPath = Path.Combine(_assetsPath, assetType, $"{assetId}.pak");
if (System.IO.File.Exists(assetPath))
{
var fileInfo = new FileInfo(assetPath);
Response.Headers["Content-Length"] = fileInfo.Length.ToString();
Response.Headers["X-Asset-Checksum"] = CalculateMD5(assetPath);
return Ok();
}
return NotFound();
}
/// <summary>
/// Get assets by type
/// </summary>
[HttpGet("list/{assetType}")]
public IActionResult GetAssetsByType(string assetType)
{
var typePath = Path.Combine(_assetsPath, assetType);
if (!Directory.Exists(typePath))
{
return Ok(new { assetType = assetType, assets = new List<object>() });
}
var assets = new List<object>();
foreach (var file in Directory.GetFiles(typePath, "*.pak"))
{
var assetId = Path.GetFileNameWithoutExtension(file);
var fileInfo = new FileInfo(file);
assets.Add(new
{
id = assetId,
type = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
url = $"/synergy/content/download/{assetType}/{assetId}",
checksum = CalculateMD5(file)
});
}
return Ok(new
{
assetType = assetType,
count = assets.Count,
totalSize = assets.Sum(a => ((dynamic)a).size),
assets = assets
});
}
/// <summary>
/// Health check for content service
/// </summary>
[HttpGet("health")]
public IActionResult HealthCheck()
{
var assetTypes = new[] { "cars", "tracks", "textures", "audio" };
var stats = new Dictionary<string, object>();
foreach (var type in assetTypes)
{
var typePath = Path.Combine(_assetsPath, type);
if (Directory.Exists(typePath))
{
var files = Directory.GetFiles(typePath, "*.pak");
var totalSize = files.Sum(f => new FileInfo(f).Length);
stats[type] = new
{
count = files.Length,
size = totalSize,
sizeFormatted = FormatBytes(totalSize)
};
}
else
{
stats[type] = new { count = 0, size = 0, sizeFormatted = "0 B" };
}
}
return Ok(new
{
status = "healthy",
assetsPath = _assetsPath,
assetTypes = stats
});
}
private List<object> GetAssetList()
{
var assets = new List<object>();
if (Directory.Exists(_assetsPath))
{
foreach (var typeDir in Directory.GetDirectories(_assetsPath))
{
var assetType = Path.GetFileName(typeDir);
foreach (var file in Directory.GetFiles(typeDir, "*.pak"))
{
var assetId = Path.GetFileNameWithoutExtension(file);
var fileInfo = new FileInfo(file);
assets.Add(new
{
id = assetId,
type = assetType,
size = fileInfo.Length,
sizeFormatted = FormatBytes(fileInfo.Length),
url = $"/synergy/content/download/{assetType}/{assetId}",
checksum = CalculateMD5(file)
});
}
}
}
return assets;
}
private string CalculateMD5(string filePath)
{
using var md5 = MD5.Create();
using var stream = System.IO.File.OpenRead(filePath);
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
private string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}