first commit
This commit is contained in:
204
RiptideSteamTransport/Transport/SteamClient.cs
Normal file
204
RiptideSteamTransport/Transport/SteamClient.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
// 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<DataReceivedEventArgs> DataReceived;
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
private const string LocalHostName = "localhost";
|
||||
private const string LocalHostIP = "127.0.0.1";
|
||||
|
||||
private SteamConnection steamConnection;
|
||||
private SteamServer localServer;
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> 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<SteamNetConnectionStatusChangedCallback_t>.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<SteamNetConnectionStatusChangedCallback_t>.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
72
RiptideSteamTransport/Transport/SteamConnection.cs
Normal file
72
RiptideSteamTransport/Transport/SteamConnection.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
// 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.Collections.Generic;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public class SteamConnection : Connection, IEquatable<SteamConnection>
|
||||
{
|
||||
public readonly CSteamID SteamId;
|
||||
public readonly HSteamNetConnection SteamNetConnection;
|
||||
|
||||
internal bool DidReceiveConnect;
|
||||
|
||||
private readonly SteamPeer peer;
|
||||
|
||||
internal SteamConnection(CSteamID steamId, HSteamNetConnection steamNetConnection, SteamPeer peer)
|
||||
{
|
||||
SteamId = steamId;
|
||||
SteamNetConnection = steamNetConnection;
|
||||
this.peer = peer;
|
||||
}
|
||||
|
||||
protected internal override void Send(byte[] dataBuffer, int amount)
|
||||
{
|
||||
peer.Send(dataBuffer, amount, SteamNetConnection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => SteamNetConnection.ToString();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object obj) => Equals(obj as SteamConnection);
|
||||
/// <inheritdoc/>
|
||||
public bool Equals(SteamConnection other)
|
||||
{
|
||||
if (other is null)
|
||||
return false;
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
return true;
|
||||
|
||||
return SteamNetConnection.Equals(other.SteamNetConnection);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -721414014 + EqualityComparer<HSteamNetConnection>.Default.GetHashCode(SteamNetConnection);
|
||||
}
|
||||
|
||||
public static bool operator ==(SteamConnection left, SteamConnection right)
|
||||
{
|
||||
if (left is null)
|
||||
{
|
||||
if (right is null)
|
||||
return true;
|
||||
|
||||
return false; // Only the left side is null
|
||||
}
|
||||
|
||||
// Equals handles case of null on right side
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(SteamConnection left, SteamConnection right) => !(left == right);
|
||||
}
|
||||
}
|
||||
69
RiptideSteamTransport/Transport/SteamPeer.cs
Normal file
69
RiptideSteamTransport/Transport/SteamPeer.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public abstract class SteamPeer
|
||||
{
|
||||
/// <summary>The name to use when logging messages via <see cref="Utils.RiptideLogger"/>.</summary>
|
||||
public const string LogName = "STEAM";
|
||||
|
||||
protected const int MaxMessages = 256;
|
||||
|
||||
private readonly byte[] receiveBuffer;
|
||||
|
||||
protected SteamPeer()
|
||||
{
|
||||
receiveBuffer = new byte[Message.MaxSize + sizeof(ushort)];
|
||||
}
|
||||
|
||||
protected void Receive(SteamConnection fromConnection)
|
||||
{
|
||||
IntPtr[] ptrs = new IntPtr[MaxMessages]; // TODO: remove allocation?
|
||||
|
||||
// TODO: consider using poll groups -> https://partner.steamgames.com/doc/api/ISteamNetworkingSockets#functions_poll_groups
|
||||
int messageCount = SteamNetworkingSockets.ReceiveMessagesOnConnection(fromConnection.SteamNetConnection, ptrs, MaxMessages);
|
||||
if (messageCount > 0)
|
||||
{
|
||||
for (int i = 0; i < messageCount; i++)
|
||||
{
|
||||
SteamNetworkingMessage_t data = Marshal.PtrToStructure<SteamNetworkingMessage_t>(ptrs[i]);
|
||||
|
||||
if (data.m_cbSize > 0)
|
||||
{
|
||||
int byteCount = data.m_cbSize;
|
||||
if (data.m_cbSize > receiveBuffer.Length)
|
||||
{
|
||||
Debug.LogWarning($"{LogName}: Can't fully handle {data.m_cbSize} bytes because it exceeds the maximum of {receiveBuffer.Length}. Data will be incomplete!");
|
||||
byteCount = receiveBuffer.Length;
|
||||
}
|
||||
|
||||
Marshal.Copy(data.m_pData, receiveBuffer, 0, data.m_cbSize);
|
||||
OnDataReceived(receiveBuffer, byteCount, fromConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Send(byte[] dataBuffer, int numBytes, HSteamNetConnection toConnection)
|
||||
{
|
||||
GCHandle handle = GCHandle.Alloc(dataBuffer, GCHandleType.Pinned);
|
||||
IntPtr pDataBuffer = handle.AddrOfPinnedObject();
|
||||
|
||||
EResult result = SteamNetworkingSockets.SendMessageToConnection(toConnection, pDataBuffer, (uint)numBytes, Constants.k_nSteamNetworkingSend_Unreliable, out long _);
|
||||
if (result != EResult.k_EResultOK)
|
||||
Debug.LogWarning($"{LogName}: Failed to send {numBytes} bytes - {result}");
|
||||
|
||||
handle.Free();
|
||||
}
|
||||
|
||||
protected abstract void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection);
|
||||
}
|
||||
}
|
||||
160
RiptideSteamTransport/Transport/SteamServer.cs
Normal file
160
RiptideSteamTransport/Transport/SteamServer.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Riptide.Transports.Steam
|
||||
{
|
||||
public class SteamServer : SteamPeer, IServer
|
||||
{
|
||||
public event EventHandler<ConnectedEventArgs> Connected;
|
||||
public event EventHandler<DataReceivedEventArgs> DataReceived;
|
||||
public event EventHandler<DisconnectedEventArgs> Disconnected;
|
||||
|
||||
public ushort Port { get; private set; }
|
||||
|
||||
private Dictionary<CSteamID, SteamConnection> connections;
|
||||
private HSteamListenSocket listenSocket;
|
||||
private Callback<SteamNetConnectionStatusChangedCallback_t> connectionStatusChanged;
|
||||
|
||||
public void Start(ushort port)
|
||||
{
|
||||
Port = port;
|
||||
connections = new Dictionary<CSteamID, SteamConnection>();
|
||||
|
||||
connectionStatusChanged = Callback<SteamNetConnectionStatusChangedCallback_t>.Create(OnConnectionStatusChanged);
|
||||
|
||||
try
|
||||
{
|
||||
#if UNITY_SERVER
|
||||
SteamGameServerNetworkingUtils.InitRelayNetworkAccess();
|
||||
#else
|
||||
SteamNetworkingUtils.InitRelayNetworkAccess();
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogException(ex);
|
||||
}
|
||||
|
||||
SteamNetworkingConfigValue_t[] options = new SteamNetworkingConfigValue_t[] { };
|
||||
listenSocket = SteamNetworkingSockets.CreateListenSocketP2P(port, options.Length, options);
|
||||
}
|
||||
|
||||
private void OnConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t callback)
|
||||
{
|
||||
CSteamID clientSteamId = callback.m_info.m_identityRemote.GetSteamID();
|
||||
switch (callback.m_info.m_eState)
|
||||
{
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connecting:
|
||||
Accept(callback.m_hConn);
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_Connected:
|
||||
Add(new SteamConnection(clientSteamId, callback.m_hConn, this));
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ClosedByPeer:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Closed by peer", false);
|
||||
OnDisconnected(clientSteamId, DisconnectReason.Disconnected);
|
||||
break;
|
||||
|
||||
case ESteamNetworkingConnectionState.k_ESteamNetworkingConnectionState_ProblemDetectedLocally:
|
||||
SteamNetworkingSockets.CloseConnection(callback.m_hConn, 0, "Problem detected", false);
|
||||
OnDisconnected(clientSteamId, DisconnectReason.TransportError);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"{LogName}: {clientSteamId}'s connection state changed - {callback.m_info.m_eState}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Add(SteamConnection connection)
|
||||
{
|
||||
if (!connections.ContainsKey(connection.SteamId))
|
||||
{
|
||||
connections.Add(connection.SteamId, connection);
|
||||
OnConnected(connection);
|
||||
}
|
||||
else
|
||||
Debug.Log($"{LogName}: Connection from {connection.SteamId} could not be accepted: Already connected");
|
||||
}
|
||||
|
||||
private void Accept(HSteamNetConnection connection)
|
||||
{
|
||||
EResult result = SteamNetworkingSockets.AcceptConnection(connection);
|
||||
if (result != EResult.k_EResultOK)
|
||||
Debug.LogWarning($"{LogName}: Connection could not be accepted: {result}");
|
||||
}
|
||||
|
||||
public void Close(Connection connection)
|
||||
{
|
||||
if (connection is SteamConnection steamConnection)
|
||||
{
|
||||
SteamNetworkingSockets.CloseConnection(steamConnection.SteamNetConnection, 0, "Disconnected by server", false);
|
||||
connections.Remove(steamConnection.SteamId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Poll()
|
||||
{
|
||||
foreach (SteamConnection connection in connections.Values)
|
||||
Receive(connection);
|
||||
}
|
||||
|
||||
// TODO: disable nagle so this isn't needed
|
||||
//public void Flush()
|
||||
//{
|
||||
// foreach (SteamConnection connection in connections.Values)
|
||||
// SteamNetworkingSockets.FlushMessagesOnConnection(connection.SteamNetConnection);
|
||||
//}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
if (connectionStatusChanged != null)
|
||||
{
|
||||
connectionStatusChanged.Dispose();
|
||||
connectionStatusChanged = null;
|
||||
}
|
||||
|
||||
foreach (SteamConnection connection in connections.Values)
|
||||
SteamNetworkingSockets.CloseConnection(connection.SteamNetConnection, 0, "Server stopped", false);
|
||||
|
||||
connections.Clear();
|
||||
SteamNetworkingSockets.CloseListenSocket(listenSocket);
|
||||
}
|
||||
|
||||
protected internal virtual void OnConnected(Connection connection)
|
||||
{
|
||||
Connected?.Invoke(this, new ConnectedEventArgs(connection));
|
||||
}
|
||||
|
||||
protected override void OnDataReceived(byte[] dataBuffer, int amount, SteamConnection fromConnection)
|
||||
{
|
||||
if ((MessageHeader)dataBuffer[0] == MessageHeader.Connect)
|
||||
{
|
||||
if (fromConnection.DidReceiveConnect)
|
||||
return;
|
||||
|
||||
fromConnection.DidReceiveConnect = true;
|
||||
}
|
||||
|
||||
DataReceived?.Invoke(this, new DataReceivedEventArgs(dataBuffer, amount, fromConnection));
|
||||
}
|
||||
|
||||
protected virtual void OnDisconnected(CSteamID steamId, DisconnectReason reason)
|
||||
{
|
||||
if (connections.TryGetValue(steamId, out SteamConnection connection))
|
||||
{
|
||||
Disconnected?.Invoke(this, new DisconnectedEventArgs(connection, reason));
|
||||
connections.Remove(steamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user