- 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>
1013 lines
34 KiB
C#
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();
|
|
}
|
|
}
|