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

829 lines
31 KiB
C#

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<DefaultRoom> All { get; } = new List<DefaultRoom>
{
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<DefaultSubRoom>
{
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<DefaultSubRoom>
{
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<DefaultSubRoom>
{
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<ItemTag>
{
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<string>();
var aligned = new List<string>();
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<string>();
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<string>();
var on = new List<string>();
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<RoomData> CreateAsync(RecDBContext db, DefaultRoom reference, List<long> 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<List<string>> AlignAsync(RecDBContext db, RoomData room, List<SubRoom> subRooms, DefaultRoom reference)
{
var changes = new List<string>();
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<string> 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<SubRoom> 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<bool> IsAlignedAsync(RecDBContext db) => HasRunAsync(db, AlignmentKey, AlignmentVersion);
private static Task MarkAlignedAsync(RecDBContext db) => MarkRunAsync(db, AlignmentKey, AlignmentVersion);
private static async Task<bool> 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<ItemTag> Rro() => new() { new ItemTag { Tag = "rro", Type = TagType.AGOnly } };
private static List<ItemTag> 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<ItemTag> Tags { get; init; } = Array.Empty<ItemTag>();
public IReadOnlyList<DefaultSubRoom> ExtraSubRooms { get; init; } = Array.Empty<DefaultSubRoom>();
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
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; }
}
}