Files
megakoop/Game/Scripts/Networking/SteamCoopNetworkManager.cs

415 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using Steamworks;
using UnityEngine;
namespace MegaKoop.Game.Networking
{
/// <summary>
/// High level orchestrator for Steam lobby + P2P messaging. Keeps track of handlers per message type.
/// </summary>
[DisallowMultipleComponent]
public class SteamCoopNetworkManager : MonoBehaviour
{
public static SteamCoopNetworkManager Instance { get; private set; }
[SerializeField] private SteamLobbyManager lobbyManager;
[SerializeField] private SteamP2PTransport p2pTransport;
private readonly Dictionary<NetworkMessageType, Action<NetworkMessage>> handlers = new();
private readonly HashSet<ulong> lobbyMembers = new();
private readonly Dictionary<ulong, string> memberNames = new();
private readonly List<OnScreenMessage> 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<ulong> PlayerDisconnected;
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
if (lobbyManager == null)
{
lobbyManager = GetComponentInChildren<SteamLobbyManager>();
}
if (p2pTransport == null)
{
p2pTransport = GetComponentInChildren<SteamP2PTransport>();
}
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<NetworkMessage> handler)
{
if (handlers.TryGetValue(type, out Action<NetworkMessage> existing))
{
existing += handler;
handlers[type] = existing;
}
else
{
handlers[type] = handler;
}
}
public void UnregisterHandler(NetworkMessageType type, Action<NetworkMessage> handler)
{
if (!handlers.TryGetValue(type, out Action<NetworkMessage> 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<NetworkMessage> 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;
}
}
}