using System; using System.Collections.Generic; using Steamworks; using UnityEngine; namespace MegaKoop.Game.Networking { /// /// High level orchestrator for Steam lobby + P2P messaging. Keeps track of handlers per message type. /// [DisallowMultipleComponent] public class SteamCoopNetworkManager : MonoBehaviour { public static SteamCoopNetworkManager Instance { get; private set; } [SerializeField] private SteamLobbyManager lobbyManager; [SerializeField] private SteamP2PTransport p2pTransport; private readonly Dictionary> handlers = new(); private readonly HashSet lobbyMembers = new(); private readonly Dictionary memberNames = new(); private readonly List onScreenMessages = new(); private bool isHost; private bool isConnected; private ulong lobbyOwnerSteamId; public bool IsHost => isHost; public bool IsConnected => isConnected; public CSteamID ActiveLobby => lobbyManager != null ? lobbyManager.GetActiveLobby() : CSteamID.Nil; public event Action HostClosed; public event Action PlayerDisconnected; private void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); if (lobbyManager == null) { lobbyManager = GetComponentInChildren(); } if (p2pTransport == null) { p2pTransport = GetComponentInChildren(); } if (lobbyManager != null) { lobbyManager.LobbyCreated += HandleLobbyCreated; lobbyManager.LobbyJoined += HandleLobbyJoined; lobbyManager.LobbyMemberJoined += HandleLobbyMemberJoined; lobbyManager.LobbyMemberLeft += HandleLobbyMemberLeft; } if (p2pTransport != null) { p2pTransport.MessageReceived += DispatchMessage; } } private void OnDestroy() { if (Instance == this) { Instance = null; } if (lobbyManager != null) { lobbyManager.LobbyCreated -= HandleLobbyCreated; lobbyManager.LobbyJoined -= HandleLobbyJoined; lobbyManager.LobbyMemberJoined -= HandleLobbyMemberJoined; lobbyManager.LobbyMemberLeft -= HandleLobbyMemberLeft; } if (p2pTransport != null) { p2pTransport.MessageReceived -= DispatchMessage; } } public void RegisterHandler(NetworkMessageType type, Action handler) { if (handlers.TryGetValue(type, out Action existing)) { existing += handler; handlers[type] = existing; } else { handlers[type] = handler; } } public void UnregisterHandler(NetworkMessageType type, Action handler) { if (!handlers.TryGetValue(type, out Action existing)) { return; } existing -= handler; if (existing == null) { handlers.Remove(type); } else { handlers[type] = existing; } } public void SendToAll(NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable) { p2pTransport?.Broadcast(type, payload, sendType); } public void SendToPlayer(CSteamID target, NetworkMessageType type, byte[] payload, EP2PSend sendType = EP2PSend.k_EP2PSendReliable) { p2pTransport?.Send(target, type, payload, sendType); } public void SynchronizeWithLobby(MegaKoop.Steam.SteamLobbyService steamService) { if (steamService == null) { return; } isConnected = steamService.IsInLobby; isHost = steamService.IsHost; if (steamService.IsInLobby) { lobbyMembers.Clear(); memberNames.Clear(); lobbyOwnerSteamId = 0; var members = steamService.GetMembers(); if (members != null) { foreach (var member in members) { if (!ulong.TryParse(member.steamId, out ulong steamId)) { continue; } lobbyMembers.Add(steamId); memberNames[steamId] = string.IsNullOrWhiteSpace(member.name) ? steamId.ToString() : member.name; if (member.isHost) { lobbyOwnerSteamId = steamId; } } } if (lobbyOwnerSteamId == 0 && steamService.IsHost && SteamBootstrap.IsInitialized) { lobbyOwnerSteamId = SteamUser.GetSteamID().m_SteamID; } } else { lobbyMembers.Clear(); memberNames.Clear(); lobbyOwnerSteamId = 0; } #if STEAMWORKSNET if (p2pTransport != null) { if (steamService.IsInLobby) { CSteamID lobbyId = steamService.LobbyId; if (lobbyId != CSteamID.Nil) { p2pTransport.SetActiveLobby(lobbyId); } else if (ulong.TryParse(steamService.LobbyIdString, out ulong lobbyValue) && lobbyValue != 0) { p2pTransport.SetActiveLobby(new CSteamID(lobbyValue)); } } else { p2pTransport.SetActiveLobby(CSteamID.Nil); } } #endif } private void DispatchMessage(NetworkMessage message) { if (handlers.TryGetValue(message.Type, out Action handler)) { handler?.Invoke(message); } } private void HandleLobbyCreated(CSteamID lobbyId) { isHost = true; isConnected = true; p2pTransport?.SetActiveLobby(lobbyId); lobbyOwnerSteamId = SteamUser.GetSteamID().m_SteamID; lobbyMembers.Clear(); memberNames.Clear(); RegisterMember(lobbyOwnerSteamId); } private void HandleLobbyJoined(CSteamID lobbyId) { isConnected = true; p2pTransport?.SetActiveLobby(lobbyId); string ownerId = SteamMatchmaking.GetLobbyData(lobbyId, "owner"); if (!string.IsNullOrEmpty(ownerId) && ulong.TryParse(ownerId, out ulong ownerSteamId)) { lobbyOwnerSteamId = ownerSteamId; isHost = ownerSteamId == SteamUser.GetSteamID().m_SteamID; } else { CSteamID owner = SteamMatchmaking.GetLobbyOwner(lobbyId); lobbyOwnerSteamId = owner.m_SteamID; isHost = owner == SteamUser.GetSteamID(); } RegisterExistingMembers(lobbyId); } private void HandleLobbyMemberJoined(CSteamID member) { RegisterMember(member.m_SteamID); if (ActiveLobby != CSteamID.Nil) { CSteamID owner = SteamMatchmaking.GetLobbyOwner(ActiveLobby); if (owner != CSteamID.Nil) { lobbyOwnerSteamId = owner.m_SteamID; } } Debug.Log("[SteamCoopNetworkManager] Member joined: " + member); } private void HandleLobbyMemberLeft(CSteamID member) { ulong steamId = member.m_SteamID; string displayName = ResolveMemberName(steamId); bool hostLeft = lobbyOwnerSteamId != 0 && steamId == lobbyOwnerSteamId; lobbyMembers.Remove(steamId); memberNames.Remove(steamId); if (hostLeft) { Debug.Log("[SteamCoopNetworkManager] Host left lobby: " + member); ShowOnScreenMessage("Host closed the game.", 6f); HostClosed?.Invoke(); if (!isHost) { ForceDisconnectFromLobby(); } } else { Debug.Log("[SteamCoopNetworkManager] Member left: " + member); string message = string.IsNullOrEmpty(displayName) ? "A player disconnected." : $"{displayName} disconnected."; ShowOnScreenMessage(message, 4f); PlayerDisconnected?.Invoke(steamId); } } private void Update() { if (onScreenMessages.Count == 0) { return; } float now = Time.unscaledTime; for (int i = onScreenMessages.Count - 1; i >= 0; i--) { if (onScreenMessages[i].ExpiresAt <= now) { onScreenMessages.RemoveAt(i); } } } private void OnGUI() { if (onScreenMessages.Count == 0) { return; } float y = 20f; foreach (var entry in onScreenMessages) { if (entry.ExpiresAt <= Time.unscaledTime) { continue; } GUIContent content = new(entry.Text); Vector2 size = GUI.skin.box.CalcSize(content); Rect rect = new(20f, y, Mathf.Max(220f, size.x + 20f), size.y + 10f); GUI.Box(rect, content); y += rect.height + 6f; } } private void RegisterExistingMembers(CSteamID lobbyId) { lobbyMembers.Clear(); memberNames.Clear(); int count = SteamMatchmaking.GetNumLobbyMembers(lobbyId); for (int i = 0; i < count; i++) { CSteamID member = SteamMatchmaking.GetLobbyMemberByIndex(lobbyId, i); RegisterMember(member.m_SteamID); } } private void RegisterMember(ulong steamId) { if (steamId == 0) { return; } lobbyMembers.Add(steamId); memberNames[steamId] = ResolveMemberNameInternal(steamId); } private string ResolveMemberName(ulong steamId) { if (memberNames.TryGetValue(steamId, out string cached) && !string.IsNullOrEmpty(cached)) { return cached; } return ResolveMemberNameInternal(steamId); } private string ResolveMemberNameInternal(ulong steamId) { #if STEAMWORKSNET if (steamId != 0 && SteamBootstrap.IsInitialized) { try { return SteamFriends.GetFriendPersonaName(new CSteamID(steamId)); } catch { // Ignore Steam API hiccups and fall back to id string. } } #endif return steamId != 0 ? steamId.ToString() : string.Empty; } private void ShowOnScreenMessage(string message, float lifetimeSeconds) { if (string.IsNullOrWhiteSpace(message)) { return; } float expiry = Time.unscaledTime + Mathf.Max(0.5f, lifetimeSeconds); onScreenMessages.Add(new OnScreenMessage { Text = message, ExpiresAt = expiry }); } private void ForceDisconnectFromLobby() { if (ActiveLobby != CSteamID.Nil && SteamBootstrap.IsInitialized) { SteamMatchmaking.LeaveLobby(ActiveLobby); } lobbyMembers.Clear(); memberNames.Clear(); lobbyOwnerSteamId = 0; isConnected = false; p2pTransport?.SetActiveLobby(CSteamID.Nil); } private struct OnScreenMessage { public string Text; public float ExpiresAt; } } }