using System.Text.Json.Nodes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Config; using RadicadeServer.API.Notifications; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Auth; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Rooms; using RadicadeServer.RecNet.Matchmaking; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Notifications; using RadicadeServer.RecNet.Responses; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; namespace RadicadeServer.API.Matchmaking { [Route("matchmaking")] public class MatchmakingAPI : Controller { private readonly RecDBContext db; public MatchmakingAPI(RecDBContext context) { db = context; } [HttpGet("rooms/requiring/developer")] [HttpGet("rooms/requiring/rrplus")] public IActionResult GetRestrictedRooms() => Ok(Array.Empty()); [HttpPost("roominstance/{instanceId}/reportjoinresult"), Authorize] public async Task ReportJoinResult(long instanceId) { var userId = User.GetUserId(); if (userId.HasValue) { var roomInstance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); if (roomInstance != null) { await RoomJoinGiftUtils.TryDeliverAsync(db, userId.Value, roomInstance.roomId); } await StorefrontShared.SendPendingGiftBoxNotificationAsync(db, userId.Value); } return Ok(); } [HttpPost("matchmake/none"), Authorize] public async Task MatchmakeNone() { long? userId = User.GetUserId(); if (!userId.HasValue) { return Unauthorized(); } var match = await MatchmakingUtils.Matchmake( JoinMode.PrivateNewInstance, "none", userId.Value); return Ok(match); } [HttpPost("matchmake/dorm"), Authorize] public async Task MatchmakeDorm() { long? userId = User.GetUserId(); if (!userId.HasValue) { return Unauthorized(); } try { var presence = await db.Presences .FirstOrDefaultAsync(x => x.playerId == userId.Value); if (presence == null) { presence = new RealPresence { playerId = userId.Value }; db.Presences.Add(presence); } var orientationInstance = await MatchmakingUtils.TryStartOrientationAsync(db, userId.Value, presence); if (orientationInstance != null) { await Notify.UpdateSubscriptions(userId.Value); return Ok(new MatchResponse { roomInstance = orientationInstance, errorCode = JoinStatus.Success }); } var dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, presence); if (dormInstance == null) { var reloaded = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId.Value); if (reloaded != null) { reloaded.roomInstance = null; await db.SaveChangesAsync(); dormInstance = await MatchmakingUtils.EnsureDormPresenceAsync(db, userId.Value, reloaded); } } if (dormInstance == null) { return NotFound(new MatchResponse { errorCode = JoinStatus.BlockedFromRoom }); } await Notify.UpdateSubscriptions(userId.Value); return Ok(new MatchResponse { roomInstance = dormInstance, errorCode = JoinStatus.Success }); } catch (Exception) { return StatusCode(StatusCodes.Status500InternalServerError, new MatchResponse { errorCode = JoinStatus.BlockedFromRoom }); } } private async Task ResolvePresenceRoomInstanceAsync(RealPresence data) { if (data.roomInstance is not > 0) return null; var roomInstance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(x => x.roomInstanceId == data.roomInstance.Value); return roomInstance == null ? null : MatchmakingUtils.NormalizeRoomInstance(roomInstance); } [HttpGet("player")] public async Task> GetPlayerPresence( [FromQuery] List id) { var presences = new List(); if (id == null || id.Count == 0) { long? currentPlayerId = User.GetUserId(); if (!currentPlayerId.HasValue) { return presences; } id = new List { currentPlayerId.Value }; } foreach (long playerId in id) { RealPresence? data = await db.Presences .FirstOrDefaultAsync( x => x.playerId == playerId ); if (data == null) { continue; } long? viewerId = User.GetUserId(); RoomInstance? roomInstance = await ResolvePresenceRoomInstanceAsync(data); if (viewerId != playerId) roomInstance = await RoomUtils.ForViewerAsync(roomInstance, viewerId); presences.Add( (await Presence.CreateAsync(db, data, roomInstance )).ForViewer(viewerId) ); } return presences; } [HttpGet("room/{roomId}/instances"), Authorize] public async Task?> GetRoomInstancesInRoom(long roomId) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return new List(); } var roomInstances = await db.RoomInstances .Where(x => x.roomId == roomId) .ToListAsync(); var converted = new List(); foreach (RoomInstance instance in roomInstances) { var players = await db.Presences .Where( x => x.roomInstance == instance.roomInstanceId ) .Select(x => x.playerId) .ToListAsync(); var summary = new Dictionary { ["roomInstanceId"] = instance.roomInstanceId, ["roomId"] = instance.roomId, ["subRoomId"] = instance.subRoomId, ["isFull"] = instance.isFull, ["playerIds"] = players, ["createdAt"] = DateTime.UtcNow }; converted.Add(summary); } return converted; } [HttpGet("clubhousesearch/mostactivenow"), Authorize] public async Task GetMostActiveClubhouses() { var instances = await db.RoomInstances .Where(x => x.clubId != null && x.clubId > 0) .ToListAsync(); var byClub = instances.GroupBy(x => x.clubId!.Value); var summaries = new List(); foreach (var group in byClub) { var playerCount = 0; foreach (var instance in group) { playerCount += await db.Presences .CountAsync(x => x.roomInstance == instance.roomInstanceId); } summaries.Add(new Dictionary { ["clubId"] = group.Key, ["playerCount"] = playerCount, ["roomId"] = group.First().roomId }); } return Ok(summaries.ToArray()); } [HttpPost("goto/instance/{id}"), Authorize] public async Task GoToRoomInstance( long id, [FromForm] long[]? AdditionalPlayerIds) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return MatchResponse.Error(JoinStatus.Banned); } long userId = nullableUserId.Value; RoomInstance? roomInstance = await db.RoomInstances.FirstOrDefaultAsync( x => x.roomInstanceId == id ); if (roomInstance == null) { return MatchResponse.Error(JoinStatus.RoomIsNotActive); } var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId; if (!isOwnDorm && await db.IsBanned(userId)) { return MatchResponse.Error(JoinStatus.Banned); } if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) { return MatchResponse.Error(JoinStatus.BannedFromRoom); } var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( db, userId, isOwnDorm, $"instance {id} (room {roomInstance.roomId})"); if (warningBounce != null) return warningBounce; if (roomInstance.isFull) { return MatchResponse.Error(JoinStatus.InsufficientSpace); } if (roomInstance.isPrivate || room.Accessibility == Accessibility.Private) { var hasGameInvite = await HasActiveGameInviteAsync(userId, roomInstance); var canManageRoom = await MatchmakingUtils.IsAtLeastRole(userId, roomInstance.roomId, RoleType.CoOwner); if (!hasGameInvite && !canManageRoom && room.CreatorAccountId != userId) { return MatchResponse.Error(roomInstance.isPrivate ? JoinStatus.RoomInstanceIsPrivate : JoinStatus.RoomIsPrivate); } } RealPresence? presence = await db.Presences .FirstOrDefaultAsync( x => x.playerId == userId ); if (presence == null) { return MatchResponse.Error(JoinStatus.Banned); } presence.roomInstance = id; presence.isOnline = true; presence.errorCode = JoinStatus.Success; await MatchmakingUtils.RecordRoomVisit(db, userId, roomInstance.roomId); await RoomJoinGiftUtils.TryDeliverAsync(db, userId, roomInstance.roomId); MatchmakingUtils.MarkInRoom(userId); await db.SaveChangesAsync(); if (AdditionalPlayerIds != null && AdditionalPlayerIds.Length > 0) { await InvitePlayers( AdditionalPlayerIds, roomInstance.roomInstanceId ); } await Notify.UpdateSubscriptions(userId); return new MatchResponse { roomInstance = roomInstance, errorCode = JoinStatus.Success }; } [HttpPost("matchmake/instance/{instanceId}"), Authorize] public async Task MatchmakeInstance( long instanceId, [FromForm] GoToRequest? goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) return null; long userId = nullableUserId.Value; RoomInstance? roomInstance = await db.RoomInstances .FirstOrDefaultAsync(x => x.roomInstanceId == instanceId); if (roomInstance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); RoomData? room = await db.Rooms .FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); bool isOwnDorm = room.IsDorm && room.CreatorAccountId == userId; if (!isOwnDorm) { if (await db.IsBanned(userId)) return MatchResponse.Error(JoinStatus.Banned); bool managesRoom = await MatchmakingUtils.IsAtLeastRole( userId, room.RoomId, RoleType.CoOwner ); if (!managesRoom) return MatchResponse.Error(JoinStatus.NoSuchRoom); } if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) { return MatchResponse.Error(JoinStatus.BannedFromRoom); } MatchResponse match = await MatchmakingUtils.JoinRoom(userId, instanceId); await UpdatePresence(userId, match); if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId); } return match; } [HttpPost("goto/club/{clubId}"), Authorize] [HttpPost("matchmake/club/{clubId}"), Authorize] public async Task GoToClub(long clubId, [FromForm] GoToRequest? goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) return null; long userId = nullableUserId.Value; var club = await Clubs.ClubStore.GetAsync(db, clubId); if (club == null) { return new MatchResponse { errorCode = JoinStatus.RoomIsNotActive, roomInstance = null }; } var instance = await db.RoomInstances .Where(x => x.clubId == clubId) .OrderBy(x => x.roomInstanceId) .FirstOrDefaultAsync(); if (instance == null) { var clubhouseRoom = await db.Rooms .Where(x => x.CreatorAccountId == club.ownerPlayerId) .OrderByDescending(x => x.PublishedAt) .FirstOrDefaultAsync(); if (clubhouseRoom == null) { return new MatchResponse { errorCode = JoinStatus.RoomIsNotActive, roomInstance = null }; } instance = await MatchmakingUtils.CreateRoomInstance(clubhouseRoom.RoomId, false, null, clubId, null); } var match = await MatchmakingUtils.JoinRoom(userId, instance!.roomInstanceId); await UpdatePresence(userId, match); if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers(goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId); } return match; } [HttpPost("goto/event/{eventId}"), Authorize] [HttpPost("matchmake/event/{eventId}"), Authorize] public async Task GoToEvent( long eventId, [FromForm] GoToRequest? goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return null; } long userId = nullableUserId.Value; MatchResponse match = await MatchmakingUtils.MatchmakeEvent( userId, eventId, goTo?.JoinMode ?? JoinMode.PublicMatchmaking ); await UpdatePresence(userId, match); if (goTo?.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers( goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId ); } return match; } [HttpPost("matchmake/room/{roomId}"), Authorize] public async Task MatchmakeRoomById( long roomId, [FromForm] GoToRequest goTo, [FromQuery] long? subRoomId = null) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) return null; long userId = nullableUserId.Value; var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); MatchResponse match = await MatchmakingUtils.Matchmake( goTo.JoinMode, room.Name, userId, subRoomId); await UpdatePresence(userId, match); if (goTo.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers( goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId ); } return match; } [HttpPost("matchmake/room/{roomId}/{subRoomId}"), Authorize] public async Task MatchmakeRoomWithSubRoom( long roomId, long subRoomId, [FromForm] GoToRequest goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) return null; long userId = nullableUserId.Value; var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return MatchResponse.Error(JoinStatus.RoomIsNotActive); MatchResponse match = await MatchmakingUtils.Matchmake( goTo.JoinMode, room.Name, userId, subRoomId); await UpdatePresence(userId, match); if (goTo.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers( goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId ); } return match; } [HttpPost("goto/room/{roomName}/{subRoomName}"), Authorize] public async Task GoToSubRoom( string roomName, string subRoomName, [FromForm] GoToRequest goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return null; } long userId = nullableUserId.Value; roomName = Uri.UnescapeDataString(roomName) .Replace("+", " "); subRoomName = Uri.UnescapeDataString(subRoomName) .Replace("+", " "); RoomData? room = await db.Rooms.FirstOrDefaultAsync( x => x.Name == roomName ); if (room == null) { return new MatchResponse { errorCode = JoinStatus.RoomIsNotActive, roomInstance = null }; } SubRoom? subRoom = await db.SubRooms .FirstOrDefaultAsync( x => x.RoomId == room.RoomId && x.Name == subRoomName ); if (subRoom == null) { return new MatchResponse { errorCode = JoinStatus.RoomIsNotActive, roomInstance = null }; } MatchResponse match = await MatchmakingUtils.Matchmake( goTo.JoinMode, roomName, userId, subRoom.SubRoomId ); await UpdatePresence(userId, match); if (goTo.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers( goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId ); } return match; } [HttpPost("goto/{name}"), Authorize] public async Task GoTo( string name, [FromForm] GoToRequest goTo) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return null; } long userId = nullableUserId.Value; name = Uri.UnescapeDataString(name) .Replace("+", " "); MatchResponse match = await MatchmakingUtils.Matchmake( goTo.JoinMode, name, userId ); await UpdatePresence(userId, match); if (goTo.AdditionalPlayerIds != null && match.roomInstance != null) { await InvitePlayers( goTo.AdditionalPlayerIds, match.roomInstance.roomInstanceId ); } return match; } [HttpPost("goto/player/{targetId}"), Authorize] public async Task GoToPlayer(long targetId, [FromForm] GoToRequest? goTo) { var userId = User.GetUserId(); return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo); } [HttpPost("matchmake/player/{targetId:long}"), Authorize] public async Task MatchmakePlayer(long targetId, [FromForm] GoToRequest? goTo) { var userId = User.GetUserId(); return userId == null ? null : await FollowPlayerIntoRoom(userId.Value, targetId, goTo); } private async Task FollowPlayerIntoRoom(long userId, long targetId, GoToRequest? goTo) { if (targetId <= 0 || targetId == userId || !await db.Relationships.AsNoTracking().AnyAsync(relationship => ((relationship.AccountId == userId && relationship.PlayerId == targetId) || (relationship.AccountId == targetId && relationship.PlayerId == userId)) && relationship.RelationshipType == RadicadeServer.RecNet.Relationships.RelationshipType.Friend)) return MatchResponse.Error(JoinStatus.NoSuchRoom); var targetPresence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(presence => presence.playerId == targetId); if (targetPresence?.roomInstance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); var instance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == targetPresence.roomInstance.Value); if (instance == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId); if (room == null) return MatchResponse.Error(JoinStatus.NoSuchRoom); if (await MatchmakingUtils.IsBannedFromRoom(db, userId, room.RoomId)) return MatchResponse.Error(JoinStatus.BannedFromRoom); var presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId); if (presence == null) { presence = new RealPresence { playerId = userId }; db.Presences.Add(presence); } presence.roomInstance = instance.roomInstanceId; presence.isOnline = true; presence.errorCode = JoinStatus.Success; await db.SaveChangesAsync(); var match = new MatchResponse { roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance), errorCode = JoinStatus.Success }; await UpdatePresence(userId, match); if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds) await InvitePlayers(additionalPlayerIds, instance.roomInstanceId); return match; } [HttpPost("invite"), Authorize] [HttpPost("invite/{playerId:long}"), Authorize] public async Task Invite() { long? callerId = User.GetUserId(); var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync(); long instanceId = sentInstanceId; if (instanceId <= 0 && callerId.HasValue) { var presence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(x => x.playerId == callerId.Value); instanceId = presence?.roomInstance ?? 0; } if (targetId <= 0) return InviteRefused("No invited player was named."); if (instanceId <= 0) return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to."); var result = await InvitePlayer(targetId, instanceId); return result; } [HttpPost("~/api/offlineinvite/v1/send"), Authorize] public async Task SendOfflineInvite() { long? callerId = User.GetUserId(); var (targetId, sentInstanceId, seen) = await ReadInviteFieldsAsync(); long instanceId = sentInstanceId; if (instanceId <= 0 && callerId.HasValue) { var presence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(x => x.playerId == callerId.Value); instanceId = presence?.roomInstance ?? 0; } if (targetId <= 0) return InviteRefused("No invited player was named."); if (instanceId <= 0) return InviteRefused("The inviter is not in a room, so there is nothing to invite anyone to."); var result = await InvitePlayer(targetId, instanceId); return result; } private Success InviteRefused(string why) { return Success.Error(why); } [HttpPost("matchmake/invite/{inviteId:long}"), Authorize] public async Task AcceptInviteByMessageId( long inviteId, [FromForm] GoToRequest? goTo) { long? callerId = User.GetUserId(); if (!callerId.HasValue) return null; long userId = callerId.Value; Message? invite = await db.Messages.AsNoTracking() .Where(message => message.Id == inviteId && message.ToPlayerId == userId && (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2)) .OrderByDescending(message => message.Id) .FirstOrDefaultAsync(); if (invite == null) return RefuseInviteAccept( $"invite {inviteId} is not addressed to account {userId}", JoinStatus.RoomInviteExpired); if (invite.FromPlayerId == userId) return RefuseInviteAccept( $"invite {inviteId} was sent by the caller", JoinStatus.ChatMessageNotAnInvite); var invitedInstanceId = RoomInviteDetails.GetInstanceId(invite); if (invitedInstanceId == null) return RefuseInviteAccept( $"invite {inviteId} names no room instance", JoinStatus.RoomInviteExpired); RealPresence? inviterPresence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(presence => presence.playerId == invite.FromPlayerId); if (inviterPresence == null || !inviterPresence.isOnline) return RefuseInviteAccept( $"account {invite.FromPlayerId}, who sent invite {inviteId}, is not online", JoinStatus.PlayerNotOnline); if (inviterPresence.roomInstance != invitedInstanceId) return RefuseInviteAccept( $"invite {inviteId} names instance {invitedInstanceId} but its sender " + $"{invite.FromPlayerId} is now in {inviterPresence.roomInstance?.ToString() ?? "no instance"}", JoinStatus.RoomInviteExpired); RoomInstance? instance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == invitedInstanceId); if (instance == null) return RefuseInviteAccept( $"instance {invitedInstanceId} from invite {inviteId} is not live", JoinStatus.RoomIsNotActive); return await AcceptInviteIntoInstanceAsync( userId, invite.FromPlayerId, instance, goTo, $"invite {inviteId}"); } [HttpPost("matchmake/chatinvite/{threadId:long}/{messageId:long}"), Authorize] public async Task MatchmakeChatInvite( long threadId, long messageId, [FromForm] GoToRequest? goTo) { long? callerId = User.GetUserId(); if (!callerId.HasValue) return null; long userId = callerId.Value; ChatThread? thread = await db.Chats.FirstOrDefaultAsync(x => x.chatThreadId == threadId); var members = thread?.GetPlayerIds(); if (thread == null || members == null || !members.Contains(userId)) return RefuseInviteAccept( $"chat invite {messageId}: thread {threadId} is not a conversation account {userId} is part of", JoinStatus.ChatPartyInviteNotFound); ChatMessage? message = thread.GetMessages()?.FirstOrDefault(x => x.chatMessageId == messageId); long inviterId; string via; if (message != null) { if (message.senderPlayerId == userId) return RefuseInviteAccept( $"chat message {messageId} in thread {threadId} was written by the caller, so it is not an invite", JoinStatus.ChatMessageNotAnInvite); if (DateTime.UtcNow - message.timeSent > TimeSpan.FromMinutes(PlayerSettingsAPI.InviteLinkLifetimeInMinutes)) return RefuseInviteAccept( $"chat invite {messageId} in thread {threadId} is older than " + $"{PlayerSettingsAPI.InviteLinkLifetimeInMinutes} minutes", JoinStatus.RoomInviteExpired); inviterId = message.senderPlayerId; via = $"chat invite {messageId} in thread {threadId}"; } else { var others = members.Where(member => member != userId).Distinct().ToList(); if (others.Count != 1) return RefuseInviteAccept( $"chat invite {messageId} in thread {threadId} is not stored and the thread " + $"has {others.Count} other member(s) to resolve an inviter from", JoinStatus.ChatPartyInviteNotFound); inviterId = others[0]; via = $"thread {threadId} member {inviterId} (message {messageId} is not stored)"; } RealPresence? inviterPresence = await db.Presences.AsNoTracking() .FirstOrDefaultAsync(presence => presence.playerId == inviterId); if (inviterPresence == null || !inviterPresence.isOnline || inviterPresence.roomInstance is null or <= 0) return RefuseInviteAccept( $"{via}: inviter {inviterId} is not in a room instance", JoinStatus.PlayerNotOnline); RoomInstance? instance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(roomInstance => roomInstance.roomInstanceId == inviterPresence.roomInstance.Value); if (instance == null) return RefuseInviteAccept( $"{via}: inviter {inviterId} is in instance {inviterPresence.roomInstance.Value}, which is not live", JoinStatus.RoomIsNotActive); return await AcceptInviteIntoInstanceAsync(userId, inviterId, instance, goTo, via); } private async Task AcceptInviteIntoInstanceAsync( long userId, long inviterId, RoomInstance instance, GoToRequest? goTo, string via) { if (await db.IsBanned(userId)) return RefuseInviteAccept($"{via}: account {userId} is banned", JoinStatus.Banned); if (await MatchmakingUtils.IsBannedFromRoom(db, userId, instance.roomId)) return RefuseInviteAccept( $"{via}: account {userId} is banned from room {instance.roomId}", JoinStatus.BannedFromRoom); RoomData? room = await db.Rooms.AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == instance.roomId); if (room == null) return RefuseInviteAccept($"{via}: room {instance.roomId} is gone", JoinStatus.RoomIsNotActive); var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( db, userId, room.IsDorm && room.CreatorAccountId == userId, $"instance {instance.roomInstanceId} (room {instance.roomId})"); if (warningBounce != null) return warningBounce; RealPresence? presence = await db.Presences.FirstOrDefaultAsync(row => row.playerId == userId); if (presence == null) { presence = new RealPresence { playerId = userId }; db.Presences.Add(presence); } presence.roomInstance = instance.roomInstanceId; presence.isOnline = true; presence.errorCode = JoinStatus.Success; await db.SaveChangesAsync(); var match = new MatchResponse { roomInstance = MatchmakingUtils.NormalizeRoomInstance(instance), errorCode = JoinStatus.Success }; await UpdatePresence(userId, match); if (goTo?.AdditionalPlayerIds is { Length: > 0 } additionalPlayerIds) await InvitePlayers(additionalPlayerIds, instance.roomInstanceId); return match; } private MatchResponse RefuseInviteAccept(string why, JoinStatus status) { return MatchResponse.Error(status); } private static readonly string[] InviteTargetKeys = { "playerId", "PlayerId", "playerIds", "PlayerIds", "id", "Id", "invitedPlayerId", "InvitedPlayerId", "invitedPlayerIds", "InvitedPlayerIds", "invitedPlayerIds[]", "InvitedPlayerIds[]", "targetPlayerId", "TargetPlayerId", "accountId", "AccountId", }; private static readonly string[] InviteInstanceKeys = { "roomInstanceId", "RoomInstanceId", "instanceId", "InstanceId", "roomInstance", "RoomInstance", }; private async Task<(long PlayerId, long RoomInstanceId, string Seen)> ReadInviteFieldsAsync() { long playerId = 0; long roomInstanceId = 0; var seen = new List(); void Collect(string key, string? value) { if (string.IsNullOrWhiteSpace(value)) return; seen.Add($"{key}={value}"); if (InviteTargetKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) { foreach (var part in value.Split(new[] { ',', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) if (playerId <= 0 && long.TryParse(part, out var parsed) && parsed > 0) playerId = parsed; } else if (InviteInstanceKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) { if (roomInstanceId <= 0 && long.TryParse(value.Trim(), out var parsed) && parsed > 0) roomInstanceId = parsed; } } foreach (var pair in Request.RouteValues) if (pair.Value != null) Collect(pair.Key, pair.Value.ToString()); if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var field in form) foreach (var value in field.Value) Collect(field.Key, value); } else if (Request.ContentLength is > 0) { try { Request.EnableBuffering(); if (await JsonNode.ParseAsync(Request.Body) is JsonObject body) foreach (var pair in body) { if (pair.Value is JsonArray array) foreach (var item in array) Collect(pair.Key, item?.ToString()); else Collect(pair.Key, pair.Value?.ToString()); } Request.Body.Position = 0; } catch { try { Request.Body.Position = 0; } catch { } } } foreach (var key in InviteTargetKeys.Concat(InviteInstanceKeys)) foreach (var value in Request.Query[key]) Collect(key, value); return (playerId, roomInstanceId, seen.Count == 0 ? "(none)" : string.Join("; ", seen)); } public async Task UpdatePresence( long userId, MatchResponse match) { RealPresence? realPresence = await db.Presences .FirstOrDefaultAsync( x => x.playerId == userId ); if (realPresence == null) { return new Success( false, "Presence does not exist.", null ); } if (match.errorCode == JoinStatus.Success && match.roomInstance != null) { RoomInstance roomInstance = match.roomInstance; roomInstance.eventId ??= 0; roomInstance.clubId ??= 0; roomInstance.roomCode ??= string.Empty; roomInstance.location ??= string.Empty; roomInstance.name ??= string.Empty; roomInstance.photonRegion ??= string.Empty; roomInstance.photonRegionId ??= string.Empty; roomInstance.photonRoomId ??= string.Empty; roomInstance.dataBlob ??= string.Empty; realPresence.roomInstance = roomInstance.roomInstanceId; realPresence.isOnline = true; realPresence.errorCode = JoinStatus.Success; MatchmakingUtils.MarkInRoom(userId); await db.SaveChangesAsync(); } else { realPresence.errorCode = match.errorCode; await db.SaveChangesAsync(); } var presence = await Presence.CreateAsync(db, realPresence, match.roomInstance ); var subscriberUpdate = Notify.UpdateSubscriptions(userId); try { await Notify.SendNotify( userId, new Notification { Id = "PresenceUpdate", Msg = presence } ); } catch (Exception) { } try { await subscriberUpdate; } catch (Exception) { } return new Success(true, null, null); } public async Task InvitePlayers( long[] players, long roomInstanceId) { try { foreach (long playerId in players) { await InvitePlayer( playerId, roomInstanceId ); } return new Success(true, null, null); } catch (Exception) { return new Success( false, "Could not invite additional player ids.", null ); } } public async Task InvitePlayer( long playerId, long roomInstanceId) { long? nullableUserId = User.GetUserId(); if (!nullableUserId.HasValue) { return Success.Error("Missing user ID."); } long userId = nullableUserId.Value; if (playerId <= 0 || playerId == userId) return Success.Error("Invalid player ID."); RoomInstance? roomInstance = await db.RoomInstances .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId); if (roomInstance == null) return Success.Error("Room instance not found."); Profile? target = await db.Profiles .AsNoTracking() .FirstOrDefaultAsync(x => x.accountId == playerId); if (target == null) return Success.Error("Player not found."); var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomInstance.roomId); if (room == null) return Success.Error("Room not found."); if (roomInstance.isFull) return Success.Error("Room instance is full."); if (await db.IsBanned(userId)) return Success.Error("Inviter is banned."); if (await MatchmakingUtils.IsBannedFromRoom(db, userId, roomInstance.roomId)) return Success.Error("Inviter is banned from this room."); if (await MatchmakingUtils.IsBannedFromRoom(db, playerId, roomInstance.roomId)) return Success.Error("Player is banned from this room."); var warningBounce = await PlayerUtils.BlockedByPendingWarningAsync( db, userId, room.IsDorm && room.CreatorAccountId == userId, $"instance {roomInstanceId}"); if (warningBounce != null) return Success.Error("Inviter is blocked from the room."); var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId); if (presence?.roomInstance != roomInstanceId || !presence.isOnline) return Success.Error("Inviter must be online in the room instance."); var playerCount = await db.Presences.CountAsync(row => row.roomInstance == roomInstanceId && row.isOnline); if (playerCount >= roomInstance.maxCapacity) return Success.Error("Room instance is full."); var details = new RoomInviteDetails { Name = room.Name, RoomInstanceId = roomInstanceId, InviteMode = 0 }; await Notify.SendMessage(playerId, new Message { Type = MessageType.GameInviteV2, RoomId = roomInstance.roomId, FromPlayerId = userId }, prepareWithId: message => { details.InviteId = message.Id; message.Data = System.Text.Json.JsonSerializer.Serialize(details); }); return new Success(true, null, null); } private async Task HasActiveGameInviteAsync(long playerId, RoomInstance roomInstance) { var invites = await db.Messages.AsNoTracking() .Where(message => message.ToPlayerId == playerId && (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) && message.RoomId == roomInstance.roomId) .OrderByDescending(message => message.Id) .ToListAsync(); var invite = invites.FirstOrDefault(message => RoomInviteDetails.GetInstanceId(message) == roomInstance.roomInstanceId); if (invite == null || await db.IsBanned(invite.FromPlayerId)) return false; return await db.Presences.AsNoTracking().AnyAsync(presence => presence.playerId == invite.FromPlayerId && presence.roomInstance == roomInstance.roomInstanceId && presence.isOnline) && !await MatchmakingUtils.IsBannedFromRoom(db, invite.FromPlayerId, roomInstance.roomId); } } }