using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using RebootKit.Engine.Network; using Steamworks; using Steamworks.Data; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Steam { public class SteamNetworkManager : INetworkManager, ISocketManager, IConnectionManager { static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager)); public const ulong k_ServerClientID = 0; const int k_DefaultPort = 419; class Client { public ulong ClientID; public Connection Connection; } readonly Dictionary m_Clients = new Dictionary(); SocketManager m_SocketManager; ConnectionManager m_ConnectionManager; float m_StatsRefreshTimer; public INetworkManagerDelegate Delegate { get; set; } public ulong LocalClientID { get { return SteamClient.SteamId; } } public NetworkManagerStats Stats { get; } = new NetworkManagerStats(); // @NOTE: Set this before starting the client to specify which server to connect to. public ulong TargetSteamID; public SteamNetworkManager() { SteamNetworkingUtils.InitRelayNetworkAccess(); SteamNetworkingUtils.OnDebugOutput += SteamDebugOutput; SteamNetworkingUtils.DebugLevel = NetDebugOutput.None; } public void Dispose() { SteamNetworkingUtils.OnDebugOutput -= SteamDebugOutput; StopHost(); Disconnect(); } void SteamDebugOutput(NetDebugOutput output, string msg) { s_Logger.Info($"Output: {output} - {msg}"); } // // @MARK: Server // public bool IsServer() { return m_SocketManager != null; } public bool StartHost() { try { TargetSteamID = SteamClient.SteamId; m_SocketManager = SteamNetworkingSockets.CreateRelaySocket(k_DefaultPort); m_SocketManager.Interface = this; m_ConnectionManager = SteamNetworkingSockets.ConnectRelay(TargetSteamID, k_DefaultPort); m_ConnectionManager.Interface = this; SteamFriends.SetRichPresence("connect", LocalClientID.ToString()); Stats.Reset(); Delegate?.OnServerStarted(); Delegate?.OnClientStarted(); } catch (Exception e) { s_Logger.Error($"Failed to create server: {e.Message}"); m_SocketManager = null; return false; } return true; } public void StopHost() { if (!IsServer()) { return; } s_Logger.Info("Stopping server..."); SteamFriends.SetRichPresence("connect", null); foreach (Client client in m_Clients.Values) { client.Connection.Close(); } m_Clients.Clear(); m_SocketManager.Close(); m_SocketManager = null; Delegate?.OnClientStopped(); Delegate?.OnServerStopped(); } // // @MARK: Client // public bool IsClient() { return m_ConnectionManager != null; } public bool StartClient() { if (IsClient()) { s_Logger.Error("Cannot start client while already connected."); return false; } s_Logger.Info("Connecting to server with steam ID: " + TargetSteamID); try { m_ConnectionManager = SteamNetworkingSockets.ConnectRelay(TargetSteamID, k_DefaultPort); m_ConnectionManager.Interface = this; Stats.Reset(); Delegate?.OnClientStarted(); } catch (Exception e) { s_Logger.Error($"Failed to connect to server with ID {TargetSteamID}: {e.Message}"); m_ConnectionManager = null; return false; } return true; } public void Disconnect() { if (!IsClient()) { return; } s_Logger.Info("Disconnecting from server..."); m_ConnectionManager.Close(); m_ConnectionManager = null; Delegate?.OnClientStopped(); } // // @MARK: Data Sending/Receiving // public void Send(ulong clientID, NativeSlice data, SendMode mode) { if (data.Length == 0) { s_Logger.Error("Cannot send empty data."); return; } unsafe { Send(clientID, (byte*) data.GetUnsafePtr(), data.Length, mode); } } public unsafe void Send(ulong clientID, byte* data, int length, SendMode mode) { if (IsServer()) { if (clientID == k_ServerClientID || clientID == LocalClientID) { NativeArray dataArray = new NativeArray(length, Allocator.Temp); UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), data, length); MessageReceived(LocalClientID, dataArray); return; } if (!m_Clients.TryGetValue(clientID, out Client client)) { s_Logger.Error($"Client with ID {clientID} not found."); return; } Result sendResult = client.Connection.SendMessage((IntPtr) data, length, GetSteamSendType(mode)); if (sendResult == Result.OK) { if (mode == SendMode.Reliable) { Stats.ReliableBytesSent += (ulong) length; } else if (mode == SendMode.Unreliable) { Stats.UnreliableBytesSent += (ulong) length; } } else { s_Logger.Error($"Failed to send message to clientID, {clientID}. Result: {sendResult}."); } } else if (IsClient()) { if (clientID != k_ServerClientID) { s_Logger.Error($"Client can only send messages to the server (clientID must be {k_ServerClientID.ToString()})."); return; } Result sendResult = m_ConnectionManager.Connection.SendMessage((IntPtr) data, length, GetSteamSendType(mode)); if (sendResult == Result.OK) { if (mode == SendMode.Reliable) { Stats.ReliableBytesSent += (ulong) length; } else if (mode == SendMode.Unreliable) { Stats.UnreliableBytesSent += (ulong) length; } } else { s_Logger.Error($"Failed to send message to server. Result: {sendResult}. Flushing and retrying..."); } } else { s_Logger.Error("Cannot send data, server or client is not started."); } } void MessageReceived(ulong senderID, NativeArray data) { Delegate?.OnMessageReceived(senderID, data); } public void Tick() { m_SocketManager?.Receive(); m_ConnectionManager?.Receive(); m_StatsRefreshTimer -= UnityEngine.Time.deltaTime; if (m_StatsRefreshTimer <= 0f) { m_StatsRefreshTimer = 1.0f; Stats.ReliableBytesSentPerSecond = Stats.ReliableBytesSent; Stats.UnreliableBytesSentPerSecond = Stats.UnreliableBytesSent; Stats.BytesReceivedPerSecond = Stats.BytesReceived; Stats.ReliableBytesSent = 0; Stats.UnreliableBytesSent = 0; Stats.BytesReceived = 0; } } // // @MARK: IConnectionManager // void ISocketManager.OnConnecting(Connection connection, ConnectionInfo info) { s_Logger.Info($"OnConnecting: {connection.Id} - {info.Identity}"); connection.Accept(); } void ISocketManager.OnConnected(Connection connection, ConnectionInfo info) { if (!m_Clients.ContainsKey(info.Identity.SteamId)) { s_Logger.Info($"OnConnected: {connection.Id} - {info.Identity}"); Client client = new Client { ClientID = info.Identity.SteamId, Connection = connection }; m_Clients.Add(info.Identity.SteamId, client); Delegate?.OnClientConnected(client.ClientID); } else { s_Logger.Error("Failed to add client, already exists: " + info.Identity.SteamId); connection.Close(); } } void ISocketManager.OnDisconnected(Connection connection, ConnectionInfo info) { s_Logger.Info($"OnDisconnected: {connection.Id} - {info.Identity}"); if (m_Clients.Remove(info.Identity.SteamId)) { Delegate?.OnClientDisconnected(info.Identity.SteamId); } else { s_Logger.Error("Failed to remove client, not found: " + info.Identity.SteamId); } } void ISocketManager.OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) { NativeArray dataArray = new NativeArray(size, Allocator.Temp); unsafe { UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size); } Stats.BytesReceived += (ulong) size; MessageReceived(identity.SteamId, dataArray); } // // @MARK: ISocketManager // void IConnectionManager.OnConnecting(ConnectionInfo info) { } void IConnectionManager.OnConnected(ConnectionInfo info) { Delegate?.OnClientConnected(info.Identity.SteamId); } void IConnectionManager.OnDisconnected(ConnectionInfo info) { Delegate?.OnClientDisconnected(info.Identity.SteamId); } void IConnectionManager.OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { NativeArray dataArray = new NativeArray(size, Allocator.Temp); unsafe { UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size); } Stats.BytesReceived += (ulong) size; MessageReceived(k_ServerClientID, dataArray); } // // @MARK: Utilities // [MethodImpl(MethodImplOptions.AggressiveInlining)] static SendType GetSteamSendType(SendMode mode) { return mode switch { SendMode.Reliable => SendType.Reliable, SendMode.Unreliable => SendType.Unreliable, _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) }; } } // class SteamNetworkManager : INetworkManager { // static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager)); // // const int k_DefaultPort = 420; // // SocketManager m_SocketManager; // ConnectionManager m_ConnectionManager; // IDisposable m_TickDisposable; // // SteamId m_HostSteamID; // // public void Initialize(INetworkManagerEventHandler eventHandler) { // m_TickDisposable = Observable.EveryUpdate() // .Subscribe(_ => Tick()); // // SteamNetworkingUtils.DebugLevel = NetDebugOutput.Debug; // // SteamNetworkingSockets.OnConnectionStatusChanged += OnConnectionStatusChanged; // SteamNetworkingUtils.OnDebugOutput += OnSteamNetworkDebugOutput; // // #if !RR_LOCALHOST_ONLY // SteamNetworkingUtils.InitRelayNetworkAccess(); // #endif // } // // public void Shutdown() { // Disconnect(); // // SteamNetworkingUtils.OnDebugOutput -= OnSteamNetworkDebugOutput; // SteamNetworkingSockets.OnConnectionStatusChanged -= OnConnectionStatusChanged; // // m_TickDisposable.Dispose(); // } // // public bool IsServer() { // return m_SocketManager != null; // } // // public bool IsClient() { // return m_ConnectionManager != null; // } // // public bool StartServer() { // Disconnect(); // // s_Logger.Info("Creating server..."); // // try { // m_HostSteamID = SteamNetworkingSockets.Identity.SteamId; // // #if RR_LOCALHOST_ONLY // m_SocketManager = SteamNetworkingSockets.CreateNormalSocket(NetAddress.LocalHost(2137), new ServerCallbacks()); // m_ConnectionManager = SteamNetworkingSockets.ConnectNormal(NetAddress.LocalHost(2137), new ClientCallbacks()); // #else // m_SocketManager = SteamNetworkingSockets.CreateRelaySocket(k_DefaultPort); // m_SocketManager.Interface = new ServerCallbacks(); // m_ConnectionManager = // SteamNetworkingSockets.ConnectRelay(m_HostSteamID, k_DefaultPort); // m_ConnectionManager.Interface = new ClientCallbacks(); // #endif // } catch (Exception e) { // s_Logger.Error($"Failed to create server: {e.Message}"); // m_SocketManager = null; // m_ConnectionManager = null; // // return false; // } // // return true; // } // // public void StopServer() { // Disconnect(); // } // // public bool Connect(ulong serverID) { // if (IsServer()) { // s_Logger.Error("Cannot connect to a server while running as a server."); // return false; // } // // Disconnect(); // // s_Logger.Info("Connecting to server with steam ID: " + serverID); // try { // #if RR_LOCALHOST_ONLY // m_ConnectionManager = // SteamNetworkingSockets.ConnectNormal(NetAddress.LocalHost(2137), new ClientCallbacks()); // #else // m_ConnectionManager = SteamNetworkingSockets.ConnectRelay(serverID, k_DefaultPort); // m_ConnectionManager.Interface = new ClientCallbacks(); // #endif // // m_HostSteamID = serverID; // } catch (Exception e) { // s_Logger.Error($"Failed to connect to server with ID {serverID}: {e.Message}"); // m_ConnectionManager = null; // // return false; // } // // return true; // } // // public void Disconnect() { // if (m_ConnectionManager != null) { // s_Logger.Info("Disconnecting from the server..."); // m_ConnectionManager.Close(); // m_ConnectionManager = null; // } // // if (m_SocketManager != null) { // s_Logger.Info("Shutting down the server..."); // m_SocketManager.Close(); // m_SocketManager = null; // } // } // public void Send(ulong clientID, NetworkMessage message, SendMode mode) { // if (!IsServer()) { // return; // } // // foreach (Connection connection in m_SocketManager.Connected) { // connection.SendMessage(new byte[] { // 0xDE, // 0xAD, // 0xBE, // 0xEF // }, SendType.Reliable); // } // } // // public void Broadcast(NetworkMessage message, SendMode mode) { // if (!IsServer()) { // return; // } // // foreach (Connection connection in m_SocketManager.Connected) { // connection.SendMessage(new byte[] { // 0xDE, // 0xAD, // 0xBE, // 0xEF // }, SendType.Reliable); // } // } // // void OnSteamNetworkDebugOutput(NetDebugOutput level, string message) { // LogLevel logLevel = level switch { // NetDebugOutput.Debug => LogLevel.Debug, // NetDebugOutput.Msg => LogLevel.Info, // NetDebugOutput.Warning => LogLevel.Warning, // NetDebugOutput.Error => LogLevel.Error, // _ => LogLevel.Info // }; // // s_Logger.Log(logLevel, message); // } // // void Tick() { // m_SocketManager?.Receive(); // m_ConnectionManager?.Receive(); // } // // void OnConnectionStatusChanged(Connection connection, ConnectionInfo info) { // s_Logger.Info($"OnConnectionStatusChanged: {connection.Id} - {info.Identity} - Status: {info.State}"); // } // // class ServerCallbacks : ISocketManager { // public void OnConnecting(Connection connection, ConnectionInfo data) { // s_Logger.Info($"OnConnecting: {connection.Id} - {data.Identity}"); // // connection.Accept(); // } // // public void OnConnected(Connection connection, ConnectionInfo data) { // s_Logger.Info($"OnConnected: {connection.Id} - {data.Identity}"); // // connection.SendMessage(new byte[] { // 0xBE, // 0xFE, // 0x00, // 0x00 // }, 0, 4, 0); // } // // public void OnDisconnected(Connection connection, ConnectionInfo data) { // s_Logger.Info($"OnDisconnected: {connection.Id} - {data.Identity}"); // } // // public void OnMessage(Connection connection, // NetIdentity identity, // IntPtr data, // int size, // long messageNum, // long recvTime, // int channel) { // byte[] buffer = new byte[size]; // System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size); // // s_Logger.Info($"OnMessage: {connection.Id} - {identity} - Size: {size} - Channel: {channel}"); // } // } // // class ClientCallbacks : IConnectionManager { // public void OnConnected(ConnectionInfo info) { // s_Logger.Info("ConnectionOnConnected"); // } // // public void OnConnecting(ConnectionInfo info) { // s_Logger.Info("ConnectionOnConnecting"); // } // // public void OnDisconnected(ConnectionInfo info) { // s_Logger.Info("ConnectionOnDisconnected"); // } // // public void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) { // byte[] buffer = new byte[size]; // System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size); // // s_Logger.Info($"OnMessage: Size: {size} - Channel: {channel}"); // } // } // } }