Files
Radicade/Server/Matchmaking/MatchmakingPlayerAPI.cs
2026-10-16 23:33:13 -07:00

611 lines
18 KiB
C#

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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}
}
}