Files
rr3-server/RR3CommunityServer/Controllers/MultiplayerController.cs
Daniel Elliott a934f57b52 Add Friends/Social & Multiplayer systems - 95 total endpoints
- Implemented Friends/Social Service (11 endpoints)
  * Friend management (list, add, accept, remove)
  * User search and invitations
  * Gift sending and claiming
  * Clubs/Teams system

- Implemented Multiplayer Service (12 endpoints)
  * Matchmaking (queue, status, leave)
  * Race sessions (create, join, ready, details)
  * Ghost data (upload, download)
  * Race results (submit, view)
  * Competitive rankings (rating, leaderboard)

- Added database entities:
  * Friends, FriendInvitations, Gifts
  * Clubs, ClubMembers
  * MatchmakingQueues, RaceSessions, RaceParticipants
  * GhostData, CompetitiveRatings

- Created migrations:
  * AddFriendsSocialSystem (5 tables)
  * AddMultiplayerSystem (5 tables)

Total: 95 endpoints - 100% EA server replacement ready

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-23 16:55:33 -08:00

1013 lines
34 KiB
C#

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<MultiplayerController> _logger;
public MultiplayerController(RR3DbContext context, ILogger<MultiplayerController> logger)
{
_context = context;
_logger = logger;
}
// ===== MATCHMAKING (3 endpoints) =====
// POST /synergy/multiplayer/matchmaking/queue - Join matchmaking queue
[HttpPost("matchmaking/queue")]
public async Task<ActionResult<MatchmakingStatusResponse>> 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<ActionResult<MatchmakingStatusResponse>> 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<ActionResult<SimpleResponse>> 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<ActionResult<RaceSessionResponse>> 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<ParticipantDto>
{
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<ActionResult<RaceSessionResponse>> 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<ActionResult<RaceSessionResponse>> 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<ActionResult<SimpleResponse>> 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<ActionResult<SimpleResponse>> 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<ActionResult<GhostDataResponse>> 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<ActionResult<RaceResultResponse>> 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<ActionResult<RaceResultsResponse>> 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<ActionResult<RatingResponse>> 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<ActionResult<CompetitiveLeaderboardResponse>> 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();
}
}