using System; using System.Collections.Generic; using System.Linq; using Campus_live_rewrite.api.dbs; using Campus_live_rewrite.api.server.Classes.db_class; using Newtonsoft.Json; namespace Campus_live_rewrite.api.server { /// /// What each role may do inside a room - the table the client applies when it spawns /// (GET /Room_server/photon_access_token -> Permissions), and the per-subroom overrides a /// room's managers save on top of it (PUT /rooms/{id}/subrooms/{sub}/permissions). /// Ported from the reference rooms.app.ts photonAccessToken / SubRoomPermissionsRequest. /// /// Entries are addressed by the (Permission, Role) PAIR: an override with a pair the defaults /// carry replaces that default in place (so the order never shifts under the client and no /// pair is listed twice with two values); one naming a new pair is appended. /// /// The defaults here are NOT the reference's. The reference hands CAN_USE_ROOM_RESET_BUTTON /// and CAN_USE_DELETE_ALL_BUTTON to Role 0 - everyone - which is why any player in the Rec /// Center could press Reset. Here those start at Moderator (ConfigCampus.RoomResetMinRole). /// public static class RoomPermissions { public class Entry { public bool Override { get; set; } = true; public string Permission { get; set; } = ""; public int Role { get; set; } public int Type { get; set; } /// A STRING, not a bool - usually "True"/"False", kept verbatim. public string Value { get; set; } = "True"; } public const string ResetButton = "CAN_USE_ROOM_RESET_BUTTON"; public const string DeleteAll = "CAN_USE_DELETE_ALL_BUTTON"; public const string SaveInventions = "CAN_SAVE_INVENTIONS"; public const string SpawnInventions = "CAN_SPAWN_INVENTIONS"; public const string PlayGizmos = "CAN_USE_PLAY_GIZMOS_TOGGLE"; public const string MakerPen = "CAN_USE_MAKER_PEN"; public const string Invite = "CAN_INVITE"; private static Entry P(string permission, int role, bool overrideFlag = true, string value = "True") => new() { Permission = permission, Role = role, Override = overrideFlag, Type = 0, Value = value }; /// The default table for one caller in one room. public static List Defaults(ulong accountId) { int everyone = (int)RoomDBClasses.Role_data.None; // 0 int mod = ConfigCampus.RoomResetMinRole; // 20 = Moderator int coOwner = (int)RoomDBClasses.Role_data.CoOwner; // 30 int host = (int)RoomDBClasses.Role_data.Host; // 10 int contributor = (int)RoomDBClasses.Role_data.Contributor; // 25 int creator = (int)RoomDBClasses.Role_data.Creator; // 255 // Every role gets an EXPLICIT row for the maker pen and the two destructive buttons. // The table used to carry grants only (and the maker pen's one row as Override:false, // i.e. "use the built-in default"), so a player whose role had no row - everyone at // Role 0, a host, a moderator - fell to the client's own default, and the client's // default lets the pen out. "You can pull it out anywhere." Now a role that may not // has a row saying False, and a role that may has a row saying True. // No developer shortcut: a dev used to get CAN_USE_MAKER_PEN True for EVERY role in every // room just for carrying the flag. The pen is a room permission - creator / co-owner / // contributor, or whatever the room's managers granted on the subroom - for everyone. var t = new List(); foreach (int role in new[] { everyone, host, mod, contributor, coOwner, creator }) { bool staff = role >= mod; // moderator and up bool builder = role >= coOwner; // co-owner and up (contributors build too, below) bool canBuild = builder || role == contributor; t.Add(P(MakerPen, role, value: canBuild ? "True" : "False")); t.Add(P(ResetButton, role, value: staff ? "True" : "False")); t.Add(P(DeleteAll, role, value: staff ? "True" : "False")); // Everyone: build with inventions and toggle play/build. t.Add(P(SaveInventions, role)); t.Add(P(SpawnInventions, role)); t.Add(P(PlayGizmos, role)); } return t; } /// Defaults with a subroom's stored overrides merged over them, by (Permission, Role). public static List Merge(List defaults, IEnumerable? overrides) { if (overrides == null) return defaults; foreach (var o in overrides) { if (string.IsNullOrWhiteSpace(o.Permission)) continue; var stored = new Entry { Override = true, Permission = o.Permission, Role = o.Role, Type = o.Type, Value = o.Value ?? "True" }; int i = defaults.FindIndex(p => p.Permission == stored.Permission && p.Role == stored.Role); if (i == -1) defaults.Add(stored); else defaults[i] = stored; } return defaults; } /// /// Apply a PUT body to a subroom's stored overrides. `Override:true` stores the pair's /// value; `Override:false` is the client's unchecked box - it DELETES the stored pair so /// it falls back to the default. Pairs not sent are left alone. Returns the new table. /// public static List Apply(List? current, IEnumerable changes) { var table = current?.Select(e => new Entry { Override = true, Permission = e.Permission, Role = e.Role, Type = e.Type, Value = e.Value }).ToList() ?? new List(); foreach (var c in changes) { if (string.IsNullOrWhiteSpace(c.Permission)) continue; table.RemoveAll(e => e.Permission == c.Permission && e.Role == c.Role); if (c.Override) table.Add(new Entry { Override = true, Permission = c.Permission, Role = c.Role, Type = c.Type, Value = c.Value ?? "True" }); } return table; } /// Parse the client's JSON array body. Null when it is not one. public static List? ParseBody(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return null; try { var arr = JsonConvert.DeserializeObject>(raw); return arr?.Where(e => e != null && !string.IsNullOrWhiteSpace(e.Permission)).ToList(); } catch { return null; } } // ---- "instant" --------------------------------------------------------------------- /// /// Tell a player's client its room changed - the SubscriptionUpdateRoom frame (id 15) the /// reference pushes after a role change, which makes the client re-read the room and the /// permissions it now grants them. Best-effort: the write has already committed. /// public static void PushRoomUpdate(ulong playerId, RoomDBClasses.RoomRoot? room) { if (room == null || playerId == 0) return; try { // The client dispatches this by its STRING name ("RoomUpdate" - reference // notification-types.ts); the numeric 15 was an unrelated id and silently dropped, // so a permission / role change only showed after re-entering the instance. var frame = new api.WebsocketEvents.Reponse { Id = "RoomUpdate", Msg = RoomBlobFilter.Strip(room) }; Notifications.FireAndForget(Notifications.SendToPlayer(playerId, JsonConvert.SerializeObject(frame)), "room update push"); } catch (Exception ex) { Console.WriteLine($"[room-perms] push to {playerId} failed: {ex.Message}"); } } /// /// A role change, pushed to EVERYONE standing in the room plus the affected player(s) /// wherever they are (reference: pushRoomUpdateToRoom). The room's Roles are what every /// client draws the nametag crown / host / moderator badge from, so telling only the /// affected player left everybody else seeing the old badge until they rejoined. /// public static void PushRoomUpdateToRoom(ulong roomId, RoomDBClasses.RoomRoot? room, params ulong[] alsoTell) { if (room == null) return; foreach (var pid in PlayersInRoom(roomId).Concat(alsoTell).Where(p => p != 0).Distinct()) PushRoomUpdate(pid, room); } /// Everyone currently standing in any instance of this room. public static List PlayersInRoom(ulong roomId) { var ids = new List(); try { foreach (var pid in PlayerDB.PlayerDBFile.GetCollection("players").FindAll().Select(p => p.playerid)) { var (online, inst) = HeartbeatDB.PeekPresence(pid); if (online && inst != null && (ulong)inst.roomId == roomId) ids.Add(pid); } } catch (Exception ex) { Console.WriteLine($"[room-perms] presence scan failed: {ex.Message}"); } return ids; } } }