Files
2026-10-16 23:33:13 -07:00

575 lines
18 KiB
C#

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<RecDBContext> DbFactory = null!;
public static async Task<bool> 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<bool> 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<bool> 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<List<RoomData>> VisibleRooms(IEnumerable<RoomData> rooms, long? accountId)
{
var visible = new List<RoomData>();
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<bool> 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<SubRoom>(),
Roles = new List<RoomRole>(),
LoadScreens = new List<RoomLoadScreen>(),
PromoImages = new List<string>(),
PromoExternalContent = new List<ExternalContent>(),
DataBlob = string.Empty,
Tags = new List<ItemTag>(),
Stats = new RoomStats { RoomId = room.RoomId },
Version = 1
};
}
public static async Task<RoomInstance?> 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<List<RoomRole>> 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<RoomRole?> 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<List<SubRoomDataSave>> 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<SubRoomDataSave>();
var saves = new List<SubRoomDataSave>();
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<SubRoom> subRooms, RoomStats stats, List<RoomRole> 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<List<RoomLoadScreen>>() ?? 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<List<FullRoom>> BulkConvertRoomData(List<RoomData> data)
{
var fullRooms = new List<FullRoom>();
foreach (var d in data)
{
var found = await GetRoom(d.RoomId);
if (found != null)
fullRooms.Add(found);
}
return fullRooms;
}
public static async Task<List<RoomDetail>> BulkConvertRoomDataToRoomDetail(List<RoomData> data)
{
var rooms = new List<RoomDetail>();
foreach (var d in data)
{
var found = await GetRoom(d.RoomId);
if (found != null)
rooms.Add(found);
}
return rooms;
}
public static async Task<FullRoom?> 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<FullRoom?> 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<RoomStats?> 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<FullRoom?> 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<FullRoom?> 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<FullRoom?> 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);
}
}
}