using Microsoft.AspNetCore.Mvc; using Vanadium.Auth; using Vanadium.Classes; using Vanadium.Classes.DBs; using Vanadium.Classes.DBs.DBClasses; using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses; using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses; using Vanadium.Classes.WebSocket; using Vanadium.Utils.NotiController; using Vanadium.Controllers; using System.Collections.Concurrent; namespace Vanadium.Controllers { [ApiController] [Route("/match")] public partial class MatchController : ControllerBase { private static readonly ConcurrentDictionary _authorizedRoomSessions = new(); private static readonly ConcurrentDictionary<(long, long), (int Count, DateTime Window)> _inviteRateLimit = new(); private static readonly string PhotonChatAppId = "9c035c47-8725-43d4-8c53-ab45ed381511"; private static readonly Random _photonRng = new Random(); public static long EnsurePlayerDorm(FullPlayer player) { long dormRoomId = player.Player.PlayerExtra.DormRoomId; if (dormRoomId > 0 && RoomDB.DoesPlayerDormExist(player.PlayerId)) return dormRoomId; var dormRoom = RoomDB.GetPlayerDormRoom(player.PlayerId); if (dormRoom != null) { dormRoomId = dormRoom.RoomId; } else { dormRoomId = RoomDB.CloneDormRoom(player.PlayerId); } PlayerDB.UpdateDormRoomId(player.PlayerId, dormRoomId); player.Player.PlayerExtra.DormRoomId = dormRoomId; return dormRoomId; } private static void AuthorizeRoomSession(long playerId, long roomId) { _authorizedRoomSessions[playerId] = (roomId, DateTime.UtcNow); } private static void ClearRoomSession(long playerId) { _authorizedRoomSessions.TryRemove(playerId, out _); } private static bool IsRoomSessionAuthorized(long playerId, long roomId) { if (!_authorizedRoomSessions.TryGetValue(playerId, out var session)) return false; return session.RoomId == roomId && (DateTime.UtcNow - session.MatchmadeAt).TotalHours < 12; } private static void KickPlayer(long playerId) { } private static bool IsBannedFromRoom(Room? room, long playerId) { return room?.Roles?.Any(r => r.AccountId == playerId && r.Role == Role.Banned) ?? false; } [HttpGet("player")] public async Task GetPlayerHeartbeatBulk([FromQuery] List id) { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); return Ok(PlayerDB.GetPlayerHeartbeatsBulk(id)); } [HttpPost("roominstance/{roomInstanceId}/reportjoinresult")] public async Task ReportJoinResultForRoomInstanceId(ulong roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); long resolvedAccountId = player.PlayerId; long newLevel = player.Player.Level; long newXP = player.Player.XP; _ = NotiController.SendEvent(resolvedAccountId, "PlayerProgressionXPUpdate", new { PlayerId = resolvedAccountId, Level = newLevel, XP = newXP }); _ = NotiController.SendEvent(resolvedAccountId, "PlayerProgressionLevelUpdate", new { PlayerId = resolvedAccountId, Level = newLevel, XP = newXP }); return Ok(); } [HttpPut("roominstance/{roomInstanceId}/markprivate")] public async Task MarkRoomInstancePrivate(long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); Sessions.MarkInstancePrivate(roomInstanceId); Sessions.FakeInstanceFull(roomInstanceId); var playersInInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId) .ToList(); foreach (var fp in playersInInstance) { if (fp.Player?.PlayerExtra?.Heartbeat?.roomInstance == null) continue; fp.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate = true; PlayerDB.Players.Update(fp); } var outsiderIds = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true && p.Player.PlayerExtra.Heartbeat.roomInstance?.roomInstanceId != roomInstanceId) .Select(p => p.PlayerId) .ToList(); foreach (var fp in playersInInstance) { if (fp.Player?.PlayerExtra?.Heartbeat == null) continue; var hbJson = Newtonsoft.Json.JsonConvert.SerializeObject(new WebsocketEvents.Response { Id = NotificationsController.EventTypes.PresenceUpdate, Msg = fp.Player.PlayerExtra.Heartbeat }); await NotificationsController.SendToPlayer(fp.PlayerId, hbJson); await NotificationsController.SendToPlayers(outsiderIds, hbJson); } return Ok(new StorefrontsDBClasses.SuccessResult()); } private static HashSet RecRoomPlusCheckWhitelisgt() { var path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "RRPlus.txt"); if (!System.IO.File.Exists(path)) return new HashSet(); var ids = new HashSet(); foreach (var line in System.IO.File.ReadAllLines(path)) { if (long.TryParse(line.Trim(), out var id)) ids.Add(id); } return ids; } [HttpGet("rooms/requiring/developer")] public async Task RoomsRequiringDeveloper() { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); return Ok(ServerConfig.Bracket); } [HttpGet("rooms/requiring/rrplus")] public async Task RoomsRequiringRRPlus() { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); return Ok(ServerConfig.Bracket); } [HttpPut("player/statusvisibility")] public IActionResult setStatusVisibilityShit() { return Ok(new { success = true }); } [HttpPut("player/photonregionpings")] public IActionResult setPhotonRegionShit() { return Ok(new { success = true }); } [HttpGet("room/{roomId}/instances")] // ripped from other code only for reference? i only needed the response data public async Task GetRoomInstances(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); var currentPlayer = PlayerDB.GetCurrentPlayer((long)id); bool isDeveloper = currentPlayer?.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool hasRoomModRole = room.CreatorAccountId == (long)id || (room.Roles?.Any(r => r.AccountId == (long)id && (int)r.Role >= (int)Role.Moderator) ?? false); if (!isDeveloper && !hasRoomModRole) return StatusCode(403, "Permission Denied"); var allPlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance != null && p.Player.PlayerExtra.Heartbeat.isOnline) .ToList(); var instanceGroups = allPlayers .Where(p => p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId) .GroupBy(p => p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId) .ToList(); if (!instanceGroups.Any()) return Ok(Array.Empty()); bool canViewAll = isDeveloper || room.CreatorAccountId == (long)id || (room.Roles?.Any(r => r.AccountId == (long)id && (int)r.Role >= (int)Role.Moderator) ?? false); var result = instanceGroups .Where(g => { var sample = g.First(); var instance = sample.Player.PlayerExtra.Heartbeat.roomInstance; return canViewAll || instance.isPrivate != true; }) .OrderByDescending(g => g.Count()) .Select(g => { var sample = g.First().Player.PlayerExtra.Heartbeat.roomInstance; return new Dictionary { ["RoomInstanceId"] = sample.roomInstanceId, ["RoomId"] = roomId, ["SubRoomId"] = sample.subRoomId, ["IsFull"] = Sessions.IsInstanceActuallyFull(sample.roomInstanceId), ["MaxCapacity"] = sample.maxCapacity, ["IsPrivate"] = sample.isPrivate, ["PlayerIds"] = g.Select(p => p.PlayerId).ToList() }; }) .ToList(); return Ok(result); } [HttpPost("player/exclusivelogin")] public async Task PlayerExclusiveLogin() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); await NotificationsController.SendPlayerOnline(id.Value, new MatchDBClasses.PlayerOnlineDTO() { SenderIsMyFavoriteFriend = true }); return Ok(); } [HttpPost("player/login")] public async Task PlayerLogin() { var id = AuthStuff.GetPlayerId(Request); if (id != null) { ClearRoomSession(id.Value); PlayerDB.UpdateLastLoginAt(id.Value); PlayerDB.UpdatePlayerHeartbeat(id.Value, null, online: true); var someRequiredSetings = new[] { ("Recroom.AccountCreation.HasStarted", "True"), ("Recroom.AccountCreation.HasChosenUsername", "False"), ("Recroom.AccountCreation.HasCreatedPassword", "True"), ("Recroom.AccountCreation.HasFinished", "False"), ("TUTORIAL_COMPLETE_MASK", "77") }; var existingSettings = PlayerDB.GetPlayerSettings(id.Value); foreach (var (key, value) in someRequiredSetings) { if (!existingSettings.Any(s => s.Key == key)) PlayerDB.SetPlayerSetting(key, value, id.Value); } } return Ok(); } [HttpPost("player/logout")] public async Task PlayerLogout() { var id = AuthStuff.GetPlayerId(Request); if (id != null) { ClearRoomSession(id.Value); PlayerDB.UpdatePlayerHeartbeat(id.Value, null, online: false); } return Ok(); } [HttpPost("player/heartbeat")] public async Task PostPlayerHeartbeat() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var playerLock = PlayerDB.GetPlayerLock(id.Value); await playerLock.WaitAsync(); FullPlayer player; Heartbeat hb; try { player = PlayerDB.Players.FindById(id.Value); if (player?.Player?.PlayerExtra == null) return Unauthorized(""); hb = player.Player.PlayerExtra.Heartbeat ?? new Heartbeat(); if (hb.roomInstance != null) { long claimedRoomId = hb.roomInstance.roomId; if (!IsRoomSessionAuthorized(id.Value, claimedRoomId)) { } } hb.playerId = id.Value; hb.isOnline = true; hb.errorCode = 0; hb.photonAuthToken = null; hb.appVersion = player.Player.PlayerExtra.Heartbeat?.appVersion ?? hb.appVersion; if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); if (form.TryGetValue("DeviceClass", out var dc) && int.TryParse(dc, out var dcVal)) hb.deviceClass = (DeviceClasses)dcVal; if (form.TryGetValue("StatusVisibility", out var sv) && int.TryParse(sv, out var svVal)) hb.statusVisibility = (StatusVisibility)svVal; if (form.TryGetValue("VRMovementMode", out var vrm) && int.TryParse(vrm, out var vrmVal)) hb.vrMovementMode = vrmVal; } player.Player.PlayerExtra.Heartbeat = hb; PlayerDB.Players.Update(player); await Services.ToxModService.EnsureCurrent(player); } finally { playerLock.Release(); } /*var (xpAwarded, leveledUp, newLevel, newXP) = PlayerDB.AwardHeartbeatXP(id.Value); if (xpAwarded) { var progressionPayload = Newtonsoft.Json.JsonConvert.SerializeObject( WebsocketEvents.CreateLevelUpdateResponse(id.Value, newLevel, newXP)); await NotificationsController.SendToPlayer(id.Value, progressionPayload); }*/ return Ok(hb); } [HttpPost("matchmake/none")] public async Task MatchmakeNone() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); ClearRoomSession(id.Value); var player = PlayerDB.GetCurrentPlayer(id.Value); if (player != null) EnsurePlayerDorm(player); var heartbeat = PlayerDB.GetPlayerHeartbeat(id.Value); if (heartbeat != null) await NotificationsController.RefreshHeartbeat(id.Value, heartbeat); return Ok(heartbeat); } [HttpPost("goto/room/{roomName}")] public async Task MatchmakeByRoomName(string roomName) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }); } if (roomName.Equals("DormRoom", StringComparison.OrdinalIgnoreCase)) { long dormRoomId = EnsurePlayerDorm(player); AuthorizeRoomSession(player.PlayerId, dormRoomId); RoomDB.IncrementRoomVisitCount(dormRoomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, dormRoomId); var dormHeartbeat = Sessions.CreateDorm(player.PlayerId, player.Player.Username, dormRoomId); if (PlayerDB.IsBanned(player.PlayerId)) dormHeartbeat.roomInstance.maxCapacity = 1; await NotificationsController.RefreshHeartbeat(player.PlayerId, dormHeartbeat); return Ok(dormHeartbeat); } var room = RoomDB.GetRoomByName(roomName); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId }); long roomId = room.RoomId; if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId }); bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId }); } var (joinMode, autoFollow, _, additionalPlayerIds) = await ParseMatchmakeForm(Request); RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, null, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, 0, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/dorm")] public async Task MatchmakeDorm() { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); long dormRoomId = EnsurePlayerDorm(player); AuthorizeRoomSession(player.PlayerId, dormRoomId); RoomDB.IncrementRoomVisitCount(dormRoomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, dormRoomId); var heartbeat = Sessions.CreateDorm(player.PlayerId, player.Player.Username, dormRoomId); if (PlayerDB.IsBanned(player.PlayerId)) heartbeat.roomInstance.maxCapacity = 1; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/event/{eventId}")] public async Task MatchmakeEvent(long eventId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }); } var ev = Vanadium.Classes.DBs.EventDB.GetPlayerEventById(eventId); if (ev == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId }); long roomId = ev.RoomId; long? subRoomId = ev.SubRoomId; var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId }); var (joinMode, autoFollow, _, additionalPlayerIds) = await ParseMatchmakeForm(Request); RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, subRoomId, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); if (heartbeat?.roomInstance != null) heartbeat.roomInstance.eventId = eventId; var fp = PlayerDB.Players.FindById(player.PlayerId); if (fp?.Player?.PlayerExtra != null) { fp.Player.PlayerExtra.Heartbeat = heartbeat; PlayerDB.Players.Update(fp); } await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/club/{clubId}")] public async Task MatchmakeToClub(long clubId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) return StatusCode(403); var targetClub = Classes.DBs.ClubsDB.GetClub(clubId); if (targetClub == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchClub, isOnline = true, playerId = player.PlayerId }); long roomId = targetClub.ClubhouseRoomId ?? 0; if (roomId <= 0) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.ClubHasNoClubhouse, isOnline = true, playerId = player.PlayerId }); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.ClubHasNoClubhouse, isOnline = true, playerId = player.PlayerId }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromClub, isOnline = true, playerId = player.PlayerId }); bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId }); } var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; var targetInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId) .Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!) .GroupBy(ri => ri.roomInstanceId) .Select(g => g.First()) .OrderByDescending(ri => Sessions.CountPlayersInInstance(ri.roomInstanceId)) .FirstOrDefault(); if (targetInstance != null) { if (currentInstanceHb?.roomInstance?.roomInstanceId == targetInstance.roomInstanceId) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId }); if (targetInstance.isFull || Sessions.CountPlayersInInstance(targetInstance.roomInstanceId) >= targetInstance.maxCapacity) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId }); RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); if (heartbeat == null) return StatusCode(500); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); return Ok(heartbeat); } RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var createdHeartbeat = Sessions.CreateRoom(player.PlayerId, roomId, joinMode: 0, additionalPlayersAutoFollow: false, roomNameOverride: targetClub.Name + " Clubhouse"); if (createdHeartbeat == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.ClubHasNoClubhouse, isOnline = true, playerId = player.PlayerId }); createdHeartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, createdHeartbeat); return Ok(createdHeartbeat); } private static async Task ShovePlayersToInstance(List playerIds, long roomId, long subRoomId, Heartbeat leaderHeartbeat) { foreach (var pid in playerIds) { var fp = PlayerDB.Players.FindById(pid); if (fp == null) continue; var hb = Sessions.CreateRoom(pid, roomId, subRoomId, joinMode: 0, additionalPlayersAutoFollow: false); if (leaderHeartbeat.roomInstance != null && hb.roomInstance != null) { hb.roomInstance.photonRoomId = leaderHeartbeat.roomInstance.photonRoomId; hb.roomInstance.photonRegion = leaderHeartbeat.roomInstance.photonRegion; hb.roomInstance.roomInstanceId = leaderHeartbeat.roomInstance.roomInstanceId; } fp.Player.PlayerExtra ??= new PlayerExtra(); fp.Player.PlayerExtra.Heartbeat = hb; PlayerDB.Players.Update(fp); await NotificationsController.RefreshHeartbeat(pid, hb); } } private static async Task<(int joinMode, bool autoFollow, bool bypassMovement, List additionalPlayerIds)> ParseMatchmakeForm(HttpRequest request) { int joinMode = 0; bool autoFollow = false; bool bypassMovement = false; var additionalPlayerIds = new List(); if (request.HasFormContentType) { var form = await request.ReadFormAsync(); if (form.TryGetValue("JoinMode", out var joinModeStr)) int.TryParse(joinModeStr, out joinMode); if (form.TryGetValue("AdditionalPlayersAutoFollow", out var autoFollowStr)) autoFollow = autoFollowStr.ToString().Equals("True", StringComparison.OrdinalIgnoreCase); if (form.TryGetValue("BypassMovementModeRestriction", out var bypassStr)) bypassMovement = bypassStr.ToString().Equals("True", StringComparison.OrdinalIgnoreCase); var additionalKey = form.Keys.FirstOrDefault(k => k.Equals("AdditionalPlayerIds", StringComparison.OrdinalIgnoreCase)); if (additionalKey != null) { foreach (var val in form[additionalKey]) { if (long.TryParse(val?.Trim(), out var pid)) additionalPlayerIds.Add(pid); } } } return (joinMode, autoFollow, bypassMovement, additionalPlayerIds); } [HttpPost("player/notifydisconnect")] public async Task NotifyDisconnect() { return Ok(); } [HttpPost("matchmake/instance/{roomInstanceId}")] public async Task MatchmakeInstance(long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }); } var targetInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (targetInstance == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId }); if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId }); var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId }); AuthorizeRoomSession(player.PlayerId, targetInstance.roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); var (_, autoFollowInstance, _, additionalPlayerIdsInstance) = await ParseMatchmakeForm(Request); if (additionalPlayerIdsInstance.Count > 0 && autoFollowInstance) await ShovePlayersToInstance(additionalPlayerIdsInstance, targetInstance.roomId, targetInstance.subRoomId, heartbeat); else if (additionalPlayerIdsInstance.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIdsInstance, targetInstance.roomId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/v2/instance/{roomInstanceId}")] public async Task MatchmakeInstanceV2(long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }}); } var targetInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (targetInstance == null) return Ok(new { success = false, error_id = "Matchmaking.NoSuchGame", error = "Instance not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId } }); if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity) return Ok(new { success = false, error_id = "Matchmaking.InsufficientSpace", error = "Instance is full.", value = new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId } }); var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId) return Ok(new { success = false, error_id = "Matchmaking.AlreadyInTargetInstance", error = "Already in this instance.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId } }); AuthorizeRoomSession(player.PlayerId, targetInstance.roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); var (_, autoFollowInstance, _, additionalPlayerIdsInstance) = await ParseMatchmakeForm(Request); if (additionalPlayerIdsInstance.Count > 0 && autoFollowInstance) await ShovePlayersToInstance(additionalPlayerIdsInstance, targetInstance.roomId, targetInstance.subRoomId, heartbeat); else if (additionalPlayerIdsInstance.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIdsInstance, targetInstance.roomId, heartbeat); return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat }); } [HttpPost("matchmake/room/{roomId}")] public async Task MatchmakeRoomRoomId(long roomId, [FromQuery] long? subRoomId = null) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; var bannedHeartbeat = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }; return Ok(bannedHeartbeat); } var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId }); if (AuthStuff.GetAndEnsureAppVersion(Request) != "20250724" && room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase))) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.UpdateRequired, isOnline = true, playerId = player.PlayerId }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId }); if (room != null) { bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId }); } } var currentHb = player.Player?.PlayerExtra?.Heartbeat; if (bypassMovement && joinMode == 0 && currentHb?.roomInstance != null && currentHb.roomInstance.roomId == roomId && !currentHb.roomInstance.isPrivate) { long currentInstanceId = currentHb.roomInstance.roomInstanceId; var otherInstance = PlayerDB.Players .FindAll() .Where(p => p.PlayerId != player.PlayerId && p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId && !p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate && p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId != currentInstanceId && Sessions.CountPlayersInInstance(p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId) < p.Player.PlayerExtra.Heartbeat.roomInstance.maxCapacity) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (otherInstance != null) { var hb = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, otherInstance); hb.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, hb); return Ok(hb); } joinMode = 1; } RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, subRoomId, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/v2/room/{roomId}")] public async Task MatchmakeRoomRoomIdV2(long roomId, [FromQuery] long? subRoomId = null) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }}); } var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new { success = false, error_id = "Matchmaking.NoSuchRoom", error = "Room not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId } }); if (AuthStuff.GetAndEnsureAppVersion(Request) != "20250724" && room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase))) return Ok(new { success = false, error_id = "Matchmaking.UpdateRequired", error = "Update required.", value = new Heartbeat { errorCode = MatchmakingErrorCode.UpdateRequired, isOnline = true, playerId = player.PlayerId } }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new { success = false, error_id = "Matchmaking.BannedFromRoom", error = "You are banned from this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId } }); if (room != null) { bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new { success = false, error_id = "Matchmaking.AccountDoesNotExist", error = "Room has no owner.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId } }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new { success = false, error_id = "Matchmaking.DeveloperOnly", error = "This room is developer only.", value = new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId } }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new { success = false, error_id = "Matchmaking.RoomIsPrivate", error = "This room is private.", value = new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId } }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new { success = false, error_id = "Matchmaking.JuniorNotAllowed", error = "Juniors are not allowed in this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId } }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new { success = false, error_id = "Matchmaking.LevelTooLow", error = "Your level is too low.", value = new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId } }); } } var currentHb = player.Player?.PlayerExtra?.Heartbeat; if (bypassMovement && joinMode == 0 && currentHb?.roomInstance != null && currentHb.roomInstance.roomId == roomId && !currentHb.roomInstance.isPrivate) { long currentInstanceId = currentHb.roomInstance.roomInstanceId; var otherInstance = PlayerDB.Players .FindAll() .Where(p => p.PlayerId != player.PlayerId && p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId && !p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate && p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId != currentInstanceId && Sessions.CountPlayersInInstance(p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId) < p.Player.PlayerExtra.Heartbeat.roomInstance.maxCapacity) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (otherInstance != null) { var hb = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, otherInstance); hb.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, hb); return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = hb }); } joinMode = 1; } RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, subRoomId, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat }); } [Route("player/avoidjuniors")] public IActionResult avoidjuniours() { return Ok(false); } [HttpPost("matchmake/room/{roomId}/{subRoomId}")] public async Task MatchmakeRoomWithSubRoom(long roomId, long subRoomId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; var bannedHeartbeat = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }; return Ok(bannedHeartbeat); } var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId }); if (room != null) { bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId }); } } RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, subRoomId, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/v2/room/{roomId}/{subRoomId}")] public async Task MatchmakeRoomWithSubRoomV2(long roomId, long subRoomId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }}); } var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new { success = false, error_id = "Matchmaking.NoSuchRoom", error = "Room not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId } }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new { success = false, error_id = "Matchmaking.BannedFromRoom", error = "You are banned from this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId } }); if (room != null) { bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new { success = false, error_id = "Matchmaking.AccountDoesNotExist", error = "Room has no owner.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId } }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new { success = false, error_id = "Matchmaking.DeveloperOnly", error = "This room is developer only.", value = new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId } }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new { success = false, error_id = "Matchmaking.RoomIsPrivate", error = "This room is private.", value = new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId } }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new { success = false, error_id = "Matchmaking.JuniorNotAllowed", error = "Juniors are not allowed in this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId } }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new { success = false, error_id = "Matchmaking.LevelTooLow", error = "Your level is too low.", value = new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId } }); } } RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = Sessions.CreateRoom( player.PlayerId, roomId, subRoomId, joinMode: joinMode, additionalPlayersAutoFollow: autoFollow); await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); if (additionalPlayerIds.Count > 0 && autoFollow) await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId, heartbeat); else if (additionalPlayerIds.Count > 0) await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat); return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat }); } /* 20230616 & 20250724.01 photons */ private static readonly string[] _fallbackRealtime = new[] { "616d6ba8-edb1-47f8-a0de-31c8ce426685", "a7c732cd-5cea-4839-b0b3-ba8e9af14b06", "90d94f73-9797-409d-84ab-ed3992177612" }; private static readonly string[] _fallbackVoice = new[] { "b3a7f09a-b167-4ba1-87bb-2603da5a39be", "7e71f40c-d1fa-4e48-8851-981402e630e5", "3e010413-c8c8-48df-95cf-bf7f2bf430e6" }; [HttpGet("player/connection-info")] public async Task GetPlayerConnectionInfo([FromQuery] long? roomInstanceId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (roomInstanceId == null || roomInstanceId <= 0) { return Ok(new { success = true, error = (string?)null, value = new { PhotonRealtimeAppId = _fallbackRealtime[_photonRng.Next(_fallbackRealtime.Length)], PhotonChatAppId, PhotonVoiceAppId = _fallbackVoice[_photonRng.Next(_fallbackVoice.Length)], experiments = new { shouldUseGameServerNetworking = false } } }); } var dbEntry = InstanceDB.Get(roomInstanceId.Value); if (dbEntry == null) { var liveInstance = PlayerDB.Players.FindAll() .Select(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance) .FirstOrDefault(ri => ri?.roomInstanceId == roomInstanceId.Value); if (liveInstance != null) { dbEntry = InstanceDB.Upsert(liveInstance, liveInstance.isPrivate); } } if (dbEntry == null) { return Ok(new { success = true, error = (string?)null, value = new { PhotonRealtimeAppId = _fallbackRealtime[_photonRng.Next(_fallbackRealtime.Length)], PhotonChatAppId, PhotonVoiceAppId = _fallbackVoice[_photonRng.Next(_fallbackVoice.Length)], experiments = new { shouldUseGameServerNetworking = false } } }); } if (string.IsNullOrEmpty(dbEntry.PhotonRealtimeAppId) || string.IsNullOrEmpty(dbEntry.PhotonVoiceAppId)) { var (rt, vo) = InstanceDB.AssignPhotonIds(roomInstanceId.Value); dbEntry.PhotonRealtimeAppId = rt; dbEntry.PhotonVoiceAppId = vo; } var liveInst = PlayerDB.Players.FindAll() .Select(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance) .FirstOrDefault(ri => ri?.roomInstanceId == roomInstanceId.Value); return Ok(new { success = true, error = (string?)null, value = new { PhotonAuthToken = "", PhotonRealtimeAppId = dbEntry.PhotonRealtimeAppId, PhotonChatAppId, PhotonVoiceAppId = dbEntry.PhotonVoiceAppId, PhotonRegion = liveInst?.photonRegion ?? dbEntry.PhotonRegion ?? "us", PhotonRoomId = liveInst?.photonRoomId ?? dbEntry.PhotonRoomId, VoiceConnectionInfo = (string?)null, VoiceServerId = (string?)null, experiments = new { shouldUseGameServerNetworking = false } } }); } [HttpPost("invite")] public async Task Invite([FromForm] long playerId, [FromForm] long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (playerId <= 0 || roomInstanceId <= 0) return BadRequest(); var targetPlayer = PlayerDB.Players.FindById(playerId); if (targetPlayer == null) return NotFound(""); var rel = FriendsDB.GetRelationships(player.PlayerId).FirstOrDefault(r => r.OtherPlayerID == playerId); if (rel?.RelationshipType != FriendsDBClasses.RelationshipType.Friend) return BadRequest(); var heartbeat = PlayerDB.GetPlayerHeartbeat(player.PlayerId); if (heartbeat?.roomInstance == null || heartbeat.roomInstance.roomInstanceId != roomInstanceId) return BadRequest(); var key = (player.PlayerId, playerId); var now = DateTime.UtcNow; _inviteRateLimit.AddOrUpdate(key, _ => (1, now), (_, e) => (now - e.Window).TotalSeconds >= 15 ? (1, now) : (e.Count + 1, e.Window)); if (_inviteRateLimit.TryGetValue(key, out var rate) && rate.Count > 3 && (now - rate.Window).TotalSeconds < 15) return BadRequest(); var invite = new MatchDBClasses.InviteDTO { InviteId = roomInstanceId, Name = heartbeat.roomInstance.Name ?? "Unknown Room", InviteMode = 0 }; await NotificationsController.SendInvite(playerId, player.PlayerId, heartbeat.roomInstance.roomId, invite); return Ok(); } //[HttpPost("matchmake/instance/{roomInstanceId}")] // added back because some bitch deleted it [HttpPost("matchmake/invite/{roomInstanceId}")] // ez //[HttpPost("matchmake/v2/instance/{roomInstanceId}")] public async Task MatchmakeToInviteInstance(long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }); } var targetInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (targetInstance == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId }); if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId }); var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId }); AuthorizeRoomSession(player.PlayerId, targetInstance.roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); return Ok(heartbeat); } [HttpPost("matchmake/v2/invite/{roomInstanceId}")] public async Task MatchmakeToInviteInstanceV2(long roomInstanceId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }}); } var targetInstance = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId) .Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance) .FirstOrDefault(); if (targetInstance == null) return Ok(new { success = false, error_id = "Matchmaking.NoSuchGame", error = "Instance not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId } }); if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity) return Ok(new { success = false, error_id = "Matchmaking.InsufficientSpace", error = "Instance is full.", value = new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId } }); var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId) return Ok(new { success = false, error_id = "Matchmaking.AlreadyInTargetInstance", error = "Already in this instance.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId } }); AuthorizeRoomSession(player.PlayerId, targetInstance.roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat }); } [HttpPost("matchmake/player/{targetPlayerId}")] public async Task MatchmakeToPlayer(long targetPlayerId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); if (PlayerDB.IsBanned(player.PlayerId)) { var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat; return Ok(new Heartbeat { appVersion = AuthStuff.GetAndEnsureAppVersion(Request), deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown, errorCode = MatchmakingErrorCode.Banned, isOnline = previousHeartbeat?.isOnline ?? true, playerId = player.PlayerId, roomInstance = null, statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online, vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0, photonAuthToken = null, photonRealtimeAppId = null, photonVoiceAppId = null, photonChatAppId = null, photonRegion = null, photonRoomId = null, voiceConnectionInfo = null, voiceServerId = null, experiments = null }); } var targetPlayer = PlayerDB.Players.FindById(targetPlayerId); if (targetPlayer == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); var targetHb = targetPlayer.Player?.PlayerExtra?.Heartbeat; if (targetHb == null || !targetHb.isOnline || targetHb.roomInstance == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.PlayerNotOnline, isOnline = true, playerId = player.PlayerId }); var targetInstance = targetHb.roomInstance; long roomId = targetInstance.roomId; long roomInstanceId = targetInstance.roomInstanceId; var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId }); if (IsBannedFromRoom(room, player.PlayerId)) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId }); bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false; bool isCreator = room.CreatorAccountId == player.PlayerId; bool hasAccess = room.Roles?.Any(r => r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false; bool isOwnerOrCoOwner = isCreator || hasAccess; bool hasOwner = room.CreatorAccountId > 0 || (room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false); if (!hasOwner) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId }); if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted) { if (!isDeveloper && !isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId }); } else if (room.Accessibility == RoomAccessibility.Private) { if (!isCreator && !hasAccess) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId }); } if (!isOwnerOrCoOwner) { if (player.Player?.IsJunior == true && room.SupportsJuniors == false) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId }); if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId }); } if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId }); var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat; if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId) return Ok(new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId }); RoomDB.IncrementRoomVisitCount(roomId); PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId); AuthorizeRoomSession(player.PlayerId, roomId); var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance); heartbeat.playerId = player.PlayerId; await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat); return Ok(heartbeat); } [HttpDelete("invite/{inviteId}")] public async Task DeclineInvite(long inviteId) { var player = AuthStuff.GetCurrentPlayer(Request); if (player == null) return Unauthorized(""); var messages = PlayerDB.GetMessages(player.PlayerId); var invite = messages.FirstOrDefault(m => m.Id == inviteId); var invitedata = new MatchDBClasses.InviteDTO { InviteId = inviteId, Name = "Unknown Room", InviteMode = 0 }; if (invite != null) { var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = player.PlayerId, Id = PlayerDB.GenerateRandomMessageId(), SentTime = DateTime.UtcNow, Type = MessageType.GameInviteDeclined, Data = invitedata, RoomId = invite.RoomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(invite.FromPlayerId, json); PlayerDB.DeleteMessage(player.PlayerId, inviteId); } return Ok(); } } }