using Mocha2023.Classes.DBs; using Mocha2023.Classes.DBs.DBClasses; using static Mocha2023.Classes.DBs.DBClasses.PlayerDBClasses; using static Mocha2023.Classes.DBs.DBClasses.RoomDBClasses; namespace Mocha2023.Classes { public class Sessions { private static readonly Random _rng = new(); private static readonly object _matchmakingLock = new(); private const long ActiveHeartbeatWindowSeconds = 120; private static readonly Dictionary> _confirmedParticipantsByInstance = new(); private static readonly Dictionary _instanceOwners = new(); private static readonly Dictionary _guestDormEnteredAt = new(); private static readonly TimeSpan GuestDormHoldWindow = TimeSpan.FromSeconds(15); public static Heartbeat? CreateRoom( long playerId, long roomId, string sceneName = "", bool isPrivate = false) { return CreateRoomInternal( playerId, roomId, sceneName, exactSubRoomId: null, isPrivate: isPrivate); } public static Heartbeat? CreateRoom( long playerId, long roomId, long subRoomId, bool isPrivate = false) { return CreateRoomInternal( playerId, roomId, sceneName: null, exactSubRoomId: subRoomId, isPrivate: isPrivate); } private static Heartbeat? CreateRoomInternal( long playerId, long roomId, string? sceneName, long? exactSubRoomId, bool isPrivate) { var room = RoomDB.GetRoom(roomId); if (room?.SubRooms == null || room.SubRooms.Count == 0) return null; SubRooms? subRoom = null; if (exactSubRoomId.HasValue) { subRoom = room.SubRooms.FirstOrDefault(sub => sub.SubRoomId == exactSubRoomId.Value); if (subRoom == null) return null; } else if (!string.IsNullOrWhiteSpace(sceneName)) { subRoom = room.SubRooms.FirstOrDefault(sub => string.Equals( sub.Name, sceneName, StringComparison.OrdinalIgnoreCase)); } if (!exactSubRoomId.HasValue) { subRoom ??= room.SubRooms .Where(sub => sub.Accessibility != RoomAccessibility.Private) .OrderByDescending(sub => string.Equals( sub.Name, "Home", StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); subRoom ??= room.SubRooms.FirstOrDefault(sub => string.Equals( sub.Name, "Home", StringComparison.OrdinalIgnoreCase)); subRoom ??= room.SubRooms.FirstOrDefault(); } if (subRoom == null) return null; lock (_matchmakingLock) { if (!RoomDB.CanPlayerAccessSubRoom( room, subRoom, playerId) || RoomDB.IsPlayerBannedFromRoom(room.RoomId, playerId)) { Console.WriteLine( $"[ROOM ACCESS DENIED] player={playerId} " + $"room={room.RoomId} subroom={subRoom.SubRoomId} " + $"isDorm={room.IsDorm} accessibility={room.Accessibility} " + $"subroomAccessibility={subRoom.Accessibility}"); return null; } bool effectivePrivate = isPrivate || room.IsDorm || room.Accessibility == RoomAccessibility.Private || subRoom.Accessibility == RoomAccessibility.Private; if (!effectivePrivate) { var existing = FindBestPublicInstance( playerId, room.RoomId, subRoom.SubRoomId, subRoom.MaxPlayers); if (existing != null) { var joinedSession = CloneInstance(existing.Value.Instance); joinedSession.isFull = existing.Value.PlayerCount + 1 >= joinedSession.maxCapacity; RemovePlayerFromConfirmedInstancesNoLock(playerId); TrackParticipantNoLock( playerId, joinedSession.roomInstanceId); Console.WriteLine( $"[MATCHMAKING] player={playerId} joined existing " + $"room={room.RoomId} instance={joinedSession.roomInstanceId} " + $"players={existing.Value.PlayerCount + 1}/{joinedSession.maxCapacity}"); return PlayerDB.UpdatePlayerHeartbeat(playerId, joinedSession); } } bool hasPersistencePair = RoomDB.TryGetPersistencePair( room, subRoom, out var dataSave, out string roomDataBlob, out _); long instanceId = _rng.NextInt64(1_000_000L, 1_000_000_000_000L); string roomName = room.Name ?? "UnknownRoom"; var session = new RoomInstance { Name = $"^{roomName}", dataBlob = hasPersistencePair ? roomDataBlob : string.Empty, isPrivate = effectivePrivate, location = subRoom.UnitySceneId, maxCapacity = Math.Max(1, subRoom.MaxPlayers), photonRegion = ServerConfig.PhotonRegion, photonRegionId = ServerConfig.PhotonRegion, photonRoomId = effectivePrivate ? $"MochaRoom-{roomName}-private-{instanceId}-{Guid.NewGuid()}" : $"MochaRoom-{roomName}-{instanceId}", roomId = room.RoomId, roomInstanceId = instanceId, roomInstanceType = effectivePrivate ? RoomInstanceType.Private : RoomInstanceType.Public, subRoomId = subRoom.SubRoomId, subRoomDataSaveId = dataSave?.SubRoomDataSaveId ?? 0, createdAt = DateTime.UtcNow }; RemovePlayerFromConfirmedInstancesNoLock(playerId); TrackParticipantNoLock(playerId, session.roomInstanceId); if (IsRestrictedInstance(session)) _instanceOwners[session.roomInstanceId] = playerId; Console.WriteLine( $"[ROOM LOAD] room={room.RoomId} " + $"subroom={subRoom.SubRoomId} " + $"name={subRoom.Name} " + $"instance={session.roomInstanceId} new=true " + $"superRoomBlob={room.DataBlob ?? "null"} " + $"subRoomBlob={subRoom.DataBlob ?? "null"} " + $"subRoomDataSaveId={session.subRoomDataSaveId}"); return PlayerDB.UpdatePlayerHeartbeat(playerId, session); } } private static (RoomInstance Instance, int PlayerCount)? FindBestPublicInstance( long playerId, long roomId, long subRoomId, int configuredCapacity) { long activeAfter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ActiveHeartbeatWindowSeconds; return PlayerDB.Players.FindAll() .Where(player => player.PlayerId != playerId) .Select(player => player.Player?.PlayerExtra?.Heartbeat) .Where(heartbeat => heartbeat != null && heartbeat.isOnline && heartbeat.lastHeartbeatUnixTime >= activeAfter && heartbeat.roomInstance != null && !heartbeat.roomInstance.isPrivate && heartbeat.roomInstance.roomId == roomId && heartbeat.roomInstance.subRoomId == subRoomId && !string.IsNullOrWhiteSpace(heartbeat.roomInstance.photonRoomId)) .GroupBy(heartbeat => heartbeat!.roomInstance!.roomInstanceId) .Select(group => { var instance = group.First()!.roomInstance!; int playerCount = group .Select(heartbeat => heartbeat!.playerId) .Distinct() .Count(); int capacity = Math.Max( 1, instance.maxCapacity > 0 ? instance.maxCapacity : configuredCapacity); return new { Instance = instance, PlayerCount = playerCount, Capacity = capacity }; }) .Where(candidate => candidate.PlayerCount < candidate.Capacity) .OrderByDescending(candidate => candidate.PlayerCount) .ThenBy(candidate => candidate.Instance.roomInstanceId) .Select(candidate => ((RoomInstance Instance, int PlayerCount)?) (candidate.Instance, candidate.PlayerCount)) .FirstOrDefault(); } private static RoomInstance CloneInstance(RoomInstance source) { return new RoomInstance { encryptVoiceChat = source.encryptVoiceChat, clubId = source.clubId, dataBlob = source.dataBlob, eventId = source.eventId, isFull = source.isFull, isInProgress = source.isInProgress, isPrivate = source.isPrivate, location = source.location, maxCapacity = source.maxCapacity, Name = source.Name, photonRegion = source.photonRegion, photonRegionId = source.photonRegionId, photonRoomId = source.photonRoomId, roomCode = source.roomCode, roomId = source.roomId, roomInstanceId = source.roomInstanceId, roomInstanceType = source.roomInstanceType, subRoomId = source.subRoomId, subRoomDataSaveId = source.subRoomDataSaveId, createdAt = source.createdAt }; } public static bool IsRestrictedInstance(RoomInstance? instance) => instance != null && (instance.isPrivate || instance.roomInstanceType is RoomInstanceType.Private or RoomInstanceType.Dormroom); public static Heartbeat? JoinRoomInstance( long playerId, long roomInstanceId) { return JoinRoomInstanceInternal( playerId, roomInstanceId, invitedByPlayerId: null, expectedRoomId: null); } public static Heartbeat? JoinInvitedRoomInstance( long playerId, long roomInstanceId, long invitedByPlayerId, long expectedRoomId) { if (invitedByPlayerId <= 0 || expectedRoomId <= 0) return null; return JoinRoomInstanceInternal( playerId, roomInstanceId, invitedByPlayerId, expectedRoomId); } private static Heartbeat? JoinRoomInstanceInternal( long playerId, long roomInstanceId, long? invitedByPlayerId, long? expectedRoomId) { if (playerId <= 0 || roomInstanceId <= 0) return null; lock (_matchmakingLock) { long activeAfter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ActiveHeartbeatWindowSeconds; var occupants = PlayerDB.Players.FindAll() .Select(player => player.Player?.PlayerExtra?.Heartbeat) .Where(heartbeat => heartbeat != null && heartbeat.isOnline && heartbeat.lastHeartbeatUnixTime >= activeAfter && heartbeat.roomInstance != null && heartbeat.roomInstance.roomInstanceId == roomInstanceId && !string.IsNullOrWhiteSpace( heartbeat.roomInstance.photonRoomId)) .ToList(); RoomInstance? source = occupants .OrderByDescending(heartbeat => heartbeat!.lastHeartbeatUnixTime) .Select(heartbeat => heartbeat!.roomInstance) .FirstOrDefault(); if (source == null) return null; if (expectedRoomId.HasValue && source.roomId != expectedRoomId.Value) { Console.WriteLine( $"[PRIVATE JOIN DENIED] player={playerId} " + $"instance={roomInstanceId} reason=invite-room-mismatch"); return null; } bool alreadyConfirmed = occupants.Any(heartbeat => heartbeat!.playerId == playerId); bool isOwner = _instanceOwners.TryGetValue( roomInstanceId, out long ownerPlayerId) && ownerPlayerId == playerId; bool inviterIsPresent = invitedByPlayerId.HasValue && invitedByPlayerId.Value != playerId && occupants.Any(heartbeat => heartbeat!.playerId == invitedByPlayerId.Value); bool hasVerifiedInviteGrant = invitedByPlayerId.HasValue && invitedByPlayerId.Value != playerId && NotificationDB.HasActiveRoomInvite( playerId, invitedByPlayerId.Value, roomInstanceId, source.roomId); var sourceRoom = RoomDB.GetRoom(source.roomId); var sourceSubRoom = sourceRoom?.SubRooms?.FirstOrDefault( subRoom => subRoom.SubRoomId == source.subRoomId); bool persistentRoomIsRestricted = sourceRoom?.IsDorm == true || sourceRoom?.Accessibility == RoomAccessibility.Private || sourceSubRoom?.Accessibility == RoomAccessibility.Private; bool hasPersistentRoomAccess = persistentRoomIsRestricted && RoomDB.CanPlayerAccessSubRoom( sourceRoom, sourceSubRoom, playerId); if (sourceRoom != null && RoomDB.IsPlayerBannedFromRoom(sourceRoom.RoomId, playerId)) { Console.WriteLine( $"[ROOM ACCESS DENIED] player={playerId} " + $"room={sourceRoom.RoomId} instance={roomInstanceId} " + "reason=room-ban"); return null; } if ((IsRestrictedInstance(source) || persistentRoomIsRestricted) && !alreadyConfirmed && !isOwner && !hasVerifiedInviteGrant && !hasPersistentRoomAccess) { Console.WriteLine( $"[PRIVATE JOIN DENIED] player={playerId} " + $"instance={roomInstanceId} room={source.roomId} " + "reason=invite-required"); return null; } int playerCount = occupants .Select(heartbeat => heartbeat!.playerId) .Where(id => id > 0) .Distinct() .Count(); int capacity = Math.Max(1, source.maxCapacity); if (!alreadyConfirmed && playerCount >= capacity) return null; RoomInstance joinedSession = CloneInstance(source); int joinedPlayerCount = alreadyConfirmed ? playerCount : playerCount + 1; joinedSession.isFull = joinedPlayerCount >= capacity; RemovePlayerFromConfirmedInstancesNoLock(playerId); TrackParticipantNoLock( playerId, joinedSession.roomInstanceId); if (hasVerifiedInviteGrant && joinedSession.roomInstanceType == RoomInstanceType.Dormroom) { _guestDormEnteredAt[playerId] = DateTime.UtcNow; } Console.WriteLine( $"[MATCHMAKING INSTANCE] player={playerId} " + $"joined room={joinedSession.roomId} " + $"instance={joinedSession.roomInstanceId} " + $"players={joinedPlayerCount}/{capacity} " + $"viaInvite={hasVerifiedInviteGrant.ToString().ToLowerInvariant()} " + $"inviterPresent={inviterIsPresent.ToString().ToLowerInvariant()}"); return PlayerDB.UpdatePlayerHeartbeat( playerId, joinedSession); } } public static void MarkGuestDormEntry(long playerId) { lock (_matchmakingLock) _guestDormEnteredAt[playerId] = DateTime.UtcNow; } public static bool ShouldHoldInGuestDorm(long playerId, Heartbeat heartbeat) { if (!heartbeat.isOnline || heartbeat.roomInstance?.roomInstanceType != RoomInstanceType.Dormroom) return false; lock (_matchmakingLock) { return _guestDormEnteredAt.TryGetValue( playerId, out DateTime enteredAt) && DateTime.UtcNow - enteredAt < GuestDormHoldWindow; } } public static Heartbeat CreateDorm(long playerId, string name) { var dorm = RoomDB.GetOrCreatePlayerDorm(playerId) ?? RoomDB.GetRoom(1); var subRoom = dorm?.SubRooms?.FirstOrDefault(sub => string.Equals( sub.Name, "Home", StringComparison.OrdinalIgnoreCase)) ?? dorm?.SubRooms?.FirstOrDefault(); long dormId = dorm?.RoomId ?? 1; long instanceId = _rng.NextInt64(1_000_000L, 1_000_000_000_000L); SubRoomDataSave? dataSave = null; string roomDataBlob = string.Empty; bool hasPersistencePair = false; if (dorm != null && subRoom != null) { hasPersistencePair = RoomDB.TryGetPersistencePair( dorm, subRoom, out dataSave, out roomDataBlob, out _); } var session = new RoomInstance { isPrivate = true, dataBlob = hasPersistencePair ? roomDataBlob : string.Empty, location = subRoom?.UnitySceneId ?? "76d98498-60a1-430c-ab76-b54a29b7a163", maxCapacity = subRoom?.MaxPlayers ?? 4, Name = $"@{name}'s Dorm", photonRegion = ServerConfig.PhotonRegion, photonRegionId = ServerConfig.PhotonRegion, photonRoomId = $"MochaDorm-{instanceId}-room", roomId = dormId, roomInstanceId = instanceId, subRoomId = subRoom?.SubRoomId ?? 0, subRoomDataSaveId = dataSave?.SubRoomDataSaveId ?? 0, roomInstanceType = RoomInstanceType.Dormroom, createdAt = DateTime.UtcNow }; lock (_matchmakingLock) { RemovePlayerFromConfirmedInstancesNoLock(playerId); TrackParticipantNoLock(playerId, session.roomInstanceId); _instanceOwners[session.roomInstanceId] = playerId; _guestDormEnteredAt.Remove(playerId); } Console.WriteLine( $"[DORM LOAD] account={playerId} room={session.roomId} " + $"subroom={session.subRoomId} " + $"superRoomBlob={(hasPersistencePair ? session.dataBlob : "null")} " + $"subRoomBlob={(hasPersistencePair ? dataSave?.DataBlob : "null")} " + $"subRoomDataSaveId={session.subRoomDataSaveId} " + $"cv2Pair={hasPersistencePair}"); return PlayerDB.UpdatePlayerHeartbeat(playerId, session) ?? new Heartbeat { playerId = playerId, isOnline = true, roomInstance = session }; } public static bool ReportJoinResult( long playerId, long roomInstanceId, bool succeeded) { if (playerId <= 0 || roomInstanceId <= 0) return false; Heartbeat heartbeat = PlayerDB.GetPlayerHeartbeat(playerId); if (heartbeat.roomInstance?.roomInstanceId != roomInstanceId) { Console.WriteLine( $"[JOIN RESULT] player={playerId} instance={roomInstanceId} " + "ignored=true reason=heartbeat-mismatch"); return false; } lock (_matchmakingLock) { RemovePlayerFromConfirmedInstancesNoLock(playerId); if (succeeded) TrackParticipantNoLock(playerId, roomInstanceId); } if (!succeeded) PlayerDB.LeaveCurrentRoom(playerId); Console.WriteLine( $"[JOIN RESULT] player={playerId} instance={roomInstanceId} " + $"success={succeeded.ToString().ToLowerInvariant()}"); return true; } public static void MarkPlayerLeft(long playerId) { if (playerId <= 0) return; lock (_matchmakingLock) { RemovePlayerFromConfirmedInstancesNoLock(playerId); _guestDormEnteredAt.Remove(playerId); } } public static bool IsConfirmedParticipant( long playerId, long roomInstanceId) { lock (_matchmakingLock) { if (IsConfirmedParticipantNoLock(playerId, roomInstanceId)) return true; } Heartbeat heartbeat = PlayerDB.GetPlayerHeartbeat(playerId); return heartbeat.isOnline && heartbeat.roomInstance?.roomInstanceId == roomInstanceId; } private static void TrackParticipantNoLock( long playerId, long roomInstanceId) { if (playerId <= 0 || roomInstanceId <= 0) return; if (!_confirmedParticipantsByInstance.TryGetValue( roomInstanceId, out HashSet? participants)) { participants = new HashSet(); _confirmedParticipantsByInstance[roomInstanceId] = participants; } participants.Add(playerId); } private static bool IsConfirmedParticipantNoLock( long playerId, long roomInstanceId) { return _confirmedParticipantsByInstance.TryGetValue( roomInstanceId, out HashSet? participants) && participants.Contains(playerId); } private static void RemovePlayerFromConfirmedInstancesNoLock( long playerId) { foreach (long instanceId in _confirmedParticipantsByInstance .Where(pair => pair.Value.Remove(playerId) && pair.Value.Count == 0) .Select(pair => pair.Key) .ToList()) { _confirmedParticipantsByInstance.Remove(instanceId); _instanceOwners.Remove(instanceId); } } } }