using Microsoft.EntityFrameworkCore; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet; using RadicadeServer.RecNet.Accounts; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Rooms; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Rooms; namespace RadicadeServer.Utils { public static class RoomUtils { public static IDbContextFactory DbFactory = null!; public static async Task VerifyRole(long playerId, long roomId, RoleType roleType) { var role = await GetRoleForPlayer(playerId, roomId); if (role == null) return false; return role.Role >= roleType; } public static async Task CanSeeRoom(RoomData room, long? accountId) { if (room.Accessibility != Accessibility.Private) return true; if (accountId == null) return false; if (room.CreatorAccountId == accountId.Value) return true; if (await VerifyRole(accountId.Value, room.RoomId, RoleType.CoOwner)) return true; if (DbFactory != null) { await using var db = await DbFactory.CreateDbContextAsync(); if (await ( from presence in db.Presences.AsNoTracking() join instance in db.RoomInstances.AsNoTracking() on presence.roomInstance equals instance.roomInstanceId where presence.playerId == accountId.Value && presence.isOnline && instance.roomId == room.RoomId select presence.playerId).AnyAsync()) return true; } return await HasLiveGameInvite(room.RoomId, accountId.Value); } public static async Task HasLiveGameInvite(long roomId, long accountId) { if (DbFactory == null) return false; await using var db = await DbFactory.CreateDbContextAsync(); long? inviter = await db.Messages.AsNoTracking() .Where(message => message.ToPlayerId == accountId && (message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) && message.RoomId == roomId) .OrderByDescending(message => message.Id) .Select(message => (long?)message.FromPlayerId) .FirstOrDefaultAsync(); if (inviter == null) return false; return await ( from presence in db.Presences.AsNoTracking() join instance in db.RoomInstances.AsNoTracking() on presence.roomInstance equals instance.roomInstanceId where presence.playerId == inviter.Value && presence.isOnline && instance.roomId == roomId select presence.playerId).AnyAsync(); } public static async Task> VisibleRooms(IEnumerable rooms, long? accountId) { var visible = new List(); foreach (var room in rooms) { if (await CanSeeRoom(room, accountId)) visible.Add(room); } return visible; } public const string PrivateRoomName = "PRIVATE ROOM"; public static async Task IsHiddenFrom(RoomData room, long? accountId) => !room.IsDorm && !await CanSeeRoom(room, accountId); public static FullRoom MaskHiddenRoom(RoomData room) { return new FullRoom { RoomId = room.RoomId, Name = PrivateRoomName, Description = string.Empty, ImageName = DBConfig.DefaultRoomImage, IsDorm = false, State = room.State, Accessibility = Accessibility.Private, CreatedAt = room.CreatedAt, CreatorAccountId = room.CreatorAccountId, MaxPlayers = room.MaxPlayers, MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, CloningAllowed = false, DisableRoomComments = true, CustomWarning = string.Empty, SubRooms = new List(), Roles = new List(), LoadScreens = new List(), PromoImages = new List(), PromoExternalContent = new List(), DataBlob = string.Empty, Tags = new List(), Stats = new RoomStats { RoomId = room.RoomId }, Version = 1 }; } public static async Task ForViewerAsync(RoomInstance? instance, long? viewerId) { if (instance == null) return null; await using var db = await DbFactory.CreateDbContextAsync(); var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId); if (room == null || !await IsHiddenFrom(room, viewerId)) return instance; var masked = instance.Copy(); masked.name = PrivateRoomName; return masked; } public static async Task> GetRoles(long roomId) { await using var db = await DbFactory.CreateDbContextAsync(); var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return []; var roomRole = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == roomId).ToListAsync(); var creatorRow = roomRole.FirstOrDefault(x => x.AccountId == room.CreatorAccountId); if (creatorRow == null) roomRole.Add(new RoomRole { AccountId = room.CreatorAccountId, InvitedRole = RoleType.None, Role = RoleType.Creator, RoomId = roomId }); else if (creatorRow.Role < RoleType.Creator) creatorRow.Role = RoleType.Creator; if (room.IsRRO) { var rroOwners = (await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"))?.Value; foreach (var dev in (rroOwners ?? string.Empty).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) { if (!long.TryParse(dev, out long userId)) continue; foreach (var role in new[] { RoleType.Creator, RoleType.Moderator }) { if (roomRole.Any(x => x.AccountId == userId && x.Role == role)) continue; roomRole.Add(new RoomRole { AccountId = userId, InvitedRole = RoleType.None, Role = role, RoomId = roomId }); } } } return roomRole; } public static async Task GetRoleForPlayer(long playerId, long roomId) { var roomRoles = await GetRoles(roomId); return roomRoles .Where(x => x.AccountId == playerId) .OrderByDescending(x => x.Role) .FirstOrDefault(); } public static SubRoomDataSave BuildCurrentSave(SubRoom subRoom) { return new SubRoomDataSave { SubRoomDataSaveId = 1, SubRoomId = subRoom.SubRoomId, DataBlob = subRoom.DataBlob, SavedByAccountId = subRoom.SavedByAccountId, CreatedAt = subRoom.CreatedAt, UnityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId, PersistenceVersion = 1, OMVersion = 1, UgcSubVersion = 1, SavedOnPlatform = 0, SavedOnDeviceClass = 0, Description = string.Empty, ModerationState = 0 }; } public static object BuildSaveResponse(SubRoom subRoom, string? dataBlobHash) { return new { subRoomDataSaveId = 1L, subRoomId = subRoom.SubRoomId, unityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId, unityAsset = (string?)null, unityAssetHash = (string?)null, dataBlob = subRoom.DataBlob ?? string.Empty, dataBlobHash, savedByAccountId = (long?)subRoom.SavedByAccountId, savedOnPlatform = 0, savedOnDeviceClass = 0, description = string.Empty, createdAt = subRoom.CreatedAt }; } public static async Task> GetSaveHistory(RecDBContext ctx, long roomId, long subRoomId) { var subRoom = await ctx.SubRooms.AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (subRoom == null) return new List(); var saves = new List(); if (!string.IsNullOrEmpty(subRoom.DataBlob)) saves.Add(BuildCurrentSave(subRoom)); var history = await ctx.SubRoomHistory.AsNoTracking() .Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId && x.DataBlob != "") .OrderByDescending(x => x.CreatedAt) .ThenByDescending(x => x.HistoryId) .ToListAsync(); saves.AddRange(history.Select(BuildRestorableSave)); return saves; } public static SubRoomDataSave BuildRestorableSave(RestorableSubRoom row) { return new SubRoomDataSave { SubRoomDataSaveId = row.HistoryId + 1, SubRoomId = row.SubRoomId, DataBlob = row.DataBlob, SavedByAccountId = row.SavedByAccountId, CreatedAt = row.CreatedAt, PersistenceVersion = 1, OMVersion = 1, UgcSubVersion = 1, SavedOnPlatform = 0, SavedOnDeviceClass = 0, Description = string.Empty, ModerationState = 0 }; } public static FullRoom ConvertToFull(this RoomData room, List subRooms, RoomStats stats, List roles) { FullRoom fullRoom = new FullRoom { Name = room.Name, RoomId = room.RoomId, Description = room.Description ?? string.Empty, ImageName = room.ImageName, IsRRO = room.IsRRO, IsDeveloperOwned = room.IsDeveloperOwned, IsDorm = room.IsDorm, State = room.State, Accessibility = room.Accessibility, CreatedAt = room.CreatedAt, CreatorAccountId = room.CreatorAccountId, SupportsJuniors = room.SupportsJuniors, SupportsLevelVoting = room.SupportsLevelVoting, SupportsMobile = room.SupportsMobile, SupportsQuest2 = room.SupportsQuest2, SupportsScreens = room.SupportsScreens, SupportsTeleportVR = room.SupportsTeleportVR, SupportsVRLow = room.SupportsVRLow, SupportsWalkVR = room.SupportsWalkVR, EncryptVoiceChat = room.EncryptVoiceChat, MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, MaxPlayers = room.MaxPlayers, SubRooms = subRooms, Stats = stats, CloningAllowed = room.CloningAllowed, CustomWarning = room.CustomWarning ?? string.Empty, Roles = roles, DataBlob = null, Tags = StringTools.ReadTags(room.Tags), LoadScreens = room.loadScreen.FromJson>() ?? new(), WarningMask = room.WarningMask, Version = room.Version, DisableRoomComments = room.DisableRoomComments, DisableMicAutoMute = room.DisableMicAutoMute, MinLevel = room.MinLevel }; foreach (var subRoom in subRooms) { subRoom.CurrentSave = string.IsNullOrEmpty(subRoom.DataBlob) ? null : BuildCurrentSave(subRoom); subRoom.CreatorAccountId = subRoom.SavedByAccountId != 0 ? subRoom.SavedByAccountId : null; } return fullRoom; } public static async Task> BulkConvertRoomData(List data) { var fullRooms = new List(); foreach (var d in data) { var found = await GetRoom(d.RoomId); if (found != null) fullRooms.Add(found); } return fullRooms; } public static async Task> BulkConvertRoomDataToRoomDetail(List data) { var rooms = new List(); foreach (var d in data) { var found = await GetRoom(d.RoomId); if (found != null) rooms.Add(found); } return rooms; } public static async Task GetRoom(string Name) { await using var ctx = await DbFactory.CreateDbContextAsync(); var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.Name == Name); if (room == null && !string.IsNullOrWhiteSpace(Name) && Name.StartsWith("@", StringComparison.Ordinal) && Name.EndsWith("'s Dorm", StringComparison.OrdinalIgnoreCase)) { var username = Name.Substring(1, Name.Length - 8); var owner = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.username == username || x.displayName == username); if (owner != null) { room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.IsDorm && x.CreatorAccountId == owner.accountId); } } if (room == null) return null; return await GetRoom(room.RoomId); } public static async Task GetRoom(long Id) { await using var ctx = await DbFactory.CreateDbContextAsync(); var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.RoomId == Id); if (room == null) return null; var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Cheered); var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Favorited); var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Visited); var visitCount = await ctx.RoomInteractions.AsNoTracking() .Where(x => x.RoomId == Id && x.Visited) .SumAsync(x => (int?)x.VisitCount) ?? 0; var roomStats = new RoomStats { CheerCount = cheers, FavoriteCount = favourites, RoomId = Id, VisitCount = visitCount, VisitorCount = visitorCount }; var roomRoles = await GetRoles(Id); var subRooms = await ctx.SubRooms.AsNoTracking().Where((x) => x.RoomId == Id).ToListAsync(); var missingBlobs = subRooms .Where(x => !string.IsNullOrEmpty(x.DataBlob) && !File.Exists(Path.Join(DBConfig.CDNStorage, x.DataBlob))) .ToList(); if (missingBlobs.Count > 0) { var missingIds = missingBlobs.Select(x => x.SubRoomId).ToList(); var candidates = await ctx.SubRoomHistory.AsNoTracking() .Where(x => missingIds.Contains(x.SubRoomId) && x.DataBlob != "") .OrderByDescending(x => x.CreatedAt) .ToListAsync(); foreach (var subRoom in missingBlobs) { var recovered = candidates .Where(x => x.SubRoomId == subRoom.SubRoomId) .Select(x => x.DataBlob) .FirstOrDefault(blob => !string.IsNullOrEmpty(blob) && File.Exists(Path.Join(DBConfig.CDNStorage, blob))); subRoom.DataBlob = recovered ?? ""; ctx.SubRooms.Attach(subRoom); ctx.Entry(subRoom).Property(x => x.DataBlob).IsModified = true; } await ctx.SaveChangesAsync(); } return room.ConvertToFull(subRooms, roomStats, roomRoles); } public static async Task GetRoomStats(long roomId, long? playerId) { await using var ctx = await DbFactory.CreateDbContextAsync(); var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Cheered); var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Favorited); var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Visited); var visited = 0; if (playerId != null) { var interaction = await ctx.RoomInteractions.AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomId && x.PlayerId == playerId); if (interaction != null) visited = interaction.VisitCount; } return new RoomStats() { CheerCount = cheers, FavoriteCount = favourites, RoomId = roomId, VisitCount = visited, VisitorCount = visitorCount }; } public static async Task CloneRoom(long originalRoomId, long newCreator, string newName) { await using var ctx = await DbFactory.CreateDbContextAsync(); FullRoom? room = await GetRoom(originalRoomId); if (room == null) return null; var tags = room.Tags; var rro = tags.Find(x => x.Tag.StartsWith("rro") && x.Type >= TagType.Auto); if (rro != null) tags.Remove(rro); RoomData roomData = new RoomData { State = RoomState.Active, SupportsJuniors = room.SupportsJuniors, SupportsLevelVoting = room.SupportsLevelVoting, SupportsMobile = room.SupportsMobile, SupportsQuest2 = room.SupportsQuest2, SupportsScreens = room.SupportsScreens, SupportsTeleportVR = room.SupportsTeleportVR, SupportsVRLow = room.SupportsVRLow, SupportsWalkVR = room.SupportsWalkVR, Accessibility = Accessibility.Private, CloningAllowed = false, CreatedAt = DateTime.Now, CreatorAccountId = newCreator, CustomWarning = room.CustomWarning, Description = room.Description, DisableMicAutoMute = room.DisableMicAutoMute, DisableRoomComments = room.DisableRoomComments, EncryptVoiceChat = room.EncryptVoiceChat, ImageName = room.ImageName, IsDorm = room.IsDorm, IsRRO = false, IsDeveloperOwned = !room.IsDorm && await ctx.IsDeveloper(newCreator), MaxPlayerCalculationMode = room.MaxPlayerCalculationMode, MaxPlayers = room.MaxPlayers, MinLevel = room.MinLevel, Name = newName, Tags = StringTools.CreateTagString(tags), WarningMask = room.WarningMask, }; await ctx.AddAsync(roomData); await ctx.SaveChangesAsync(); if (room.SubRooms == null || room.SubRooms.Count == 0) { ctx.Remove(roomData); await ctx.SaveChangesAsync(); return null; } foreach (SubRoom subRoom in room.SubRooms) { if (subRoom == null) continue; await ctx.SubRooms.AddAsync(new SubRoom { Accessibility = subRoom.Accessibility, DataBlob = subRoom.DataBlob, UnityAssetId = subRoom.UnityAssetId, IsSandbox = subRoom.IsSandbox, UnitySceneId = subRoom.UnitySceneId, MaxPlayers = subRoom.MaxPlayers, Name = subRoom.Name, RoomId = roomData.RoomId }); } await ctx.SaveChangesAsync(); await ctx.RoomStats.AddAsync(new RoomStats { RoomId = roomData.RoomId, FavoriteCount = 0, CheerCount = 0, VisitCount = 0, VisitorCount = 0, }); await ctx.RoomRoles.AddAsync(new RoomRole { RoomId = roomData.RoomId, Role = RoleType.Creator, InvitedRole = RoleType.None, AccountId = newCreator }); await ctx.SaveChangesAsync(); return await GetRoom(roomData.RoomId); } private static async Task GetDormTemplateRoom() { var templateRoom = await GetRoom(1); if (templateRoom != null) return templateRoom; await using var ctx = await DbFactory.CreateDbContextAsync(); var template = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1); return template == null ? null : await GetRoom(template.RoomId); } public static async Task GetOrCreateDorm(long playerId) { await using var ctx = await DbFactory.CreateDbContextAsync(); var roomData = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.IsDorm && x.CreatorAccountId == playerId); if (roomData == null) { var templateRoom = await GetDormTemplateRoom(); if (templateRoom == null) { return null; } return await CloneRoom(templateRoom.RoomId, playerId, "DormRoom"); } var hasSubRoom = await ctx.SubRooms.AnyAsync(s => s.RoomId == roomData.RoomId); if (!hasSubRoom) { var templateRoom = await GetDormTemplateRoom(); var templateSub = templateRoom?.SubRooms?.FirstOrDefault(); if (templateSub != null) { await ctx.SubRooms.AddAsync(new SubRoom { RoomId = roomData.RoomId, DataBlob = templateSub.DataBlob, UnityAssetId = templateSub.UnityAssetId, IsSandbox = templateSub.IsSandbox, UnitySceneId = templateSub.UnitySceneId, MaxPlayers = templateSub.MaxPlayers, Name = templateSub.Name, Accessibility = templateSub.Accessibility }); await ctx.SaveChangesAsync(); } } return await GetRoom(roomData.RoomId); } } }