using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.Auth; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Players; using RadicadeServer.RecNet.Notifications; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; namespace RadicadeServer.API.Matchmaking { [Route("matchmaking/player")] public class MatchmakingPlayerAPI : Controller { private readonly RecDBContext db; public MatchmakingPlayerAPI(RecDBContext context) { db = context; } [HttpPut("photonregionpings")] public IActionResult SendPhotonRegionPings() { return Ok(new { }); } [HttpPut("gameserverregionpings")] public IActionResult SendGameServerRegionPings() { return Ok(new { }); } [HttpGet("qos")] public IActionResult GetQos() { return Ok(new[] { new { id = "us-west1", address = "34.169.254.144:50000" }, new { id = "europe-west1", address = "35.205.141.119:50000" }, new { id = "asia-northeast1", address = "35.200.67.228:50000" }, new { id = "us-east1", address = "34.73.244.122:50000" }, new { id = "us-central1", address = "34.69.179.51:50000" }, new { id = "northamerica-northeast1", address = "34.152.4.100:50000" } }); } [HttpGet("connection-info"), Authorize] public async Task GetConnectionInfo([FromQuery] long? roomInstanceId = null) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Unauthorized(); } long userId = nullableUserId.Value; string photonRoomId = string.Empty; var presence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(x => x.playerId == userId); if (presence?.roomInstance.HasValue == true) { var instance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value); photonRoomId = instance?.photonRoomId ?? string.Empty; } if (string.IsNullOrEmpty(photonRoomId) && roomInstanceId.HasValue) { var requested = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId.Value); photonRoomId = requested?.photonRoomId ?? string.Empty; } var platformId = User.GetPlatId() ?? string.Empty; var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(DBConfig.JwtKey); var now = DateTime.UtcNow; var photonToken = tokenHandler.WriteToken(tokenHandler.CreateToken( new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor { Subject = new System.Security.Claims.ClaimsIdentity(new[] { new System.Security.Claims.Claim("sub", userId.ToString()), new System.Security.Claims.Claim("rn.platid", platformId), new System.Security.Claims.Claim("rn.plat", User.FindFirst("rn.plat")?.Value ?? "0"), new System.Security.Claims.Claim("rn.deviceclass", User.FindFirst("rn.deviceclass")?.Value ?? "0"), new System.Security.Claims.Claim("rn.env", "prod") }), Issuer = DBConfig.TokenIssuer, Expires = now.AddHours(1), SigningCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials( new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(key), Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature) })); return Ok(new { success = true, value = new { photonAuthToken = photonToken, photonRealtimeAppId = DBConfig.PhotonRealtimeAppId, photonVoiceAppId = DBConfig.PhotonVoiceAppId, photonChatAppId = DBConfig.PhotonChatAppId, photonRegion = DBConfig.PhotonRegion, photonRoomId, voiceConnectionInfo = string.Empty, voiceServerId = string.Empty, experiments = new { networkTransformSyncInterval = 10.0, shouldUseUnreliableOnChange = false, shouldAvoidDiscontinuityRPCs = true, shouldAvoidRedundantDiscontinuity = false, r2RuntimeStaticBaking = true, r2AutoEmbodiment = true, r2RuntimeStaticBakingMinShapeThreshold = 1, r2UseCheapReplicas = true, shouldUseGameServerNetworking = false } }, error = (string?)null }); } [HttpGet("avoidjuniors"), Authorize] public IActionResult GetAvoidJuniors() { return Ok(false); } [HttpPut("avoidjuniors"), Authorize] public IActionResult SetAvoidJuniors() { return Ok(false); } [HttpPut("statusvisibility"), Authorize] public async Task SetStatusVisibility( StatusVisibility statusVisibility) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Unauthorized("Missing user ID."); } long userId = nullableUserId.Value; var presence = await db.Presences .FirstOrDefaultAsync(x => x.playerId == userId); if (presence == null) { return NotFound("Player presence does not exist."); } presence.statusVisibility = statusVisibility; await db.SaveChangesAsync(); await Notify.UpdateSubscriptions(userId); return Ok(new { }); } [HttpPost("heartbeat"), Authorize] public async Task Heartbeat() { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Unauthorized("Missing user ID."); } long userId = nullableUserId.Value; try { return await HeartbeatCore(userId); } catch (Exception) { return Ok(new PlayerHeartbeat { playerId = userId, isOnline = true, deviceClass = DeviceClass.Screen, statusVisibility = StatusVisibility.Public, roomInstance = null }); } } private async Task HeartbeatCore(long userId) { var presence = await db.Presences .FirstOrDefaultAsync(x => x.playerId == userId); if (presence == null) { presence = new RealPresence { playerId = userId }; db.Presences.Add(presence); await MatchmakingUtils.EnsureDormPresenceAsync(db, userId, presence); } RoomInstance? roomInstance = null; if (presence.roomInstance.HasValue) { long roomInstanceId = presence.roomInstance.Value; roomInstance = await db.RoomInstances .FirstOrDefaultAsync( x => x.roomInstanceId == roomInstanceId ); if (roomInstance == null) { presence.roomInstance = null; MatchmakingUtils.ClearInRoom(userId); } } if (roomInstance != null) { roomInstance.eventId ??= 0; roomInstance.clubId ??= 0; roomInstance.roomCode ??= string.Empty; roomInstance.location ??= string.Empty; } if (roomInstance != null) { var currentRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); bool isOwnDorm = currentRoom != null && currentRoom.IsDorm && currentRoom.CreatorAccountId == userId; if (!isOwnDorm) { var ban = await db.ResolveBanAsync(userId, Request.GetClientIp()); if (ban != null && !await db.IsDeveloper(userId)) { await PlayerUtils.EjectToDormAsync(db, userId); roomInstance = presence.roomInstance.HasValue ? await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance.Value) : null; await Notify.SendNotify(userId, new Notification { Id = NotificationType.ModerationKick, Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, userId) }); } } } presence.isOnline = true; presence.deviceClass = DeviceClass.Screen; await db.SaveChangesAsync(); if (roomInstance != null) { try { var xpRoom = await db.Rooms.AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); bool publicRoom = xpRoom != null && !xpRoom.IsDorm && xpRoom.Accessibility == RadicadeServer.RecNet.Enums.Accessibility.Public; await RoomProgressionService.AwardRoomXpAsync(db, userId, roomInstance.roomId, publicRoom); } catch (Exception) { } } var heartbeat = new PlayerHeartbeat { deviceClass = presence.deviceClass, isOnline = true, playerId = userId, roomInstance = roomInstance, statusVisibility = presence.statusVisibility }; if (roomInstance != null) { try { var presenceUpdate = await Presence.CreateAsync(db, presence, roomInstance); await Notify.SendNotify( userId, new Notification { Id = "PresenceUpdate", Msg = presenceUpdate } ); } catch (Exception) { } } try { await Notify.UpdateSubscriptions(userId); } catch (Exception) { } return Ok(heartbeat); } [HttpPost("notifydisconnect"), Authorize] public async Task NotifyDisconnect( [FromForm(Name = "playerId")] long playerId = 0, [FromForm(Name = "loginLock")] string? loginLock = null) { var requesterId = User.GetUserId(); if (!requesterId.HasValue) return Unauthorized(); long targetId = playerId > 0 ? playerId : 0; if (targetId == 0 && !string.IsNullOrWhiteSpace(loginLock)) { var byLock = await db.Presences.FirstOrDefaultAsync(x => x.LoginLock == loginLock); if (byLock != null) targetId = byLock.playerId; } if (targetId == 0) targetId = requesterId.Value; var ban = await db.ResolveBanAsync(targetId, Request.GetClientIp()); if (ban == null) return Ok(); await PlayerUtils.EjectToDormAsync(db, targetId); if (targetId != requesterId.Value) { await Notify.SendNotify(targetId, new Notification { Id = NotificationType.ModerationKick, Msg = await PlayerUtils.BuildBanBlockDetailsAsync(db, ban, targetId) }); } return Ok(); } [HttpPost("login"), Authorize] public async Task Login( [FromForm(Name = "LoginLock")] string? loginLock = null, [FromForm(Name = "identification")] string? identification = null, [FromForm(Name = "JoinMode")] int joinMode = 0, [FromForm(Name = "BypassMovementModeRestriction")] bool bypassMovementModeRestriction = false) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Unauthorized(new { error = "invalid_token", error_description = "Missing user ID." }); } long userId = nullableUserId.Value; string? platformId = User.GetPlatId(); if (string.IsNullOrWhiteSpace(platformId)) { return Unauthorized(new { error = "invalid_token", error_description = "Missing platform ID." }); } bool isWhitelisted; try { isWhitelisted = await AuthShared.IsWhitelisted(platformId); } catch (Exception) { return StatusCode( StatusCodes.Status500InternalServerError, new { error = "server_error", error_description = "Whitelist check failed." } ); } if (!isWhitelisted) { return Unauthorized(new { error = "not_whitelisted", error_description = "This account is not whitelisted." }); } var presence = await db.Presences .FirstOrDefaultAsync( x => x.playerId == userId ); if (presence == null) { presence = new RealPresence { playerId = userId }; db.Presences.Add(presence); } if (!string.IsNullOrWhiteSpace(loginLock)) { presence.LoginLock = loginLock; } presence.isOnline = true; presence.deviceClass = DeviceClass.Screen; if (!presence.roomInstance.HasValue) { await MatchmakingUtils.EnsureDormPresenceAsync( db, userId, presence); } else { presence.isOnline = true; } if (presence.roomInstance.HasValue) { MatchmakingUtils.MarkInRoom(userId); } else { MatchmakingUtils.ClearInRoom(userId); } await db.SaveChangesAsync(); try { await Notify.UpdateSubscriptions(userId); } catch (Exception) { } return Ok(new { success = true, playerId = userId, accountId = userId, platformId, loginLock = presence.LoginLock ?? string.Empty }); } [HttpPost("exclusivelogin"), Authorize] public Task ExclusiveLogin( [FromForm(Name = "LoginLock")] string? loginLock = null, [FromForm(Name = "identification")] string? identification = null, [FromForm(Name = "JoinMode")] int joinMode = 0, [FromForm(Name = "BypassMovementModeRestriction")] bool bypassMovementModeRestriction = false) { return Login( loginLock, identification, joinMode, bypassMovementModeRestriction); } [HttpPost("logout"), Authorize] public async Task Logout( [FromForm(Name = "LoginLock")] string? loginLock = null) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Ok(); } long userId = nullableUserId.Value; var presence = await db.Presences .FirstOrDefaultAsync( x => x.playerId == userId ); if (presence == null) { MatchmakingUtils.ClearInRoom(userId); return Ok(); } presence.LoginLock = null; presence.isOnline = false; presence.roomInstance = null; MatchmakingUtils.ClearInRoom(userId); await db.SaveChangesAsync(); var subscriberUpdate = Notify.UpdateSubscriptions(userId); try { await Notify.SendNotify(userId, new Notification { Id = "PresenceUpdate", Msg = await Presence.CreateAsync(db, presence, null) }); } catch (Exception) { } await subscriberUpdate; return Ok(); } } }