using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RR3CommunityServer.Data; using RR3CommunityServer.Models; using System.Security.Cryptography; using static RR3CommunityServer.Data.RR3DbContext; namespace RR3CommunityServer.Controllers; [ApiController] [Route("modding/api")] public class ModdingController : ControllerBase { private readonly RR3DbContext _context; private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly string _customAssetsPath; private readonly string _modsPath; public ModdingController(RR3DbContext context, ILogger logger, IConfiguration configuration) { _context = context; _logger = logger; _configuration = configuration; _customAssetsPath = configuration.GetValue("CustomAssetsPath") ?? Path.Combine(Directory.GetCurrentDirectory(), "Assets", "custom"); _modsPath = configuration.GetValue("ModsPath") ?? Path.Combine(Directory.GetCurrentDirectory(), "Assets", "mods"); // Ensure directories exist Directory.CreateDirectory(_customAssetsPath); Directory.CreateDirectory(_modsPath); } /// /// Upload a custom car /// [HttpPost("cars/upload")] [RequestSizeLimit(100_000_000)] // 100 MB max public async Task UploadCustomCar([FromForm] CustomCarUpload upload) { _logger.LogInformation("Custom car upload: {Name} by {Author}", upload.CarName, upload.AuthorName); try { // Validate files if (upload.Model3D == null || upload.Thumbnail == null) { return BadRequest(new { error = "Model and thumbnail are required" }); } // Generate unique car ID var carId = $"custom_{Guid.NewGuid():N}"; var carFolder = Path.Combine(_customAssetsPath, "cars", carId); Directory.CreateDirectory(carFolder); // Save files var modelPath = await SaveFile(upload.Model3D, carFolder, "model.pak"); var thumbnailPath = await SaveFile(upload.Thumbnail, carFolder, "thumbnail.png"); string? texturePath = null; if (upload.Textures != null) { texturePath = await SaveFile(upload.Textures, carFolder, "textures.pvr"); } string? audioPath = null; if (upload.EngineAudio != null) { audioPath = await SaveFile(upload.EngineAudio, carFolder, "engine.ogg"); } // Calculate MD5 hashes var modelMd5 = await CalculateMd5(modelPath); // Create car record var customCar = new Car { CarId = carId, Name = upload.CarName, Manufacturer = upload.Manufacturer, ClassType = upload.ClassType, BasePerformanceRating = upload.PerformanceRating, Year = upload.Year ?? DateTime.Now.Year, CashPrice = upload.CashPrice ?? 0, GoldPrice = upload.GoldPrice ?? 0, Description = upload.Description, IsCustom = true, CustomAuthor = upload.AuthorName, CustomVersion = upload.Version ?? "1.0", CreatedAt = DateTime.UtcNow }; _context.Cars.Add(customCar); // Create asset entries var modelAsset = new GameAsset { AssetType = "car_model", FileName = "model.pak", EaCdnPath = $"/custom/cars/{carId}/model.pak", LocalPath = modelPath, FileSize = new FileInfo(modelPath).Length, Md5Hash = modelMd5, ContentType = "application/octet-stream", Category = "cars", CarId = carId, IsCustomContent = true, CustomAuthor = upload.AuthorName, DownloadedAt = DateTime.UtcNow }; _context.GameAssets.Add(modelAsset); // Add thumbnail asset var thumbAsset = new GameAsset { AssetType = "car_thumbnail", FileName = "thumbnail.png", EaCdnPath = $"/custom/cars/{carId}/thumbnail.png", LocalPath = thumbnailPath, FileSize = new FileInfo(thumbnailPath).Length, Md5Hash = await CalculateMd5(thumbnailPath), ContentType = "image/png", Category = "ui", CarId = carId, IsCustomContent = true, CustomAuthor = upload.AuthorName, DownloadedAt = DateTime.UtcNow }; _context.GameAssets.Add(thumbAsset); await _context.SaveChangesAsync(); _logger.LogInformation("Custom car uploaded successfully: {CarId} - {Name}", carId, upload.CarName); return Ok(new { success = true, carId = carId, name = upload.CarName, message = "Custom car uploaded successfully! It will appear in the catalog.", files = new { model = modelPath, thumbnail = thumbnailPath, textures = texturePath, audio = audioPath } }); } catch (Exception ex) { _logger.LogError(ex, "Error uploading custom car"); return StatusCode(500, new { error = "Upload failed", details = ex.Message }); } } /// /// Upload a custom track /// [HttpPost("tracks/upload")] [RequestSizeLimit(200_000_000)] // 200 MB max for tracks public async Task UploadCustomTrack([FromForm] CustomTrackUpload upload) { _logger.LogInformation("Custom track upload: {Name} by {Author}", upload.TrackName, upload.AuthorName); try { if (upload.TrackData == null || upload.Thumbnail == null) { return BadRequest(new { error = "Track data and thumbnail are required" }); } var trackId = $"custom_{Guid.NewGuid():N}"; var trackFolder = Path.Combine(_customAssetsPath, "tracks", trackId); Directory.CreateDirectory(trackFolder); // Save files var trackPath = await SaveFile(upload.TrackData, trackFolder, "track.pak"); var thumbnailPath = await SaveFile(upload.Thumbnail, trackFolder, "thumbnail.png"); string? sceneryPath = null; if (upload.Scenery != null) { sceneryPath = await SaveFile(upload.Scenery, trackFolder, "scenery.pak"); } // Create track record in database (you'll need to add Track entity) var trackAsset = new GameAsset { AssetType = "track", FileName = "track.pak", EaCdnPath = $"/custom/tracks/{trackId}/track.pak", LocalPath = trackPath, FileSize = new FileInfo(trackPath).Length, Md5Hash = await CalculateMd5(trackPath), ContentType = "application/octet-stream", Category = "tracks", TrackId = trackId, IsCustomContent = true, CustomAuthor = upload.AuthorName, DownloadedAt = DateTime.UtcNow }; _context.GameAssets.Add(trackAsset); await _context.SaveChangesAsync(); return Ok(new { success = true, trackId = trackId, name = upload.TrackName, message = "Custom track uploaded successfully!", files = new { track = trackPath, thumbnail = thumbnailPath, scenery = sceneryPath } }); } catch (Exception ex) { _logger.LogError(ex, "Error uploading custom track"); return StatusCode(500, new { error = "Upload failed", details = ex.Message }); } } /// /// Get list of all custom content /// [HttpGet("content")] public async Task GetCustomContent( [FromQuery] string? type = null, [FromQuery] string? author = null) { var query = _context.GameAssets .Where(a => a.IsCustomContent); if (!string.IsNullOrEmpty(type)) { query = query.Where(a => a.AssetType == type); } if (!string.IsNullOrEmpty(author)) { query = query.Where(a => a.CustomAuthor == author); } var content = await query .GroupBy(a => new { a.CarId, a.TrackId, a.CustomAuthor }) .Select(g => new { id = g.Key.CarId ?? g.Key.TrackId, author = g.Key.CustomAuthor, type = g.First().AssetType, files = g.Select(a => new { name = a.FileName, path = a.EaCdnPath, size = a.FileSize, uploadedAt = a.DownloadedAt }).ToList() }) .ToListAsync(); return Ok(new { success = true, count = content.Count, content = content }); } /// /// Get custom cars list /// [HttpGet("cars")] public async Task GetCustomCars() { var customCars = await _context.Cars .Where(c => c.IsCustom) .Select(c => new { carId = c.CarId, name = c.Name, manufacturer = c.Manufacturer, classType = c.ClassType, performanceRating = c.BasePerformanceRating, year = c.Year, author = c.CustomAuthor, version = c.CustomVersion, description = c.Description, cashPrice = c.CashPrice, goldPrice = c.GoldPrice, createdAt = c.CreatedAt }) .ToListAsync(); return Ok(new { success = true, count = customCars.Count, cars = customCars }); } /// /// Delete custom content (moderator only) /// [HttpDelete("content/{contentId}")] public async Task DeleteCustomContent(string contentId) { _logger.LogInformation("Deleting custom content: {ContentId}", contentId); // Find all assets for this content var assets = await _context.GameAssets .Where(a => a.CarId == contentId || a.TrackId == contentId) .ToListAsync(); if (assets.Count == 0) { return NotFound(new { error = "Content not found" }); } // Delete files foreach (var asset in assets) { if (!string.IsNullOrEmpty(asset.LocalPath) && System.IO.File.Exists(asset.LocalPath)) { System.IO.File.Delete(asset.LocalPath); } } // Delete from database _context.GameAssets.RemoveRange(assets); // Delete car record if it's a car var car = await _context.Cars.FirstOrDefaultAsync(c => c.CarId == contentId); if (car != null) { _context.Cars.Remove(car); } await _context.SaveChangesAsync(); return Ok(new { success = true, message = "Content deleted" }); } /// /// Create a mod pack (bundle of custom content) /// [HttpPost("modpack/create")] public async Task CreateModPack([FromBody] ModPackRequest request) { _logger.LogInformation("Creating mod pack: {Name} by {Author}", request.PackName, request.Author); var modPackId = $"modpack_{Guid.NewGuid():N}"; var modPackPath = Path.Combine(_modsPath, modPackId); Directory.CreateDirectory(modPackPath); // Create mod pack metadata var modPack = new ModPack { PackId = modPackId, Name = request.PackName, Author = request.Author, Description = request.Description, Version = request.Version ?? "1.0", CarIds = string.Join(",", request.CarIds ?? Array.Empty()), TrackIds = string.Join(",", request.TrackIds ?? Array.Empty()), CreatedAt = DateTime.UtcNow }; _context.ModPacks.Add(modPack); await _context.SaveChangesAsync(); return Ok(new { success = true, packId = modPackId, name = request.PackName, message = "Mod pack created! Users can now subscribe to it.", downloadUrl = $"/modding/api/modpack/{modPackId}/download" }); } /// /// Get available mod packs /// [HttpGet("modpacks")] public async Task GetModPacks() { var packs = await _context.ModPacks .Select(p => new { packId = p.PackId, name = p.Name, author = p.Author, description = p.Description, version = p.Version, carCount = p.CarIds != null ? p.CarIds.Split(',').Length : 0, trackCount = p.TrackIds != null ? p.TrackIds.Split(',').Length : 0, downloads = p.DownloadCount, rating = p.Rating, createdAt = p.CreatedAt }) .ToListAsync(); return Ok(new { success = true, count = packs.Count, modPacks = packs }); } #region Helper Methods private async Task SaveFile(IFormFile file, string folder, string filename) { var filePath = Path.Combine(folder, filename); using var stream = new FileStream(filePath, FileMode.Create); await file.CopyToAsync(stream); return filePath; } private async Task CalculateMd5(string filePath) { using var md5 = MD5.Create(); using var stream = System.IO.File.OpenRead(filePath); var hash = await md5.ComputeHashAsync(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } #endregion } #region Request Models public class CustomCarUpload { public IFormFile Model3D { get; set; } = null!; public IFormFile Thumbnail { get; set; } = null!; public IFormFile? Textures { get; set; } public IFormFile? EngineAudio { get; set; } public string CarName { get; set; } = string.Empty; public string Manufacturer { get; set; } = string.Empty; public string ClassType { get; set; } = "Custom"; public int PerformanceRating { get; set; } public int? Year { get; set; } public int? CashPrice { get; set; } public int? GoldPrice { get; set; } public string? Description { get; set; } public string AuthorName { get; set; } = string.Empty; public string? Version { get; set; } } public class CustomTrackUpload { public IFormFile TrackData { get; set; } = null!; public IFormFile Thumbnail { get; set; } = null!; public IFormFile? Scenery { get; set; } public string TrackName { get; set; } = string.Empty; public string Country { get; set; } = string.Empty; public string? Description { get; set; } public string AuthorName { get; set; } = string.Empty; public string? Version { get; set; } public double? LengthKm { get; set; } public int? Corners { get; set; } } public class ModPackRequest { public string PackName { get; set; } = string.Empty; public string Author { get; set; } = string.Empty; public string? Description { get; set; } public string? Version { get; set; } public List? CarIds { get; set; } public List? TrackIds { get; set; } } #endregion