Files
Mocha-2023-RecRoom-Server/Classes/Sessions.cs
T
mochaisvibecoded 42f7f57b72 :3
2026-08-24 17:00:26 -04:00

649 lines
25 KiB
C#

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<long, HashSet<long>>
_confirmedParticipantsByInstance = new();
private static readonly Dictionary<long, long>
_instanceOwners = new();
private static readonly Dictionary<long, DateTime>
_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<long>? participants))
{
participants = new HashSet<long>();
_confirmedParticipantsByInstance[roomInstanceId] = participants;
}
participants.Add(playerId);
}
private static bool IsConfirmedParticipantNoLock(
long playerId,
long roomInstanceId)
{
return _confirmedParticipantsByInstance.TryGetValue(
roomInstanceId,
out HashSet<long>? 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);
}
}
}
}