using Google.Protobuf; using RecRoom.Protobuf; namespace DorkNet.Server.Services; /// /// Builds the binary `PersistedRoomData` protobuf the client downloads when /// it loads a room scene. The 2020 RecRoom client gates the Maker Pen, /// invention spawning, room-role editing and a dozen other tools behind /// `RoomRoleCollectionData.RoomRoles[i].CanUseMakerPen.InnerValue` etc., /// all of which default to `false` if the bytes we serve are empty. /// /// We emit a minimal blob with one PlayerRoomRoleData entry that flips /// every relevant `OverridableBoolData` to `(overrides=true, inner_value=true)`, /// which the client picks up regardless of which RoomRoleId the local /// player ends up assigned to. Net result: the room's creator (and /// everyone else, since this is a private server) gets all build/admin /// tools the moment they spawn into the scene. /// /// Wire compatibility: our trimmed .proto only defines the messages and /// field numbers we set — the 2020 client's full PersistedRoomData has /// many more fields, but proto3 silently ignores unknown wire entries and /// defaults missing ones, so the bytes we generate deserialize cleanly. /// public class RoomDataBlobService { private const int December2020PersistenceVersion = 26; /// /// Cached bytes of the "all-permissions-on" blob. Identical for every /// room — there's no per-room state we vary yet, so building once at /// startup beats rebuilding on every download request. /// private readonly byte[] _allPermsBlob = BuildAllPermsBlob(); private readonly RoomRoleCollectionData _allPermsRoleData = BuildAllPermsRoleData(); private readonly byte[] _rroEditableBlob = BuildRroEditableBlob(); private readonly RoomRoleCollectionData _rroEditableRoleData = BuildRroEditableRoleData(); public byte[] GetDefaultBlob() => _allPermsBlob; public byte[] GetRroEditableBlob() => _rroEditableBlob; public byte[] OverlayAllPermsRoleData(byte[] existingBlob) { var msg = PersistedRoomData.Parser.ParseFrom(existingBlob); EnsureEditablePersistenceVersion(msg); msg.RoomRoleData = _allPermsRoleData.Clone(); return msg.ToByteArray(); } public byte[] OverlayRroEditableRoleData(byte[] existingBlob) { var msg = PersistedRoomData.Parser.ParseFrom(existingBlob); EnsureEditablePersistenceVersion(msg); msg.RoomRoleData = _rroEditableRoleData.Clone(); return msg.ToByteArray(); } private static byte[] BuildAllPermsBlob() { var msg = new PersistedRoomData { DEPRECATEDVersion = December2020PersistenceVersion, RoomRoleData = BuildAllPermsRoleData(), }; return msg.ToByteArray(); } private static RoomRoleCollectionData BuildAllPermsRoleData() { var collection = new RoomRoleCollectionData(); collection.RoomRoles.Add(BuildPermissiveRole(0, 100, "Creator")); return collection; } private static byte[] BuildRroEditableBlob() { var msg = new PersistedRoomData { DEPRECATEDVersion = December2020PersistenceVersion, RoomRoleData = BuildRroEditableRoleData(), }; return msg.ToByteArray(); } private static RoomRoleCollectionData BuildRroEditableRoleData() { var collection = new RoomRoleCollectionData(); collection.RoomRoles.Add(BuildPermissiveRole(2_097_152, 100, "Creator")); // AG_CREATOR collection.RoomRoles.Add(BuildPermissiveRole(4_194_304, 90, "Co-owner")); // AG_COOWNER collection.RoomRoles.Add(BuildPermissiveRole(8_388_608, 80, "Host")); // AG_HOST collection.RoomRoles.Add(BuildPermissiveRole(16_777_216, 70, "Moderator")); // AG_MODERATOR return collection; } private static PlayerRoomRoleData BuildPermissiveRole(int roleId, int rank, string name) { // OverridableBoolData(overrides=true, inner_value=true) — used for // every Can* field on the role. OverridableBoolData OverrideTrue() => new() { Overrides = true, InnerValue = true, }; return new PlayerRoomRoleData { // Identity fields — these are deprecated in the 2026 schema // but the 2020 client still reads them. DEPRECATEDRoleId = roleId, DEPRECATEDRoleRank = rank, DEPRECATEDIsRoleActive = true, DEPRECATEDIsAgRole = true, RoleName = name, RoleVersion = 1, RoleGuid = StableRoleGuid(roleId), // Display name override — keeps the watch's role-list UI from // showing a blank label. Name = new OverridableStringData { Overrides = true, InnerValue = name, }, // Permission grants. Each Overridable*Data(overrides=true, // inner_value=true) tells the client "the room has explicitly // configured this permission to true". Without overrides=true // the client falls back to its own defaults, which for most // perms is false. CanAssignRoles = OverrideTrue(), CanInvite = OverrideTrue(), CanStartGames = OverrideTrue(), CanTalk = OverrideTrue(), CanPrintPhotos = OverrideTrue(), CanEditRoomRoles = OverrideTrue(), CanSelfRevive = OverrideTrue(), CanEndGamesEarly = OverrideTrue(), CanChangeGameMode = OverrideTrue(), CanUseMakerPen = OverrideTrue(), CanUseDeleteAllButton = OverrideTrue(), CanSaveInventions = OverrideTrue(), DisableMicAutoMute = OverrideTrue(), CanUseShareCam = OverrideTrue(), // Vote-kick permission — int field, set to a permissive value. // 0 = AnyoneCanVoteKickAnyone in the 2020 enum (verified via // VoteKickPermission disasm). overrides=true to make sure the // client respects it rather than falling back to a stricter // default. VoteKickPermission = new OverridableIntData { Overrides = true, InnerValue = 0, }, }; } private static void EnsureEditablePersistenceVersion(PersistedRoomData msg) { if (msg.DEPRECATEDVersion < 20) { msg.DEPRECATEDVersion = December2020PersistenceVersion; } } private static string StableRoleGuid(int roleId) => GuidUtility.Create(GuidUtility.UrlNamespace, $"dorknet-room-role:{roleId}").ToString(); } internal static class GuidUtility { public static readonly Guid UrlNamespace = new("6ba7b811-9dad-11d1-80b4-00c04fd430c8"); public static Guid Create(Guid namespaceId, string name) { var namespaceBytes = namespaceId.ToByteArray(); SwapByteOrder(namespaceBytes); var nameBytes = System.Text.Encoding.UTF8.GetBytes(name); var data = new byte[namespaceBytes.Length + nameBytes.Length]; Buffer.BlockCopy(namespaceBytes, 0, data, 0, namespaceBytes.Length); Buffer.BlockCopy(nameBytes, 0, data, namespaceBytes.Length, nameBytes.Length); var hash = System.Security.Cryptography.SHA1.HashData(data); var newGuid = new byte[16]; Array.Copy(hash, 0, newGuid, 0, 16); newGuid[6] = (byte)((newGuid[6] & 0x0F) | 0x50); newGuid[8] = (byte)((newGuid[8] & 0x3F) | 0x80); SwapByteOrder(newGuid); return new Guid(newGuid); } private static void SwapByteOrder(byte[] guid) { (guid[0], guid[3]) = (guid[3], guid[0]); (guid[1], guid[2]) = (guid[2], guid[1]); (guid[4], guid[5]) = (guid[5], guid[4]); (guid[6], guid[7]) = (guid[7], guid[6]); } }