using Microsoft.EntityFrameworkCore; using RadicadeServer.DB; using RadicadeServer.RecNet; using RadicadeServer.RecNet.Config; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Rooms; using RadicadeServer.RecNet.Rooms; namespace RadicadeServer.Utils { public static class DefaultRooms { private const string AlignmentKey = "DefaultRooms.AlignmentVersion"; private const int AlignmentVersion = 1; private const string RroCorrectionKey = "DefaultRooms.RroCorrectionVersion"; private const string RecCenterRepairKey = "DefaultRooms.RecCenterRepairVersion"; private const int RecCenterRepairVersion = 1; private const string RecCenterRepairRoom = "RecCenter"; private const string CloningCorrectionKey = "DefaultRooms.NoCloneVersion"; private const int CloningCorrectionVersion = 1; private const int RroCorrectionVersion = 2; private const string ShippedRoomResetKey = "DefaultRooms.ShippedRoomResetVersion"; private const int ShippedRoomResetVersion = 3; public static IReadOnlyList All { get; } = new List { new DefaultRoom { Name = "Orientation", Description = "An introductory tour of Rec Room!", MaxPlayers = 1, SceneId = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8", Tags = Rro(), CloningAllowed = false, DisableRoomComments = true }, new DefaultRoom { Name = "RecCenter", Description = "A social hub to meet and mingle with friends new and old.", MaxPlayers = 12, SceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6", Tags = Rro(), CloningAllowed = false }, new DefaultRoom { Name = "TheRiseofJumbotron", Description = "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!", MaxPlayers = 4, SceneId = "acc06e66-c2d0-4361-b0cd-46246a4c455c", Tags = RroWith("quest") }, new DefaultRoom { Name = "GoldenTrophy", Description = "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!", MaxPlayers = 4, SceneId = "91e16e35-f48f-4700-ab8a-a1b79e50e51b", Tags = RroWith("quest") }, new DefaultRoom { Name = "Crescendo", Description = "Gather your vampire hunting crew, conquer a legendary castle, and restore peace to Rec Room!", MaxPlayers = 3, SceneId = "49cb8993-a956-43e2-86f4-1318f279b22a", Tags = RroWith("quest") }, new DefaultRoom { Name = "CrimsonCauldron", Description = "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?", MaxPlayers = 4, SceneId = "949fa41f-4347-45c0-b7ac-489129174045", Tags = RroWith("quest") }, new DefaultRoom { Name = "IsleOfLostSkulls", Description = "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?", MaxPlayers = 3, SceneId = "7e01cfe0-820a-406f-b1b3-0a5bf575235c", Tags = RroWith("quest") }, new DefaultRoom { Name = "StuntRunner", Description = "Sprint, climb, and wall jump in this high-speed obstacle course where every second counts!", MaxPlayers = 12, SceneId = "b7281665-a715-4051-826b-8e08e69c6172", SubRoomName = "StuntRunner", Tags = RroWith("sports"), CloningAllowed = false, ExtraSubRooms = new List { new() { Name = "TheMainEvent", SceneId = "3a636bd2-f896-424c-9225-c184522c0d87", MaxPlayers = 4 } } }, new DefaultRoom { Name = "3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!", MaxPlayers = 8, SceneId = "a673712c-877f-4749-b69a-4a4c6310d545", SubRoomName = "InkSpaceHome", Tags = Rro(), DisableRoomComments = true }, new DefaultRoom { Name = "Legacy3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!", MaxPlayers = 8, SceneId = "4078dfed-24bb-4db7-863f-578ba48d726b", Tags = Rro(), DisableRoomComments = true }, new DefaultRoom { Name = "TheInkSpace", Description = "A cozy event venue with a stage. The entire room is made of props, so customize to your heart's content!", MaxPlayers = 8, SceneId = "1fa06e3c-c307-4c11-a91b-1fabcddb8a96", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true, DisableRoomComments = true }, new DefaultRoom { Name = "Dodgeball", Description = "Throw dodgeballs to knock out your friends in this gym classic!", MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f", Tags = RroWith("sports") }, new DefaultRoom { Name = "DodgeballVR", Description = "Throw dodgeballs to knock out your friends in this gym classic!", MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f" }, new DefaultRoom { Name = "Bowling", Description = "Hang out with friends, bowl a few games, eat snacks!", MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800", Tags = RroWith("sports") }, new DefaultRoom { Name = "BowlingAlley", Description = "Lone Shoe Lanes is the hottest hangout in town.", MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Paddleball", Description = "A simple rally game between two players in a plexiglass tube with a zero-g ball.", MaxPlayers = 2, SceneId = "d89f74fa-d51e-477a-a425-025a891dd499", Tags = RroWith("sports") }, new DefaultRoom { Name = "Soccer", Description = "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!", MaxPlayers = 6, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", Tags = RroWith("sports") }, new DefaultRoom { Name = "DiscGolfLake", Description = "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?", MaxPlayers = 4, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0", Tags = RroWith("sports") }, new DefaultRoom { Name = "DiscGolfPropulsion", Description = "Throw your disc through hazards and around wind machines on this challenging course!", MaxPlayers = 4, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", Tags = RroWith("sports") }, new DefaultRoom { Name = "Paintball", Description = "Red and Blue teams splat each other in capture the flag and team battle.", MaxPlayers = 8, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0", SubRoomName = "River", Tags = RroWith("sports"), ExtraSubRooms = new List { new() { Name = "Homestead", SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7" }, new() { Name = "Quarry", SceneId = "ff4c6427-7079-4f59-b22a-69b089420827" }, new() { Name = "Clearcut", SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161" }, new() { Name = "Spillway", SceneId = "58763055-2dfb-4814-80b8-16fac5c85709" }, new() { Name = "Drive-in", SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3" } } }, new DefaultRoom { Name = "LaserTag", Description = "Teams battle each other and waves of robots.", MaxPlayers = 8, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3", SubRoomName = "Hangar", Tags = RroWith("pvp"), ExtraSubRooms = new List { new() { Name = "CyberJunkCity", SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038" } } }, new DefaultRoom { Name = "RecRoyaleSquads", Description = "Squads of three battle it out on Frontier Island. Last squad standing wins!", MaxPlayers = 18, SceneId = "253fa009-6e65-4c90-91a1-7137a56a267f", ImageName = "RecRoyaleSquads", Tags = RroWith("pvp"), CloningAllowed = false }, new DefaultRoom { Name = "RecRoyaleSolos", Description = "Battle it out on Frontier Island. Last person standing wins!", MaxPlayers = 16, SceneId = "b010171f-4875-4e89-baba-61e878cd41e1", ImageName = "RecRoyaleSolos", Tags = RroWith("pvp"), CloningAllowed = false, Aliases = new[] { "RecRoyaleSolo" } }, new DefaultRoom { Name = "Showdown", Description = "Showdown", MaxPlayers = 16, SceneId = SceneLocations.MakerRoom, ImageName = "Showdown", Tags = new List { new() { Tag = "rro", Type = TagType.AGOnly }, new() { Tag = "games", Type = TagType.General }, new() { Tag = "beta", Type = TagType.Auto } }, CloningAllowed = true, SaveName = "Showdown.room", BundleName = "Showdown_0_1.assetbundle" }, new DefaultRoom { Name = "Lounge", Description = "A low-key lounge to chill with your friends. Great for private parties!", MaxPlayers = 20, SceneId = "a067557f-ca32-43e6-b6e5-daaec60b4f5a", Tags = Rro(), SubRoomSandbox = true }, new DefaultRoom { Name = "Park", Description = "A sprawling park with amphitheater, play fields, and a cave.", MaxPlayers = 20, SceneId = "0a864c86-5a71-4e18-8041-8124e4dc9d98", Tags = Rro(), SubRoomSandbox = true }, new DefaultRoom { Name = "MakerRoom", Description = "This room is a blank canvas. Make it into whatever you like!", MaxPlayers = 20, SceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92", ImageName = "MakerRoom", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Gym", Description = "A school gymnasium for smaller sporting events.", MaxPlayers = 20, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Stadium", Description = "A professional stadium for larger sporting events.", MaxPlayers = 20, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "River", Description = "The original outdoor paintball course. Simple, balanced, classic.", MaxPlayers = 20, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Homestead", Description = "A day on the farm. Great for asymmetrical battles!", MaxPlayers = 20, SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Quarry", Description = "The sun sets on this construction site in the desert. Great for sniping battles!", MaxPlayers = 20, SceneId = "ff4c6427-7079-4f59-b22a-69b089420827", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Clearcut", Description = "The sun rises on this logging camp in the forest. Great for mid-range splatting!", MaxPlayers = 20, SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Spillway", Description = "Night shift at the hydroelectric plant. Great for stealthy splatting!", MaxPlayers = 20, SceneId = "58763055-2dfb-4814-80b8-16fac5c85709", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Lake", Description = "A leisurely trail to the lake and beyond.", MaxPlayers = 20, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "PropulsionTestRange", Description = "The science department left some of their equipment laying around. What will you do with it?", MaxPlayers = 20, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Hangar", Description = "A late-eighties-style laser tag battle arcade.", MaxPlayers = 20, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "CyberJunkCity", Description = "A late-2080s-style cyberpunk dystopian laser tag battle arcade", MaxPlayers = 20, SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "StuntRunnerBaseRoom", Description = "A soundstage warehouse used to film Stunt Runner and other RecTV shows.", MaxPlayers = 20, SceneId = "882e9b96-7115-4b03-86f6-c0c9d8e22e00", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "Registration", Description = "Registration", MaxPlayers = 1, SceneId = "cf61556d-68fd-4288-9ae5-7a512621e569", IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false }, new DefaultRoom { Name = "ARRoom", Description = "Your real room!", MaxPlayers = 4, SceneId = "bf268f5f-b55b-41af-8628-32fa4b5d70b6", IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false, SubRoomSandbox = true, WarningMask = WarningMask.Custom }, new DefaultRoom { Name = "DriveIn", Description = "A drive-in theater, diner, and truck stop make this a multi-area paint arena", MaxPlayers = 20, SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3", IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true }, new DefaultRoom { Name = "RecRally", Description = "A six-player off-road rally race where you boost, bump, and powerslide your way to the finish line!", MaxPlayers = 6, SceneId = "56193568-9ae0-498c-8a77-4df79dec91f5", Tags = RroWith("racing", "driving", "recrally"), WarningMask = WarningMask.IntenseMotion, SupportsTeleportVR = false }, new DefaultRoom { Name = "FrontierUGC", Description = "A public copyable room featuring the Rec Royale map!", MaxPlayers = 40, SceneId = "a16bfd31-ffb9-46ac-a199-362c163130c0", ImageName = "RecRoyale", IsRRO = false, Accessibility = Accessibility.Unlised } }; public static async Task EnsureAsync(RecDBContext db) { try { await ResetRoomsAsync(db); await EnsureCatalogAsync(db); await RemoveStrayRroAsync(db); await DisableCloningAsync(db); await RepairRecCenterAsync(db); } catch (Exception) { db.ChangeTracker.Clear(); } } private static async Task RepairRecCenterAsync(RecDBContext db) { try { if (await HasRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion)) return; var reference = All.FirstOrDefault(room => Matches(RecCenterRepairRoom, room)); if (reference == null) { await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); return; } var rooms = await db.Rooms.ToListAsync(); var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference)); if (existing == null) { await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); return; } var subRooms = await db.SubRooms.ToListAsync(); var changes = await AlignAsync(db, existing, subRooms, reference); foreach (var sub in subRooms.Where(sub => sub.RoomId == existing.RoomId)) { if (string.IsNullOrEmpty(sub.DataBlob)) continue; changes.Add($"subroom {sub.Name} content reset to the stock scene ({sub.DataBlob.Length} chars discarded)"); sub.DataBlob = string.Empty; sub.UnityAssetId = string.Empty; } await db.SaveChangesAsync(); await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion); } catch (Exception) { db.ChangeTracker.Clear(); } } private static async Task ResetRoomsAsync(RecDBContext db) { if (await HasRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion)) return; var rooms = await db.Rooms.ToListAsync(); if (rooms.Count == 0) { await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion); return; } var ids = rooms.Select(room => room.RoomId).ToList(); var subRoomIds = await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).Select(sub => sub.SubRoomId).ToListAsync(); db.SubRoomPermissions.RemoveRange(await db.SubRoomPermissions.Where(permission => subRoomIds.Contains(permission.subRoomId)).ToListAsync()); db.SubRoomHistory.RemoveRange(await db.SubRoomHistory.Where(history => ids.Contains(history.RoomId)).ToListAsync()); db.SubRooms.RemoveRange(await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).ToListAsync()); db.RoomStats.RemoveRange(await db.RoomStats.Where(stats => ids.Contains(stats.RoomId)).ToListAsync()); db.RoomRoles.RemoveRange(await db.RoomRoles.Where(role => ids.Contains(role.RoomId)).ToListAsync()); db.RoomKeys.RemoveRange(await db.RoomKeys.Where(key => ids.Contains(key.RoomId)).ToListAsync()); db.RoomBans.RemoveRange(await db.RoomBans.Where(ban => ids.Contains(ban.roomId)).ToListAsync()); db.RoomComments.RemoveRange(await db.RoomComments.Where(comment => ids.Contains(comment.RoomId)).ToListAsync()); db.RoomInteractions.RemoveRange(await db.RoomInteractions.Where(interaction => ids.Contains(interaction.RoomId)).ToListAsync()); db.RoomInstances.RemoveRange(await db.RoomInstances.Where(instance => ids.Contains(instance.roomId) || subRoomIds.Contains(instance.subRoomId)).ToListAsync()); db.LeaderboardScores.RemoveRange(await db.LeaderboardScores.Where(score => ids.Contains(score.RoomId)).ToListAsync()); var events = await db.PlayerEvents.Where(playerEvent => ids.Contains(playerEvent.RoomId)).ToListAsync(); if (events.Count > 0) { var eventIds = events.Select(playerEvent => playerEvent.PlayerEventId).ToList(); db.PlayerEventResponses.RemoveRange(await db.PlayerEventResponses.Where(response => eventIds.Contains(response.PlayerEventId)).ToListAsync()); db.PlayerEvents.RemoveRange(events); } foreach (var image in await db.Images.Where(image => image.RoomId.HasValue && ids.Contains(image.RoomId.Value)).ToListAsync()) image.RoomId = null; foreach (var message in await db.Messages.Where(message => message.RoomId.HasValue && ids.Contains(message.RoomId.Value)).ToListAsync()) message.RoomId = null; db.Rooms.RemoveRange(rooms); await db.SaveChangesAsync(); await db.AddDormRoom(); await ClearCatalogMarkerAsync(db); await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion); var dorm = await db.Rooms.FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1); } private static async Task ClearCatalogMarkerAsync(RecDBContext db) { var row = await db.GameConfig.FirstOrDefaultAsync(config => config.Key == AlignmentKey); if (row == null) await db.GameConfig.AddAsync(new ConfigVal(AlignmentKey, "0")); else row.Value = "0"; await db.SaveChangesAsync(); } private static async Task EnsureCatalogAsync(RecDBContext db) { try { if (await IsAlignedAsync(db)) return; var rooms = await db.Rooms.ToListAsync(); var subRooms = await db.SubRooms.ToListAsync(); var statsRoomIds = await db.RoomStats.Select(x => x.RoomId).ToListAsync(); var created = new List(); var aligned = new List(); foreach (var reference in All) { var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference)); if (existing == null) { var room = await CreateAsync(db, reference, statsRoomIds); rooms.Add(room); created.Add($"{reference.Name} ({reference.MaxPlayers} players)"); continue; } var changes = await AlignAsync(db, existing, subRooms, reference); if (changes.Count > 0) aligned.Add($"{existing.Name}: {string.Join(", ", changes)}"); } await db.SaveChangesAsync(); await MarkAlignedAsync(db); foreach (var line in created) ; foreach (var line in aligned) ; } catch (Exception) { db.ChangeTracker.Clear(); } } private static async Task RemoveStrayRroAsync(RecDBContext db) { if (await HasRunAsync(db, RroCorrectionKey, RroCorrectionVersion)) return; var rooms = await db.Rooms.ToListAsync(); var corrected = new List(); foreach (var room in rooms) { var entry = All.FirstOrDefault(candidate => !candidate.IsRRO && Matches(room.Name, candidate)); if (entry == null) continue; var tags = StringTools.ReadTags(room.Tags); var strayTags = tags.Where(tag => string.Equals(tag.Tag, "rro", StringComparison.OrdinalIgnoreCase)).ToList(); if (strayTags.Count == 0 && !room.IsRRO) continue; foreach (var tag in strayTags) tags.Remove(tag); room.Tags = StringTools.CreateTagString(tags); room.IsRRO = false; corrected.Add(room.Name); } if (corrected.Count > 0) await db.SaveChangesAsync(); await MarkRunAsync(db, RroCorrectionKey, RroCorrectionVersion); } private static async Task DisableCloningAsync(RecDBContext db) { if (await HasRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion)) return; var rooms = await db.Rooms.ToListAsync(); var off = new List(); var on = new List(); foreach (var room in rooms) { var entry = All.FirstOrDefault(candidate => Matches(room.Name, candidate)); if (entry == null || entry.CloningAllowed == room.CloningAllowed) continue; room.CloningAllowed = entry.CloningAllowed; (entry.CloningAllowed ? on : off).Add(room.Name); } if (off.Count > 0 || on.Count > 0) await db.SaveChangesAsync(); await MarkRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion); if (off.Count == 0 && on.Count == 0) { return; } } private static async Task CreateAsync(RecDBContext db, DefaultRoom reference, List statsRoomIds) { var room = new RoomData { Name = reference.Name, Description = reference.Description, ImageName = reference.ImageName ?? reference.Name, MaxPlayers = reference.MaxPlayers, MaxPlayerCalculationMode = MaxPlayerMode.OnlyEntrySubrooms, CreatorAccountId = 1, IsRRO = reference.IsRRO, IsDorm = false, Accessibility = reference.Accessibility, CreatedAt = DateTime.Now, State = RoomState.Active, CloningAllowed = reference.CloningAllowed, DisableRoomComments = reference.DisableRoomComments, WarningMask = reference.WarningMask, SupportsTeleportVR = reference.SupportsTeleportVR, Tags = StringTools.CreateTagString(reference.Tags.ToList()) }; await db.Rooms.AddAsync(room); await db.SaveChangesAsync(); await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId, reference.MaxPlayers, reference.Accessibility, reference.SubRoomSandbox, reference.SaveName, reference.BundleName); foreach (var extra in reference.ExtraSubRooms) await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference), reference.Accessibility, reference.SubRoomSandbox); if (!statsRoomIds.Contains(room.RoomId)) { await db.RoomStats.AddAsync(new RoomStats { RoomId = room.RoomId }); statsRoomIds.Add(room.RoomId); } await db.RoomRoles.AddAsync(new RoomRole { RoomId = room.RoomId, AccountId = 1, Role = RoleType.Creator, InvitedRole = RoleType.None }); await db.SaveChangesAsync(); return room; } private static async Task> AlignAsync(RecDBContext db, RoomData room, List subRooms, DefaultRoom reference) { var changes = new List(); var image = reference.ImageName ?? reference.Name; if (room.MaxPlayers != reference.MaxPlayers) { changes.Add($"max players {room.MaxPlayers} -> {reference.MaxPlayers}"); room.MaxPlayers = reference.MaxPlayers; } if (!string.Equals(room.ImageName, image, StringComparison.Ordinal)) { changes.Add($"image '{room.ImageName}' -> '{image}'"); room.ImageName = image; } if (room.Accessibility != reference.Accessibility) { changes.Add($"accessibility {room.Accessibility} -> {reference.Accessibility}"); room.Accessibility = reference.Accessibility; } if (room.CloningAllowed != reference.CloningAllowed) { changes.Add($"cloning {room.CloningAllowed} -> {reference.CloningAllowed}"); room.CloningAllowed = reference.CloningAllowed; } if (room.DisableRoomComments != reference.DisableRoomComments) { changes.Add($"room comments {(reference.DisableRoomComments ? "off" : "on")}"); room.DisableRoomComments = reference.DisableRoomComments; } if (room.WarningMask != reference.WarningMask) { changes.Add($"warning mask {room.WarningMask} -> {reference.WarningMask}"); room.WarningMask = reference.WarningMask; } if (room.SupportsTeleportVR != reference.SupportsTeleportVR) { changes.Add($"teleport VR {reference.SupportsTeleportVR}"); room.SupportsTeleportVR = reference.SupportsTeleportVR; } if (room.IsRRO != reference.IsRRO) { changes.Add(reference.IsRRO ? "marked IsRRO" : "no longer marked IsRRO"); room.IsRRO = reference.IsRRO; } var mine = subRooms.Where(sub => sub.RoomId == room.RoomId).OrderBy(sub => sub.SubRoomId).ToList(); var entry = mine.FirstOrDefault(); if (entry == null) { subRooms.Add(await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId, reference.MaxPlayers, reference.Accessibility, reference.SubRoomSandbox, reference.SaveName, reference.BundleName)); changes.Add($"added the {reference.SubRoomName} subroom"); } else { AlignSubRoom(changes, entry, reference.SubRoomName, reference.SceneId, reference.MaxPlayers, reference.Accessibility, reference.SubRoomSandbox); } foreach (var extra in reference.ExtraSubRooms) { var match = subRooms.FirstOrDefault(sub => sub.RoomId == room.RoomId && string.Equals(sub.UnitySceneId, extra.SceneId, StringComparison.OrdinalIgnoreCase)); if (match == null) { subRooms.Add(await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference), reference.Accessibility, reference.SubRoomSandbox)); changes.Add($"added the {extra.Name} subroom"); } else { AlignSubRoom(changes, match, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference), reference.Accessibility, reference.SubRoomSandbox); } } return changes; } private static void AlignSubRoom(List changes, SubRoom sub, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null) { var what = $"subroom {sub.Name}"; if (!string.Equals(sub.Name, name, StringComparison.Ordinal)) { changes.Add($"{what} renamed to {name}"); sub.Name = name; } if (!string.Equals(sub.UnitySceneId, sceneId, StringComparison.OrdinalIgnoreCase)) { changes.Add($"{what} scene {sub.UnitySceneId} -> {sceneId}"); sub.UnitySceneId = sceneId; } if (sub.MaxPlayers != maxPlayers) { changes.Add($"{what} max players {sub.MaxPlayers} -> {maxPlayers}"); sub.MaxPlayers = maxPlayers; } if (sub.Accessibility != accessibility) { changes.Add($"{what} accessibility {sub.Accessibility} -> {accessibility}"); sub.Accessibility = accessibility; } if (sub.IsSandbox != sandbox) { changes.Add($"{what} sandbox {sandbox}"); sub.IsSandbox = sandbox; } if (saveName != null && !string.Equals(sub.DataBlob, saveName, StringComparison.Ordinal)) { changes.Add($"{what} room file '{(sub.DataBlob.Length == 0 ? "none" : sub.DataBlob)}' -> '{saveName}'"); sub.DataBlob = saveName; } if (bundleName != null && !string.Equals(sub.UnityAssetId, bundleName, StringComparison.Ordinal)) { changes.Add($"{what} bundle '{(sub.UnityAssetId.Length == 0 ? "none" : sub.UnityAssetId)}' -> '{bundleName}'"); sub.UnityAssetId = bundleName; } } private static async Task AddSubRoomAsync(RecDBContext db, long roomId, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null) { var subRoom = new SubRoom { RoomId = roomId, SavedByAccountId = 1, Name = name, DataBlob = saveName ?? "", UnityAssetId = bundleName ?? "", IsSandbox = sandbox, MaxPlayers = maxPlayers, Accessibility = accessibility, UnitySceneId = sceneId, CreatedAt = DateTime.Now }; await db.SubRooms.AddAsync(subRoom); return subRoom; } private static int ExtraSubRoomSize(DefaultSubRoom subRoom, DefaultRoom room) => subRoom.MaxPlayers > 0 ? subRoom.MaxPlayers : room.MaxPlayers; private static Task IsAlignedAsync(RecDBContext db) => HasRunAsync(db, AlignmentKey, AlignmentVersion); private static Task MarkAlignedAsync(RecDBContext db) => MarkRunAsync(db, AlignmentKey, AlignmentVersion); private static async Task HasRunAsync(RecDBContext db, string key, int version) { var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key); return int.TryParse(row?.Value, out var recorded) && recorded >= version; } private static async Task MarkRunAsync(RecDBContext db, string key, int version) { var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key); if (row == null) await db.GameConfig.AddAsync(new ConfigVal(key, version.ToString())); else row.Value = version.ToString(); await db.SaveChangesAsync(); } private static bool Matches(string? existingName, DefaultRoom reference) { var key = NormalizeName(existingName); if (key.Length == 0) return false; return key == NormalizeName(reference.Name) || reference.Aliases.Any(alias => key == NormalizeName(alias)); } private static string NormalizeName(string? name) => string.Concat((name ?? "").Where(char.IsLetterOrDigit)).ToLowerInvariant(); private static List Rro() => new() { new ItemTag { Tag = "rro", Type = TagType.AGOnly } }; private static List RroWith(params string[] tags) { var list = Rro(); list.AddRange(tags.Select(tag => new ItemTag { Tag = tag, Type = TagType.General })); return list; } } public sealed class DefaultRoom { public string Name { get; init; } = ""; public string Description { get; init; } = ""; public int MaxPlayers { get; init; } public string SceneId { get; init; } = ""; public string SubRoomName { get; init; } = "Home"; public string? ImageName { get; init; } public string? SaveName { get; init; } public string? BundleName { get; init; } public IReadOnlyList Tags { get; init; } = Array.Empty(); public IReadOnlyList ExtraSubRooms { get; init; } = Array.Empty(); public IReadOnlyList Aliases { get; init; } = Array.Empty(); public Accessibility Accessibility { get; init; } = Accessibility.Public; public bool CloningAllowed { get => cloningAllowedOverride ?? Accessibility == Accessibility.Unlised; init => cloningAllowedOverride = value; } private bool? cloningAllowedOverride; public bool IsRRO { get; init; } = true; public bool DisableRoomComments { get; init; } public bool SubRoomSandbox { get; init; } public WarningMask WarningMask { get; init; } = WarningMask.None; public bool SupportsTeleportVR { get; init; } = true; } public sealed class DefaultSubRoom { public string Name { get; init; } = ""; public string SceneId { get; init; } = ""; public int MaxPlayers { get; init; } } }