using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RR3CommunityServer.Data; using RR3CommunityServer.Models; namespace RR3CommunityServer.Controllers; [ApiController] [Route("synergy/multiplayer")] public class MultiplayerController : ControllerBase { private readonly RR3DbContext _context; private readonly ILogger _logger; public MultiplayerController(RR3DbContext context, ILogger logger) { _context = context; _logger = logger; } // ===== MATCHMAKING (3 endpoints) ===== // POST /synergy/multiplayer/matchmaking/queue - Join matchmaking queue [HttpPost("matchmaking/queue")] public async Task> JoinMatchmakingQueue([FromBody] JoinMatchmakingRequest request) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return Ok(new MatchmakingStatusResponse { ResultCode = -1, Message = "User not found" }); } // Check if already in queue var existingQueue = await _context.MatchmakingQueues .FirstOrDefaultAsync(q => q.UserId == user.Id && q.Status == "queued"); if (existingQueue != null) { return Ok(new MatchmakingStatusResponse { ResultCode = 0, Status = "queued", QueueId = existingQueue.Id, QueuedAt = existingQueue.QueuedAt, EstimatedWaitSeconds = 30 }); } // Add to matchmaking queue var queueEntry = new MatchmakingQueue { UserId = user.Id, CarClass = request.CarClass, Track = request.Track, GameMode = request.GameMode, Status = "queued", QueuedAt = DateTime.UtcNow }; _context.MatchmakingQueues.Add(queueEntry); await _context.SaveChangesAsync(); _logger.LogInformation("User {User} joined matchmaking for {Track} ({Class})", user.SynergyId, request.Track, request.CarClass); // Simple matchmaking: find other queued player for same track/class var matchedPlayer = await _context.MatchmakingQueues .Where(q => q.Id != queueEntry.Id && q.Status == "queued" && q.Track == request.Track && q.CarClass == request.CarClass) .FirstOrDefaultAsync(); if (matchedPlayer != null) { // Create race session var session = new RaceSession { SessionCode = GenerateSessionCode(), Track = request.Track, CarClass = request.CarClass, HostUserId = user.Id, MaxPlayers = 8, Status = "lobby", IsPrivate = false, CreatedAt = DateTime.UtcNow }; _context.RaceSessions.Add(session); await _context.SaveChangesAsync(); // Add both players as participants var participants = new[] { new RaceParticipant { SessionId = session.Id, UserId = user.Id }, new RaceParticipant { SessionId = session.Id, UserId = matchedPlayer.UserId } }; _context.RaceParticipants.AddRange(participants); // Update queue entries queueEntry.Status = "matched"; queueEntry.MatchedAt = DateTime.UtcNow; queueEntry.SessionId = session.Id; matchedPlayer.Status = "matched"; matchedPlayer.MatchedAt = DateTime.UtcNow; matchedPlayer.SessionId = session.Id; await _context.SaveChangesAsync(); _logger.LogInformation("Matched 2 players, created session {SessionCode}", session.SessionCode); return Ok(new MatchmakingStatusResponse { ResultCode = 0, Status = "matched", QueueId = queueEntry.Id, SessionId = session.Id, SessionCode = session.SessionCode, QueuedAt = queueEntry.QueuedAt }); } return Ok(new MatchmakingStatusResponse { ResultCode = 0, Status = "queued", QueueId = queueEntry.Id, QueuedAt = queueEntry.QueuedAt, EstimatedWaitSeconds = 30 }); } catch (Exception ex) { _logger.LogError(ex, "Error joining matchmaking queue"); return Ok(new MatchmakingStatusResponse { ResultCode = -1, Message = "Error joining queue" }); } } // GET /synergy/multiplayer/matchmaking/status - Check matchmaking status [HttpGet("matchmaking/status")] public async Task> GetMatchmakingStatus( [FromQuery] string synergyId, [FromQuery] int queueId) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return Ok(new MatchmakingStatusResponse { ResultCode = -1, Message = "User not found" }); } var queueEntry = await _context.MatchmakingQueues .Include(q => q.Session) .FirstOrDefaultAsync(q => q.Id == queueId && q.UserId == user.Id); if (queueEntry == null) { return Ok(new MatchmakingStatusResponse { ResultCode = -2, Message = "Queue entry not found" }); } return Ok(new MatchmakingStatusResponse { ResultCode = 0, Status = queueEntry.Status, QueueId = queueEntry.Id, SessionId = queueEntry.SessionId, SessionCode = queueEntry.Session?.SessionCode, QueuedAt = queueEntry.QueuedAt, EstimatedWaitSeconds = queueEntry.Status == "queued" ? 30 : null }); } catch (Exception ex) { _logger.LogError(ex, "Error checking matchmaking status"); return Ok(new MatchmakingStatusResponse { ResultCode = -1, Message = "Error checking status" }); } } // DELETE /synergy/multiplayer/matchmaking/leave - Leave matchmaking queue [HttpDelete("matchmaking/leave")] public async Task> LeaveMatchmakingQueue( [FromQuery] string synergyId, [FromQuery] int queueId) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return Ok(new SimpleResponse { ResultCode = -1, Message = "User not found" }); } var queueEntry = await _context.MatchmakingQueues .FirstOrDefaultAsync(q => q.Id == queueId && q.UserId == user.Id); if (queueEntry == null) { return Ok(new SimpleResponse { ResultCode = -2, Message = "Queue entry not found" }); } if (queueEntry.Status == "matched") { return Ok(new SimpleResponse { ResultCode = -3, Message = "Already matched, cannot leave" }); } queueEntry.Status = "cancelled"; await _context.SaveChangesAsync(); _logger.LogInformation("User {User} left matchmaking queue", user.SynergyId); return Ok(new SimpleResponse { ResultCode = 0, Message = "Left queue" }); } catch (Exception ex) { _logger.LogError(ex, "Error leaving matchmaking queue"); return Ok(new SimpleResponse { ResultCode = -1, Message = "Error leaving queue" }); } } // ===== RACE SESSIONS (4 endpoints) ===== // POST /synergy/multiplayer/session/create - Create private race session [HttpPost("session/create")] public async Task> CreateRaceSession([FromBody] CreateRaceSessionRequest request) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return Ok(new RaceSessionResponse { ResultCode = -1, Message = "User not found" }); } var session = new RaceSession { SessionCode = GenerateSessionCode(), Track = request.Track, CarClass = request.CarClass, HostUserId = user.Id, MaxPlayers = request.MaxPlayers, Status = "lobby", IsPrivate = request.IsPrivate, CreatedAt = DateTime.UtcNow }; _context.RaceSessions.Add(session); await _context.SaveChangesAsync(); // Add host as first participant var participant = new RaceParticipant { SessionId = session.Id, UserId = user.Id }; _context.RaceParticipants.Add(participant); await _context.SaveChangesAsync(); _logger.LogInformation("User {User} created race session {Code}", user.SynergyId, session.SessionCode); return Ok(new RaceSessionResponse { ResultCode = 0, Session = new RaceSessionDto { SessionId = session.Id, SessionCode = session.SessionCode, Track = session.Track, CarClass = session.CarClass, HostUserId = user.Id, HostNickname = user.Nickname ?? "Player", CurrentPlayers = 1, MaxPlayers = session.MaxPlayers, Status = session.Status, CreatedAt = session.CreatedAt }, Participants = new List { new ParticipantDto { UserId = user.Id, Nickname = user.Nickname ?? "Player", SynergyId = user.SynergyId, IsReady = false } } }); } catch (Exception ex) { _logger.LogError(ex, "Error creating race session"); return Ok(new RaceSessionResponse { ResultCode = -1, Message = "Error creating session" }); } } // POST /synergy/multiplayer/session/join - Join race session [HttpPost("session/join")] public async Task> JoinRaceSession( [FromQuery] string synergyId, [FromQuery] int? sessionId = null, [FromQuery] string? joinCode = null) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return Ok(new RaceSessionResponse { ResultCode = -1, Message = "User not found" }); } RaceSession? session = null; if (sessionId.HasValue) { session = await _context.RaceSessions .Include(s => s.Host) .Include(s => s.Participants) .ThenInclude(p => p.User) .FirstOrDefaultAsync(s => s.Id == sessionId.Value); } else if (!string.IsNullOrEmpty(joinCode)) { session = await _context.RaceSessions .Include(s => s.Host) .Include(s => s.Participants) .ThenInclude(p => p.User) .FirstOrDefaultAsync(s => s.SessionCode == joinCode); } if (session == null) { return Ok(new RaceSessionResponse { ResultCode = -2, Message = "Session not found" }); } if (session.Status != "lobby") { return Ok(new RaceSessionResponse { ResultCode = -3, Message = "Session already started" }); } if (session.Participants.Count >= session.MaxPlayers) { return Ok(new RaceSessionResponse { ResultCode = -4, Message = "Session is full" }); } // Check if already in session if (session.Participants.Any(p => p.UserId == user.Id)) { return Ok(new RaceSessionResponse { ResultCode = -5, Message = "Already in session" }); } // Add participant var participant = new RaceParticipant { SessionId = session.Id, UserId = user.Id }; _context.RaceParticipants.Add(participant); await _context.SaveChangesAsync(); _logger.LogInformation("User {User} joined session {Code}", user.SynergyId, session.SessionCode); // Reload participants session = await _context.RaceSessions .Include(s => s.Host) .Include(s => s.Participants) .ThenInclude(p => p.User) .FirstAsync(s => s.Id == session.Id); return Ok(new RaceSessionResponse { ResultCode = 0, Session = new RaceSessionDto { SessionId = session.Id, SessionCode = session.SessionCode, Track = session.Track, CarClass = session.CarClass, HostUserId = session.HostUserId, HostNickname = session.Host?.Nickname ?? "Player", CurrentPlayers = session.Participants.Count, MaxPlayers = session.MaxPlayers, Status = session.Status, CreatedAt = session.CreatedAt }, Participants = session.Participants.Select(p => new ParticipantDto { UserId = p.UserId, Nickname = p.User?.Nickname ?? "Player", SynergyId = p.User?.SynergyId ?? "", CarId = p.CarId, IsReady = p.IsReady }).ToList() }); } catch (Exception ex) { _logger.LogError(ex, "Error joining race session"); return Ok(new RaceSessionResponse { ResultCode = -1, Message = "Error joining session" }); } } // GET /synergy/multiplayer/session/{sessionId} - Get session details [HttpGet("session/{sessionId}")] public async Task> GetRaceSession(int sessionId) { try { var session = await _context.RaceSessions .Include(s => s.Host) .Include(s => s.Participants) .ThenInclude(p => p.User) .FirstOrDefaultAsync(s => s.Id == sessionId); if (session == null) { return Ok(new RaceSessionResponse { ResultCode = -1, Message = "Session not found" }); } return Ok(new RaceSessionResponse { ResultCode = 0, Session = new RaceSessionDto { SessionId = session.Id, SessionCode = session.SessionCode, Track = session.Track, CarClass = session.CarClass, HostUserId = session.HostUserId, HostNickname = session.Host?.Nickname ?? "Player", CurrentPlayers = session.Participants.Count, MaxPlayers = session.MaxPlayers, Status = session.Status, CreatedAt = session.CreatedAt }, Participants = session.Participants.Select(p => new ParticipantDto { UserId = p.UserId, Nickname = p.User?.Nickname ?? "Player", SynergyId = p.User?.SynergyId ?? "", CarId = p.CarId, IsReady = p.IsReady, FinishPosition = p.FinishPosition, RaceTime = p.RaceTime }).ToList() }); } catch (Exception ex) { _logger.LogError(ex, "Error getting race session"); return Ok(new RaceSessionResponse { ResultCode = -1, Message = "Error loading session" }); } } // POST /synergy/multiplayer/session/{sessionId}/ready - Mark player as ready [HttpPost("session/{sessionId}/ready")] public async Task> MarkReady( int sessionId, [FromQuery] string synergyId) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return Ok(new SimpleResponse { ResultCode = -1, Message = "User not found" }); } var participant = await _context.RaceParticipants .Include(p => p.Session) .FirstOrDefaultAsync(p => p.SessionId == sessionId && p.UserId == user.Id); if (participant == null) { return Ok(new SimpleResponse { ResultCode = -2, Message = "Not in this session" }); } participant.IsReady = true; await _context.SaveChangesAsync(); // Check if all players are ready var allReady = await _context.RaceParticipants .Where(p => p.SessionId == sessionId) .AllAsync(p => p.IsReady); if (allReady && participant.Session != null) { participant.Session.Status = "countdown"; participant.Session.StartedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); _logger.LogInformation("Session {Session} starting - all players ready", sessionId); } return Ok(new SimpleResponse { ResultCode = 0, Message = "Ready" }); } catch (Exception ex) { _logger.LogError(ex, "Error marking ready"); return Ok(new SimpleResponse { ResultCode = -1, Message = "Error updating status" }); } } // ===== GHOST DATA (2 endpoints) ===== // POST /synergy/multiplayer/ghost/upload - Upload ghost race data [HttpPost("ghost/upload")] public async Task> UploadGhostData([FromBody] UploadGhostRequest request) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return Ok(new SimpleResponse { ResultCode = -1, Message = "User not found" }); } var ghost = new GhostData { UserId = user.Id, Track = request.Track, CarId = request.CarId, RaceTime = request.RaceTime, TelemetryData = request.TelemetryData, UploadedAt = DateTime.UtcNow }; _context.GhostData.Add(ghost); await _context.SaveChangesAsync(); _logger.LogInformation("User {User} uploaded ghost for {Track} ({Time:F3}s)", user.SynergyId, request.Track, request.RaceTime); return Ok(new SimpleResponse { ResultCode = 0, Message = $"Ghost uploaded (ID: {ghost.Id})" }); } catch (Exception ex) { _logger.LogError(ex, "Error uploading ghost data"); return Ok(new SimpleResponse { ResultCode = -1, Message = "Error uploading ghost" }); } } // GET /synergy/multiplayer/ghost/download - Download ghost race data [HttpGet("ghost/download")] public async Task> DownloadGhostData( [FromQuery] string track, [FromQuery] string? synergyId = null, [FromQuery] int? rank = null) { try { GhostData? ghost = null; if (!string.IsNullOrEmpty(synergyId)) { // Get specific user's best ghost for this track var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user != null) { ghost = await _context.GhostData .Where(g => g.UserId == user.Id && g.Track == track) .OrderBy(g => g.RaceTime) .FirstOrDefaultAsync(); } } else if (rank.HasValue) { // Get nth fastest ghost for this track var ghosts = await _context.GhostData .Where(g => g.Track == track) .OrderBy(g => g.RaceTime) .Skip(rank.Value - 1) .Take(1) .Include(g => g.User) .ToListAsync(); ghost = ghosts.FirstOrDefault(); } else { // Get fastest ghost for this track ghost = await _context.GhostData .Where(g => g.Track == track) .OrderBy(g => g.RaceTime) .Include(g => g.User) .FirstOrDefaultAsync(); } if (ghost == null) { return Ok(new GhostDataResponse { ResultCode = -1, Message = "Ghost not found" }); } // Increment download counter ghost.Downloads++; await _context.SaveChangesAsync(); return Ok(new GhostDataResponse { ResultCode = 0, Ghost = new GhostDataDto { GhostId = ghost.Id, UserId = ghost.UserId, Nickname = ghost.User?.Nickname ?? "Player", Track = ghost.Track, CarId = ghost.CarId, RaceTime = ghost.RaceTime, TelemetryData = ghost.TelemetryData, UploadedAt = ghost.UploadedAt } }); } catch (Exception ex) { _logger.LogError(ex, "Error downloading ghost data"); return Ok(new GhostDataResponse { ResultCode = -1, Message = "Error downloading ghost" }); } } // ===== RACE RESULTS (2 endpoints) ===== // POST /synergy/multiplayer/race/submit - Submit race results [HttpPost("race/submit")] public async Task> SubmitRaceResult([FromBody] SubmitRaceResultRequest request) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == request.SynergyId); if (user == null) { return Ok(new RaceResultResponse { ResultCode = -1, Message = "User not found" }); } var participant = await _context.RaceParticipants .Include(p => p.Session) .FirstOrDefaultAsync(p => p.SessionId == request.SessionId && p.UserId == user.Id); if (participant == null) { return Ok(new RaceResultResponse { ResultCode = -2, Message = "Not in this session" }); } // Update participant results participant.FinishPosition = request.Position; participant.RaceTime = request.RaceTime; // Calculate rewards based on position int goldReward = Math.Max(0, 100 - (request.Position - 1) * 20); int cashReward = Math.Max(0, 500 - (request.Position - 1) * 100); int xpReward = Math.Max(0, 200 - (request.Position - 1) * 30); participant.RewardGold = goldReward; participant.RewardCash = cashReward; participant.RewardXP = xpReward; // Apply rewards to user user.Gold = (user.Gold ?? 0) + goldReward; user.Cash = (user.Cash ?? 0) + cashReward; user.Experience = (user.Experience ?? 0) + xpReward; await _context.SaveChangesAsync(); _logger.LogInformation("User {User} finished position {Pos} in session {Session}", user.SynergyId, request.Position, request.SessionId); var response = new RaceResultResponse { ResultCode = 0, Position = request.Position, RewardGold = goldReward, RewardCash = cashReward, RewardXP = xpReward }; // If ranked match, update rating if (participant.Session?.Status == "racing" || participant.Session?.Status == "countdown") { var rating = await _context.CompetitiveRatings .FirstOrDefaultAsync(r => r.UserId == user.Id); if (rating == null) { rating = new CompetitiveRating { UserId = user.Id, Rating = 1000 }; _context.CompetitiveRatings.Add(rating); } // Simple rating adjustment: +20 for 1st, +10 for 2nd, 0 for 3rd, -10 for 4th+ int ratingChange = request.Position switch { 1 => 20, 2 => 10, 3 => 0, _ => -10 }; rating.Rating += ratingChange; if (request.Position == 1) rating.Wins++; else rating.Losses++; rating.LastMatchAt = DateTime.UtcNow; // Update division based on rating rating.Division = rating.Rating switch { >= 2000 => "Diamond", >= 1500 => "Platinum", >= 1200 => "Gold", >= 900 => "Silver", _ => "Bronze" }; await _context.SaveChangesAsync(); response.RatingChange = ratingChange; response.NewRating = rating.Rating; } return Ok(response); } catch (Exception ex) { _logger.LogError(ex, "Error submitting race result"); return Ok(new RaceResultResponse { ResultCode = -1, Message = "Error submitting result" }); } } // GET /synergy/multiplayer/race/{sessionId}/results - Get race results [HttpGet("race/{sessionId}/results")] public async Task> GetRaceResults(int sessionId) { try { var participants = await _context.RaceParticipants .Where(p => p.SessionId == sessionId && p.FinishPosition.HasValue) .Include(p => p.User) .OrderBy(p => p.FinishPosition) .ToListAsync(); if (!participants.Any()) { return Ok(new RaceResultsResponse { ResultCode = -1, Message = "No results yet" }); } return Ok(new RaceResultsResponse { ResultCode = 0, Results = participants.Select(p => new ParticipantDto { UserId = p.UserId, Nickname = p.User?.Nickname ?? "Player", SynergyId = p.User?.SynergyId ?? "", CarId = p.CarId, FinishPosition = p.FinishPosition, RaceTime = p.RaceTime }).ToList() }); } catch (Exception ex) { _logger.LogError(ex, "Error getting race results"); return Ok(new RaceResultsResponse { ResultCode = -1, Message = "Error loading results" }); } } // ===== RANKED/COMPETITIVE (2 endpoints) ===== // GET /synergy/multiplayer/ranked/rating - Get player's competitive rating [HttpGet("ranked/rating")] public async Task> GetCompetitiveRating([FromQuery] string synergyId) { try { var user = await _context.Users.FirstOrDefaultAsync(u => u.SynergyId == synergyId); if (user == null) { return Ok(new RatingResponse { ResultCode = -1, Message = "User not found" }); } var rating = await _context.CompetitiveRatings .FirstOrDefaultAsync(r => r.UserId == user.Id); if (rating == null) { // Create default rating rating = new CompetitiveRating { UserId = user.Id, Rating = 1000, Division = "Bronze" }; _context.CompetitiveRatings.Add(rating); await _context.SaveChangesAsync(); } return Ok(new RatingResponse { ResultCode = 0, Rating = new CompetitiveRatingDto { UserId = user.Id, Nickname = user.Nickname ?? "Player", Rating = rating.Rating, Wins = rating.Wins, Losses = rating.Losses, Draws = rating.Draws, Division = rating.Division, DivisionRank = rating.DivisionRank } }); } catch (Exception ex) { _logger.LogError(ex, "Error getting competitive rating"); return Ok(new RatingResponse { ResultCode = -1, Message = "Error loading rating" }); } } // GET /synergy/multiplayer/ranked/leaderboard - Get competitive leaderboard [HttpGet("ranked/leaderboard")] public async Task> GetCompetitiveLeaderboard( [FromQuery] int limit = 100) { try { var ratings = await _context.CompetitiveRatings .Include(r => r.User) .OrderByDescending(r => r.Rating) .Take(limit) .ToListAsync(); return Ok(new CompetitiveLeaderboardResponse { ResultCode = 0, Leaderboard = ratings.Select(r => new CompetitiveRatingDto { UserId = r.UserId, Nickname = r.User?.Nickname ?? "Player", Rating = r.Rating, Wins = r.Wins, Losses = r.Losses, Draws = r.Draws, Division = r.Division, DivisionRank = r.DivisionRank }).ToList() }); } catch (Exception ex) { _logger.LogError(ex, "Error getting competitive leaderboard"); return Ok(new CompetitiveLeaderboardResponse { ResultCode = -1, Message = "Error loading leaderboard" }); } } // Helper method to generate session codes private static string GenerateSessionCode() { var random = new Random(); return random.Next(100000, 999999).ToString(); } }