using System.Collections; using CampusNet.Protocol; namespace CampusNet.Services; public sealed class Actor { public int Nr; public string UserId = ""; public Connection? Conn; // null while inactive (disconnected with PlayerTtl) public readonly Hashtable Props = new(); public bool Inactive; public DateTime InactiveSince; public readonly HashSet Groups = new(); // interest groups (0 = always) public readonly List OwnCache = new(); // MergeCache / ReplaceCache per actor (legacy) // Traffic counters for the periodic room summary (see GameService.Sweep): what this actor // raised, how much of it was unreliable, per event code, and how many recipients it reached. public long Raised, RaisedUnreliable, Delivered, DroppedNoRecipient; public readonly Dictionary RaisedByCode = new(); public readonly Dictionary RaisedByGroup = new(); } public sealed class CachedEvent { public int ActorNr; public byte Code; public object? Data; public byte Group; } /// A game room: actors, properties, and the event cache PUN relies on for late joiners. public sealed class Room { public readonly string Name; public readonly Hashtable Props = new(); // byte system keys + custom keys public readonly SortedDictionary Actors = new(); public readonly List Cache = new(); // AddToRoomCache / AddToRoomCacheGlobal, in order public int NextActorNr = 1; public int PlayerTtl, EmptyRoomTtl; public DateTime EmptySince = DateTime.UtcNow; public readonly object Sync = new(); public readonly DateTime CreatedAt = DateTime.UtcNow; public Room(string name) { Name = name; } public int MaxPlayers => Props[GameProp.MaxPlayers] is byte b ? b : (Props[GameProp.MaxPlayers] is int i ? i : 0); public bool IsOpen => Props[GameProp.IsOpen] is bool o ? o : true; public bool IsVisible => Props[GameProp.IsVisible] is bool v ? v : true; public int ActiveCount => Actors.Values.Count(a => !a.Inactive); public int MasterClientId => Actors.Values.Where(a => !a.Inactive).Select(a => a.Nr).DefaultIfEmpty(0).Min(); public Actor? FindByUserId(string userId) => Actors.Values.FirstOrDefault(a => a.UserId == userId); /// Game properties as sent to clients: stored props plus the live system values. public Hashtable GamePropsForClient() { var h = new Hashtable(); foreach (DictionaryEntry e in Props) h[e.Key] = e.Value; h[GameProp.MasterClientId] = MasterClientId; h[GameProp.PlayerCount] = (byte)ActiveCount; if (!h.ContainsKey(GameProp.IsOpen)) h[GameProp.IsOpen] = true; if (!h.ContainsKey(GameProp.IsVisible)) h[GameProp.IsVisible] = true; return h; } /// All players' properties keyed by actor number (what the join response carries). public Hashtable AllPlayerProps() { var h = new Hashtable(); foreach (var a in Actors.Values) h[a.Nr] = PlayerPropsForClient(a); return h; } public static Hashtable PlayerPropsForClient(Actor a) { var h = new Hashtable(); foreach (DictionaryEntry e in a.Props) h[e.Key] = e.Value; h[PlayerProp.UserId] = a.UserId; h[PlayerProp.IsInactive] = a.Inactive; if (!h.ContainsKey(PlayerProp.NickName)) h[PlayerProp.NickName] = ""; return h; } public int[] ActorList() => Actors.Values.Where(a => !a.Inactive).Select(a => a.Nr).ToArray(); /// Copy props from a client hashtable into a store, honouring the "null removes" rule; returns the changed subset. public static Hashtable Merge(Hashtable target, Hashtable incoming) { var changed = new Hashtable(); foreach (DictionaryEntry e in incoming) { if (e.Value == null) { if (target.ContainsKey(e.Key)) { target.Remove(e.Key); changed[e.Key] = null; } } else { target[e.Key] = e.Value; changed[e.Key] = e.Value; } } return changed; } /// Check-and-set: every expected value must match the current one (both absent counts as match). public static bool CasMatches(Hashtable target, Hashtable? expected) { if (expected == null) return true; foreach (DictionaryEntry e in expected) { var cur = target[e.Key]; if (!ValuesEqual(cur, e.Value)) return false; } return true; } public static bool ValuesEqual(object? a, object? b) { if (a == null || b == null) return a == null && b == null; if (a is byte[] ab && b is byte[] bb) return ab.AsSpan().SequenceEqual(bb); if (a is Array aa && b is Array ba) { if (aa.Length != ba.Length) return false; for (int i = 0; i < aa.Length; i++) if (!ValuesEqual(aa.GetValue(i), ba.GetValue(i))) return false; return true; } if (a is CustomValue ca && b is CustomValue cb) return ca.Code == cb.Code && ca.Data.AsSpan().SequenceEqual(cb.Data); try { return Convert.ToDouble(a) == Convert.ToDouble(b); } catch { } return a.Equals(b); } } /// Every room this process hosts (master and game share it - one process). public static class Rooms { private static readonly Dictionary _rooms = new(); public static readonly object Sync = new(); public static Room? Get(string name) { lock (Sync) return _rooms.TryGetValue(name, out var r) ? r : null; } public static Room GetOrCreate(string name, out bool created) { lock (Sync) { if (_rooms.TryGetValue(name, out var r)) { created = false; return r; } r = new Room(name); _rooms[name] = r; created = true; return r; } } public static void Remove(string name) { lock (Sync) _rooms.Remove(name); } public static List All() { lock (Sync) return _rooms.Values.ToList(); } public static int Count { get { lock (Sync) return _rooms.Count; } } }