using System; using System.Collections.Generic; using Unity.Services.Lobbies.Models; using UnityEngine; namespace Unity.BossRoom.UnityServices.Lobbies { /// /// A local wrapper around a lobby's remote data, with additional functionality for providing that data to UI elements and tracking local player objects. /// [Serializable] public sealed class LocalLobby { public event Action changed; /// /// Create a list of new LocalLobbies from the result of a lobby list query. /// public static List CreateLocalLobbies(QueryResponse response) { var retLst = new List(); foreach (var lobby in response.Results) { retLst.Add(Create(lobby)); } return retLst; } public static LocalLobby Create(Lobby lobby) { var data = new LocalLobby(); data.ApplyRemoteData(lobby); return data; } Dictionary m_LobbyUsers = new Dictionary(); public Dictionary LobbyUsers => m_LobbyUsers; public struct LobbyData { public string LobbyID { get; set; } public string LobbyCode { get; set; } public string RelayJoinCode { get; set; } public string LobbyName { get; set; } public bool Private { get; set; } public int MaxPlayerCount { get; set; } public LobbyData(LobbyData existing) { LobbyID = existing.LobbyID; LobbyCode = existing.LobbyCode; RelayJoinCode = existing.RelayJoinCode; LobbyName = existing.LobbyName; Private = existing.Private; MaxPlayerCount = existing.MaxPlayerCount; } public LobbyData(string lobbyCode) { LobbyID = null; LobbyCode = lobbyCode; RelayJoinCode = null; LobbyName = null; Private = false; MaxPlayerCount = -1; } } LobbyData m_Data; public LobbyData Data => new LobbyData(m_Data); public void AddUser(LocalLobbyUser user) { if (!m_LobbyUsers.ContainsKey(user.ID)) { DoAddUser(user); OnChanged(); } } void DoAddUser(LocalLobbyUser user) { m_LobbyUsers.Add(user.ID, user); user.changed += OnChangedUser; } public void RemoveUser(LocalLobbyUser user) { DoRemoveUser(user); OnChanged(); } void DoRemoveUser(LocalLobbyUser user) { if (!m_LobbyUsers.ContainsKey(user.ID)) { Debug.LogWarning($"Player {user.DisplayName}({user.ID}) does not exist in lobby: {LobbyID}"); return; } m_LobbyUsers.Remove(user.ID); user.changed -= OnChangedUser; } void OnChangedUser(LocalLobbyUser user) { OnChanged(); } void OnChanged() { changed?.Invoke(this); } public string LobbyID { get => m_Data.LobbyID; set { m_Data.LobbyID = value; OnChanged(); } } public string LobbyCode { get => m_Data.LobbyCode; set { m_Data.LobbyCode = value; OnChanged(); } } public string RelayJoinCode { get => m_Data.RelayJoinCode; set { m_Data.RelayJoinCode = value; OnChanged(); } } public string LobbyName { get => m_Data.LobbyName; set { m_Data.LobbyName = value; OnChanged(); } } public bool Private { get => m_Data.Private; set { m_Data.Private = value; OnChanged(); } } public int PlayerCount => m_LobbyUsers.Count; public int MaxPlayerCount { get => m_Data.MaxPlayerCount; set { m_Data.MaxPlayerCount = value; OnChanged(); } } public void CopyDataFrom(LobbyData data, Dictionary currUsers) { m_Data = data; if (currUsers == null) { m_LobbyUsers = new Dictionary(); } else { List toRemove = new List(); foreach (var oldUser in m_LobbyUsers) { if (currUsers.ContainsKey(oldUser.Key)) { oldUser.Value.CopyDataFrom(currUsers[oldUser.Key]); } else { toRemove.Add(oldUser.Value); } } foreach (var remove in toRemove) { DoRemoveUser(remove); } foreach (var currUser in currUsers) { if (!m_LobbyUsers.ContainsKey(currUser.Key)) { DoAddUser(currUser.Value); } } } OnChanged(); } public Dictionary GetDataForUnityServices() => new Dictionary() { {"RelayJoinCode", new DataObject(DataObject.VisibilityOptions.Public, RelayJoinCode)} }; public void ApplyRemoteData(Lobby lobby) { var info = new LobbyData(); // Technically, this is largely redundant after the first assignment, but it won't do any harm to assign it again. info.LobbyID = lobby.Id; info.LobbyCode = lobby.LobbyCode; info.Private = lobby.IsPrivate; info.LobbyName = lobby.Name; info.MaxPlayerCount = lobby.MaxPlayers; if (lobby.Data != null) { info.RelayJoinCode = lobby.Data.ContainsKey("RelayJoinCode") ? lobby.Data["RelayJoinCode"].Value : null; // By providing RelayCode through the lobby data with Member visibility, we ensure a client is connected to the lobby before they could attempt a relay connection, preventing timing issues between them. } else { info.RelayJoinCode = null; } var lobbyUsers = new Dictionary(); foreach (var player in lobby.Players) { if (player.Data != null) { if (LobbyUsers.ContainsKey(player.Id)) { lobbyUsers.Add(player.Id, LobbyUsers[player.Id]); continue; } } // If the player isn't connected to Relay, get the most recent data that the lobby knows. // (If we haven't seen this player yet, a new local representation of the player will have already been added by the LocalLobby.) var incomingData = new LocalLobbyUser { IsHost = lobby.HostId.Equals(player.Id), DisplayName = player.Data != null && player.Data.ContainsKey("DisplayName") ? player.Data["DisplayName"].Value : default, ID = player.Id }; lobbyUsers.Add(incomingData.ID, incomingData); } CopyDataFrom(info, lobbyUsers); } public void Reset(LocalLobbyUser localUser) { CopyDataFrom(new LobbyData(), new Dictionary()); AddUser(localUser); } } }