// This file is provided under The MIT License as part of RiptideSteamTransport. // Copyright (c) Tom Weiland // For additional information please see the included LICENSE.md file or view it on GitHub: // https://github.com/tom-weiland/RiptideSteamTransport/blob/main/LICENSE.md using Steamworks; using System; using System.Threading.Tasks; using UnityEngine; namespace Riptide.Transports.Steam { public class SteamClient : SteamPeer, IClient { public event EventHandler Connected; public event EventHandler ConnectionFailed; public event EventHandler DataReceived; public event EventHandler Disconnected; private const string LocalHostName = "localhost"; private const string LocalHostIP = "127.0.0.1"; private SteamConnection steamConnection; private SteamServer localServer; private Callback connectionStatusChanged; public SteamClient(SteamServer localServer = null) { this.localServer = localServer; } public void ChangeLocalServer(SteamServer newLocalServer) { localServer = newLocalServer; } public bool Connect(string hostAddress, out Connection connection, out string connectError) { connection = null; try { //SteamGameServerNetworkingUtils.InitRelayNetworkAccess(); SteamNetworkingUtils.InitRelayNetworkAccess(); } catch (Exception ex) { connectError = $"Couldn't connect: {ex}"; return false; } connectError = $"Invalid host address '{hostAddress}'! Expected '{LocalHostIP}' or '{LocalHostName}' for local connections, or a valid Steam ID."; if (hostAddress == LocalHostIP || hostAddress == LocalHostName) { if (localServer == null) { connectError = $"No locally running server was specified to connect to! Either pass a {nameof(SteamServer)} instance to your {nameof(SteamClient)}'s constructor or call its {nameof(SteamClient.ChangeLocalServer)} method before attempting to connect locally."; connection = null; return false; } connection = steamConnection = ConnectLocal(); return true; } else if (ulong.TryParse(hostAddress, out ulong hostId)) { connection = steamConnection = TryConnect(new CSteamID(hostId)); return connection != null; } return false; } private SteamConnection ConnectLocal() { Debug.Log($"{LogName}: Connecting to locally running server..."); connectionStatusChanged = Callback.Create(OnConnectionStatusChanged); CSteamID playerSteamId = SteamUser.GetSteamID(); SteamNetworkingIdentity clientIdentity = new SteamNetworkingIdentity(); clientIdentity.SetSteamID(playerSteamId); SteamNetworkingIdentity serverIdentity = new SteamNetworkingIdentity(); serverIdentity.SetSteamID(playerSteamId); SteamNetworkingSockets.CreateSocketPair(out HSteamNetConnection connectionToClient, out HSteamNetConnection connectionToServer, false, ref clientIdentity, ref serverIdentity); localServer.Add(new SteamConnection(playerSteamId, connectionToClient, this)); OnConnected(); return new SteamConnection(playerSteamId, connectionToServer, this); } private SteamConnection TryConnect(CSteamID hostId) { try { connectionStatusChanged = Callback.Create(OnConnectionStatusChanged); SteamNetworkingIdentity serverIdentity = new SteamNetworkingIdentity(); serverIdentity.SetSteamID(hostId); SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { }; HSteamNetConnection connectionToServer = SteamNetworkingSockets.ConnectP2P(ref serverIdentity, 0, options.Length, options); ConnectTimeout(); return new SteamConnection(hostId, connectionToServer, this); } catch (Exception ex) { Debug.LogException(ex); OnConnectionFailed(); return null; } } private async void ConnectTimeout() // TODO: confirm if this is needed, Riptide *should* take care of timing out the connection { Task timeOutTask = Task.Delay(6000); // TODO: use Riptide Client's TimeoutTime await Task.WhenAny(timeOutTask); if (!steamConnection.IsConnected) OnConnectionFailed(); } private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t callback) { if (!callback.m_hConn.Equals(steamConnection.SteamNetConnection)) { // When connecting via local loopback connection to a locally running SteamServer (aka // this player is also the host), other external clients that attempt to connect seem // to trigger ConnectionStatusChanged callbacks for the locally connected client. Not // 100% sure why this is the case, but returning out of the callback here when the // connection doesn't match that between local client & server avoids the problem. return; } switch (callback.m_info.m_eState) { case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected: OnConnected(); break; case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer: SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Closed by peer", false); OnDisconnected(DisconnectReason.Disconnected); break; case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally: SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Problem detected", false); OnDisconnected(DisconnectReason.TransportError); break; default: Debug.Log($"{LogName}: Connection state changed - {callback.m_info.m_eState} | {callback.m_info.m_szEndDebug}"); break; } } public void Poll() { if (steamConnection != null) Receive(steamConnection); } // TODO: disable nagle so this isn't needed //public void Flush() //{ // foreach (SteamConnection connection in connections.Values) // SteamNetworkingSockets.FlushMessagesOnConnection(connection.SteamNetConnection); //} public void Disconnect() { if (connectionStatusChanged != null) { connectionStatusChanged.Dispose(); connectionStatusChanged = null; } SteamNetworkingSockets.CloseConnection(steamConnection.SteamNetConnection, 0, "Disconnected", false); steamConnection = null; } protected virtual void OnConnected() { Connected?.Invoke(this, EventArgs.Empty); } protected virtual void OnConnectionFailed() { ConnectionFailed?.Invoke(this, EventArgs.Empty); } protected override void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection) { DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, fromConnection)); } protected virtual void OnDisconnected(DisconnectReason reason) { Disconnected?.Invoke(this, new DisconnectedEventArgs(steamConnection, reason)); } } }