v1.3.0.1 (Epic Store release)

This commit is contained in:
Regalis11
2024-03-28 18:34:33 +02:00
parent 81ca8637be
commit 3791670c42
269 changed files with 13160 additions and 2966 deletions
@@ -0,0 +1,82 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class DualStackP2PSocket : P2PSocket
{
private readonly Option<EosP2PSocket> eosSocket;
private readonly Option<SteamListenSocket> steamSocket;
private DualStackP2PSocket(
Callbacks callbacks,
Option<EosP2PSocket> eosSocket,
Option<SteamListenSocket> steamSocket) :
base(callbacks)
{
this.eosSocket = eosSocket;
this.steamSocket = steamSocket;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
{
var eosP2PSocketResult = EosP2PSocket.Create(callbacks);
var steamP2PSocketResult = SteamListenSocket.Create(callbacks);
if (eosP2PSocketResult.TryUnwrapFailure(out var eosError)
&& steamP2PSocketResult.TryUnwrapFailure(out var steamError))
{
return Result.Failure(new Error(eosError, steamError));
}
return Result.Success((P2PSocket)new DualStackP2PSocket(
callbacks,
eosP2PSocketResult.TryUnwrapSuccess(out var eosP2PSocket)
? Option.Some((EosP2PSocket)eosP2PSocket)
: Option.None,
steamP2PSocketResult.TryUnwrapSuccess(out var steamP2PSocket)
? Option.Some((SteamListenSocket)steamP2PSocket)
: Option.None));
}
public override void ProcessIncomingMessages()
{
if (eosSocket.TryUnwrap(out var eosP2PSocket)) { eosP2PSocket.ProcessIncomingMessages(); }
if (steamSocket.TryUnwrap(out var steamP2PSocket)) { steamP2PSocket.ProcessIncomingMessages(); }
}
public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
{
return endpoint switch
{
EosP2PEndpoint eosP2PEndpoint when eosSocket.TryUnwrap(out var eosP2PSocket)
=> eosP2PSocket.SendMessage(eosP2PEndpoint, outMsg, deliveryMethod),
SteamP2PEndpoint steamP2PEndpoint when steamSocket.TryUnwrap(out var steamP2PSocket)
=> steamP2PSocket.SendMessage(steamP2PEndpoint, outMsg, deliveryMethod),
_
=> false
};
}
public override void CloseConnection(P2PEndpoint endpoint)
{
switch (endpoint)
{
case EosP2PEndpoint eosP2PEndpoint:
if (eosSocket.TryUnwrap(out var eosP2PSocket))
{
eosP2PSocket.CloseConnection(eosP2PEndpoint);
}
break;
case SteamP2PEndpoint steamP2PEndpoint:
if (steamSocket.TryUnwrap(out var steamP2PSocket))
{
steamP2PSocket.CloseConnection(steamP2PEndpoint);
}
break;
}
}
public override void Dispose()
{
if (eosSocket.TryUnwrap(out var eosP2PSocket)) { eosP2PSocket.Dispose(); }
if (steamSocket.TryUnwrap(out var steamP2PSocket)) { steamP2PSocket.Dispose(); }
}
}
@@ -0,0 +1,99 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class EosP2PSocket : P2PSocket
{
private readonly EosInterface.P2PSocket eosSocket;
private EosP2PSocket(
Callbacks callbacks,
EosInterface.P2PSocket eosSocket)
: base(callbacks)
{
this.eosSocket = eosSocket;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
{
if (!EosInterface.Core.IsInitialized) { return Result.Failure(new Error(ErrorCode.EosNotInitialized)); }
var eosSocketId = new EosInterface.SocketId { SocketName = EosP2PEndpoint.SocketName };
if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids)
{
return Result.Failure(new Error(ErrorCode.EosNotLoggedIn));
}
var socketCreateResult = EosInterface.P2PSocket.Create(puids[0], eosSocketId);
if (!socketCreateResult.TryUnwrapSuccess(out var eosSocket)) { return Result.Failure(new Error(ErrorCode.FailedToCreateEosP2PSocket, socketCreateResult.ToString())); }
var retVal = new EosP2PSocket(callbacks, eosSocket);
eosSocket.HandleIncomingConnection.Register("Event".ToIdentifier(), retVal.OnIncomingConnection);
eosSocket.HandleClosedConnection.Register("Event".ToIdentifier(), retVal.OnConnectionClosed);
return Result.Success((P2PSocket)retVal);
}
public override void ProcessIncomingMessages()
{
foreach (var msg in eosSocket.GetMessageBatch())
{
callbacks.OnData(new EosP2PEndpoint(msg.Sender), new ReadWriteMessage(msg.Buffer, 0, msg.ByteLength * 8, false));
}
}
public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
{
if (endpoint is not EosP2PEndpoint { ProductUserId: var puid }) { return false; }
var sendResult = eosSocket.SendMessage(new EosInterface.P2PSocket.OutgoingMessage(
Buffer: outMsg.Buffer,
ByteLength: outMsg.LengthBytes,
Destination: puid,
DeliveryMethod: deliveryMethod));
return sendResult.IsSuccess;
}
private void OnIncomingConnection(EosInterface.P2PSocket.IncomingConnectionRequest request)
{
var remoteEndpoint = new EosP2PEndpoint(request.RemoteUserId);
if (callbacks.OnIncomingConnection(remoteEndpoint))
{
request.Accept();
}
}
private void OnConnectionClosed(EosInterface.P2PSocket.RemoteConnectionClosed data)
{
var remoteEndpoint = new EosP2PEndpoint(data.RemoteUserId);
var peerDisconnectPacket = PeerDisconnectPacket.WithReason(data.Reason switch
{
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.Unknown => DisconnectReason.Unknown,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ClosedByLocalUser => DisconnectReason.Disconnected,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ClosedByPeer => DisconnectReason.Disconnected,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.TimedOut => DisconnectReason.Timeout,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.TooManyConnections => DisconnectReason.ServerFull,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.InvalidMessage => DisconnectReason.Unknown,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.InvalidData => DisconnectReason.Unknown,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ConnectionFailed => DisconnectReason.AuthenticationFailed,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.ConnectionClosed => DisconnectReason.Disconnected,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.NegotiationFailed => DisconnectReason.AuthenticationFailed,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.UnexpectedError => DisconnectReason.Unknown,
EosInterface.P2PSocket.RemoteConnectionClosed.ConnectionClosedReason.Unhandled => DisconnectReason.Unknown,
_ => DisconnectReason.Unknown
});
callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket);
}
public override void CloseConnection(P2PEndpoint endpoint)
{
if (endpoint is not EosP2PEndpoint { ProductUserId: var puid }) { return; }
eosSocket.CloseConnection(puid);
}
public override void Dispose()
{
eosSocket.Dispose();
}
}
@@ -0,0 +1,56 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Networking;
abstract class P2PSocket : IDisposable
{
public enum ErrorCode
{
EosNotInitialized,
EosNotLoggedIn,
FailedToCreateEosP2PSocket,
SteamNotInitialized,
FailedToCreateSteamP2PSocket
}
public readonly record struct Error(
ImmutableArray<(ErrorCode Code, string AdditionalInfo)> CodesAndInfo)
{
public Error(ErrorCode code, string? additionalInfo = "") : this((code, additionalInfo ?? "").ToEnumerable().ToImmutableArray()) { }
public Error(params Error[] innerErrors) : this(innerErrors.SelectMany(ie => ie.CodesAndInfo).ToImmutableArray()) { }
public override string? ToString()
{
if (CodesAndInfo.IsDefault)
{
return "default(Error)";
}
return $"Errors({string.Join("; ", CodesAndInfo)})";
}
}
public readonly record struct Callbacks(
Predicate<P2PEndpoint> OnIncomingConnection,
Action<P2PEndpoint, PeerDisconnectPacket> OnConnectionClosed,
Action<P2PEndpoint, IReadMessage> OnData);
protected readonly Callbacks callbacks;
protected P2PSocket(Callbacks callbacks)
{
this.callbacks = callbacks;
}
public abstract void ProcessIncomingMessages();
public abstract bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod);
public abstract void CloseConnection(P2PEndpoint endpoint);
public abstract void Dispose();
}
@@ -0,0 +1,114 @@
using System;
using System.Runtime.InteropServices;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
sealed class SteamConnectSocket : P2PSocket
{
private sealed class ConnectionManager : Steamworks.ConnectionManager, Steamworks.IConnectionManager
{
private SteamP2PEndpoint endpoint;
private Callbacks callbacks;
public void SetEndpointAndCallbacks(SteamP2PEndpoint endpoint, Callbacks callbacks)
{
this.endpoint = endpoint;
this.callbacks = callbacks;
}
public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel)
{
var dataArray = new byte[size];
Marshal.Copy(source: data, destination: dataArray, startIndex: 0, length: size);
callbacks.OnData(endpoint, new ReadWriteMessage(dataArray, bitPos: 0, lBits: size * 8, copyBuf: false));
}
public override void OnDisconnected(Steamworks.Data.ConnectionInfo info)
{
if (!info.Identity.IsSteamId) { return; }
var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity));
var peerDisconnectPacket = PeerDisconnectPacket.WithReason(info.EndReason switch
{
Steamworks.NetConnectionEnd.App_Generic => DisconnectReason.Disconnected,
Steamworks.NetConnectionEnd.AppException_Generic => DisconnectReason.Unknown,
Steamworks.NetConnectionEnd.Local_OfflineMode => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_ManyRelayConnectivity => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_HostedServerPrimaryRelay => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_NetworkConfig => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_Rights => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_Timeout => DisconnectReason.SteamP2PTimeOut,
Steamworks.NetConnectionEnd.Remote_BadCrypt => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_BadCert => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_Generic => DisconnectReason.Unknown,
Steamworks.NetConnectionEnd.Misc_InternalError => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_Timeout => DisconnectReason.SteamP2PTimeOut,
Steamworks.NetConnectionEnd.Misc_SteamConnectivity => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_NoRelaySessionsToClient => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_P2P_Rendezvous => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.SteamP2PError,
_ => DisconnectReason.Unknown
});
callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket);
base.OnDisconnected(info);
}
}
private readonly SteamP2PEndpoint expectedEndpoint;
private readonly ConnectionManager connectionManager;
private SteamConnectSocket(SteamP2PEndpoint expectedEndpoint, Callbacks callbacks, ConnectionManager connectionManager) : base(callbacks)
{
this.expectedEndpoint = expectedEndpoint;
this.connectionManager = connectionManager;
}
public static Result<P2PSocket, Error> Create(SteamP2PEndpoint endpoint, Callbacks callbacks)
{
if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); }
var connectionManager = Steamworks.SteamNetworkingSockets.ConnectRelay<ConnectionManager>(endpoint.SteamId.Value);
if (connectionManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); }
connectionManager.SetEndpointAndCallbacks(endpoint, callbacks);
return Result.Success((P2PSocket)new SteamConnectSocket(endpoint, callbacks, connectionManager));
}
public override void ProcessIncomingMessages()
{
connectionManager.Receive();
}
public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
{
if (endpoint != expectedEndpoint) { return false; }
var result = connectionManager.Connection.SendMessage(
data: outMsg.Buffer,
offset: 0,
length: outMsg.LengthBytes,
sendType: deliveryMethod switch
{
DeliveryMethod.Reliable => Steamworks.Data.SendType.Reliable,
_ => Steamworks.Data.SendType.Unreliable
});
return result == Steamworks.Result.OK;
}
public override void CloseConnection(P2PEndpoint endpoint)
{
if (endpoint != expectedEndpoint) { return; }
connectionManager.Close();
}
public override void Dispose()
{
connectionManager.Close();
}
}
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
sealed class SteamListenSocket : P2PSocket
{
private sealed class SocketManager : Steamworks.SocketManager, Steamworks.ISocketManager
{
private Callbacks callbacks;
private readonly Dictionary<SteamP2PEndpoint, Steamworks.Data.Connection> endpointToConnection = new();
public void SetCallbacks(Callbacks callbacks)
{
this.callbacks = callbacks;
}
public override void OnConnecting(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info)
{
if (!info.Identity.IsSteamId) { return; }
var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity));
endpointToConnection[remoteEndpoint] = connection;
if (callbacks.OnIncomingConnection(remoteEndpoint))
{
connection.Accept();
}
}
public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info)
{
if (!info.Identity.IsSteamId) { return; }
var remoteEndpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)info.Identity));
endpointToConnection.Remove(remoteEndpoint);
var peerDisconnectPacket = PeerDisconnectPacket.WithReason(info.EndReason switch
{
Steamworks.NetConnectionEnd.App_Generic => DisconnectReason.Disconnected,
Steamworks.NetConnectionEnd.AppException_Generic => DisconnectReason.Unknown,
Steamworks.NetConnectionEnd.Local_OfflineMode => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_ManyRelayConnectivity => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_HostedServerPrimaryRelay => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_NetworkConfig => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_Rights => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_Timeout => DisconnectReason.SteamP2PTimeOut,
Steamworks.NetConnectionEnd.Remote_BadCrypt => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_BadCert => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_Generic => DisconnectReason.Unknown,
Steamworks.NetConnectionEnd.Misc_InternalError => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_Timeout => DisconnectReason.SteamP2PTimeOut,
Steamworks.NetConnectionEnd.Misc_SteamConnectivity => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_NoRelaySessionsToClient => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_P2P_Rendezvous => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.SteamP2PError,
Steamworks.NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.SteamP2PError,
_ => DisconnectReason.Unknown
});
callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket);
base.OnDisconnected(connection, info);
}
public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel)
{
if (!identity.IsSteamId) { return; }
var endpoint = new SteamP2PEndpoint(new SteamId((Steamworks.SteamId)identity));
var dataArray = new byte[size];
Marshal.Copy(source: data, destination: dataArray, startIndex: 0, length: size);
callbacks.OnData(endpoint, new ReadWriteMessage(dataArray, bitPos: 0, lBits: size * 8, copyBuf: false));
}
internal bool SendMessage(SteamP2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
{
if (!endpointToConnection.TryGetValue(endpoint, out var connection))
{
return false;
}
var result = connection.SendMessage(
data: outMsg.Buffer,
offset: 0,
length: outMsg.LengthBytes,
sendType: deliveryMethod switch
{
DeliveryMethod.Reliable => Steamworks.Data.SendType.Reliable,
_ => Steamworks.Data.SendType.Unreliable
});
return result == Steamworks.Result.OK;
}
internal void CloseConnection(SteamP2PEndpoint endpoint)
{
if (!endpointToConnection.TryGetValue(endpoint, out var connection)) { return; }
connection.Close();
}
}
private readonly SocketManager socketManager;
private SteamListenSocket(
Callbacks callbacks,
SocketManager socketManager)
: base(callbacks)
{
this.socketManager = socketManager;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
{
if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); }
var socketManager = Steamworks.SteamNetworkingSockets.CreateRelaySocket<SocketManager>();
if (socketManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); }
socketManager.SetCallbacks(callbacks);
return Result.Success((P2PSocket)new SteamListenSocket(callbacks, socketManager));
}
public override void ProcessIncomingMessages()
{
socketManager.Receive();
}
public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
{
if (endpoint is not SteamP2PEndpoint steamP2PEndpoint) { return false; }
return socketManager.SendMessage(steamP2PEndpoint, outMsg, deliveryMethod);
}
public override void CloseConnection(P2PEndpoint endpoint)
{
if (endpoint is not SteamP2PEndpoint steamP2PEndpoint) { return; }
socketManager.CloseConnection(steamP2PEndpoint);
}
public override void Dispose()
{
socketManager.Close();
}
}
@@ -1,12 +1,18 @@
#nullable enable
using Barotrauma.Steam;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
internal abstract class ClientPeer<TEndpoint> : ClientPeer where TEndpoint : Endpoint
{
public new TEndpoint ServerEndpoint => (base.ServerEndpoint as TEndpoint)!;
protected ClientPeer(TEndpoint serverEndpoint, ImmutableArray<Endpoint> allServerEndpoints, Callbacks callbacks, Option<int> ownerKey)
: base(serverEndpoint, allServerEndpoints, callbacks, ownerKey) { }
}
internal abstract class ClientPeer
{
public ImmutableArray<ServerContentPackage> ServerContentPackages { get; set; } =
@@ -25,21 +31,22 @@ namespace Barotrauma.Networking
protected readonly Callbacks callbacks;
public readonly Endpoint ServerEndpoint;
public readonly ImmutableArray<Endpoint> AllServerEndpoints;
public NetworkConnection? ServerConnection { get; protected set; }
protected readonly bool isOwner;
protected bool IsOwner => ownerKey.IsSome();
protected readonly Option<int> ownerKey;
public bool IsActive => isActive;
protected bool isActive;
public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey)
protected ClientPeer(Endpoint serverEndpoint, ImmutableArray<Endpoint> allServerEndpoints, Callbacks callbacks, Option<int> ownerKey)
{
ServerEndpoint = serverEndpoint;
AllServerEndpoints = allServerEndpoints;
this.callbacks = callbacks;
this.ownerKey = ownerKey;
isOwner = ownerKey.IsSome();
}
public abstract void Start();
@@ -53,7 +60,7 @@ namespace Barotrauma.Networking
protected ConnectionInitialization initializationStep;
public bool ContentPackageOrderReceived { get; set; }
protected int passwordSalt;
protected Option<Steamworks.AuthTicket> steamAuthTicket;
protected Option<AuthenticationTicket> authTicket;
private GUIMessageBox? passwordMsgBox;
public bool WaitingForPassword
@@ -67,43 +74,64 @@ namespace Barotrauma.Networking
public IReadMessage Message;
}
protected abstract Task<Option<AccountId>> GetAccountId();
protected void OnInitializationComplete()
{
passwordMsgBox?.Close();
if (initializationStep == ConnectionInitialization.Success) { return; }
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
}
protected void ReadConnectionInitializationStep(IncomingInitializationMessage inc)
{
if (inc.InitializationStep != ConnectionInitialization.Password)
{
passwordMsgBox?.Close();
}
switch (inc.InitializationStep)
{
case ConnectionInitialization.SteamTicketAndVersion:
case ConnectionInitialization.AuthInfoAndVersion:
{
if (initializationStep != ConnectionInitialization.SteamTicketAndVersion) { return; }
if (initializationStep != ConnectionInitialization.AuthInfoAndVersion) { return; }
PeerPacketHeaders headers = new PeerPacketHeaders
TaskPool.Add($"{GetType().Name}.{nameof(GetAccountId)}", GetAccountId(), t =>
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.SteamTicketAndVersion
};
if (GameMain.Client?.ClientPeer is null) { return; }
if (!t.TryGetResult(out Option<AccountId> accountId))
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
}
var headers = new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.AuthInfoAndVersion
};
if (steamAuthTicket.TryUnwrap(out var authTicket) && authTicket is { Canceled: true })
{
throw new InvalidOperationException("ReadConnectionInitializationStep failed: Steam auth ticket has been cancelled.");
}
var body = new ClientAuthTicketAndVersionPacket
{
Name = GameMain.Client.Name,
OwnerKey = ownerKey,
AccountId = accountId,
AuthTicket = authTicket,
GameVersion = GameMain.Version.ToString(),
Language = GameSettings.CurrentConfig.Language.Value
};
ClientSteamTicketAndVersionPacket body = new ClientSteamTicketAndVersionPacket
{
Name = GameMain.Client.Name,
OwnerKey = ownerKey,
SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id),
SteamAuthTicket = steamAuthTicket.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None),
GameVersion = GameMain.Version.ToString(),
Language = GameSettings.CurrentConfig.Language.Value
};
SendMsgInternal(headers, body);
SendMsgInternal(headers, body);
});
break;
}
case ConnectionInitialization.ContentPackageOrder:
{
if (initializationStep
is ConnectionInitialization.SteamTicketAndVersion
is ConnectionInitialization.AuthInfoAndVersion
or ConnectionInitialization.Password)
{
initializationStep = ConnectionInitialization.ContentPackageOrder;
@@ -136,7 +164,7 @@ namespace Barotrauma.Networking
break;
}
case ConnectionInitialization.Password:
if (initializationStep == ConnectionInitialization.SteamTicketAndVersion)
if (initializationStep == ConnectionInitialization.AuthInfoAndVersion)
{
initializationStep = ConnectionInitialization.Password;
}
@@ -152,6 +180,7 @@ namespace Barotrauma.Networking
LocalizedString pwMsg = TextManager.Get("PasswordRequired");
passwordMsgBox?.Close();
passwordMsgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") },
relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170)));
var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter);
@@ -1,26 +1,25 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Lidgren.Network;
using Barotrauma.Steam;
using System.Net.Sockets;
namespace Barotrauma.Networking
{
internal sealed class LidgrenClientPeer : ClientPeer
internal sealed class LidgrenClientPeer : ClientPeer<LidgrenEndpoint>
{
private NetClient? netClient;
private readonly NetPeerConfiguration netPeerConfiguration;
private readonly List<NetIncomingMessage> incomingLidgrenMessages;
private LidgrenEndpoint lidgrenEndpoint =>
ServerConnection is LidgrenConnection { Endpoint: LidgrenEndpoint result }
? result
: throw new InvalidOperationException();
public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option<int> ownerKey) : base(endpoint, callbacks, ownerKey)
public LidgrenClientPeer(LidgrenEndpoint endpoint, Callbacks callbacks, Option<int> ownerKey) : base(endpoint, ((Endpoint)endpoint).ToEnumerable().ToImmutableArray(), callbacks, ownerKey)
{
ServerConnection = null;
@@ -56,18 +55,9 @@ namespace Barotrauma.Networking
netClient = new NetClient(netPeerConfiguration);
if (SteamManager.IsInitialized)
{
steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint);
if (steamAuthTicket.IsNone())
{
throw new Exception("GetAuthSessionTicket returned null");
}
}
initializationStep = ConnectionInitialization.AuthInfoAndVersion;
initializationStep = ConnectionInitialization.SteamTicketAndVersion;
if (!(ServerEndpoint is LidgrenEndpoint lidgrenEndpointValue))
if (ServerEndpoint is not { } lidgrenEndpointValue)
{
throw new InvalidCastException($"Endpoint is not {nameof(LidgrenEndpoint)}");
}
@@ -79,12 +69,25 @@ namespace Barotrauma.Networking
netClient.Start();
var netConnection = netClient.Connect(lidgrenEndpointValue.NetEndpoint);
TaskPool.Add(
$"{nameof(LidgrenClientPeer)}.GetAuthTicket",
AuthenticationTicket.Create(ServerEndpoint),
t =>
{
if (!t.TryGetResult(out Option<AuthenticationTicket> authenticationTicket))
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
return;
}
authTicket = authenticationTicket;
ServerConnection = new LidgrenConnection(netConnection)
{
Status = NetworkConnectionStatus.Connected
};
var netConnection = netClient.Connect(lidgrenEndpointValue.NetEndpoint);
ServerConnection = new LidgrenConnection(netConnection)
{
Status = NetworkConnectionStatus.Connected
};
});
isActive = true;
}
@@ -96,7 +99,7 @@ namespace Barotrauma.Networking
ToolBox.ThrowIfNull(netClient);
ToolBox.ThrowIfNull(incomingLidgrenMessages);
if (isOwner && !ChildServerRelay.IsProcessAlive)
if (IsOwner && !ChildServerRelay.IsProcessAlive)
{
var gameClient = GameMain.Client;
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
@@ -112,9 +115,11 @@ namespace Barotrauma.Networking
foreach (NetIncomingMessage inc in incomingLidgrenMessages)
{
if (!inc.SenderConnection.RemoteEndPoint.EquivalentTo(lidgrenEndpoint.NetEndpoint))
var remoteEndpoint = new LidgrenEndpoint(inc.SenderEndPoint);
if (remoteEndpoint != ServerEndpoint)
{
DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}");
DebugConsole.AddWarning($"Mismatched endpoint: expected {ServerEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}");
continue;
}
@@ -152,11 +157,7 @@ namespace Barotrauma.Networking
}
else
{
if (initializationStep != ConnectionInitialization.Success)
{
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
}
OnInitializationComplete();
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
callbacks.OnMessageReceived.Invoke(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection));
@@ -212,9 +213,6 @@ namespace Barotrauma.Networking
netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation());
netClient = null;
if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
steamAuthTicket = Option.None;
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
@@ -270,7 +268,21 @@ namespace Barotrauma.Networking
return netClient.SendMessage(msg.ToLidgren(netClient), deliveryMethod.ToLidgren());
}
protected override async Task<Option<AccountId>> GetAccountId()
{
if (!EosInterface.Core.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); }
var selfPuids = EosInterface.IdQueries.GetLoggedInPuids();
if (selfPuids.None()) { return Option.None; }
var accountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(selfPuids.First());
return accountIdsResult.TryUnwrapSuccess(out var accountIds) && accountIds.Length > 0
? Option.Some(accountIds[0])
: Option.None;
}
#if DEBUG
public override void ForceTimeOut()
{
netClient?.ServerConnection?.ForceTimeOut();
@@ -1,136 +1,142 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Barotrauma.Steam;
using System.Threading;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.Steam;
namespace Barotrauma.Networking
{
internal sealed class SteamP2PClientPeer : ClientPeer
internal sealed class P2PClientPeer : ClientPeer<P2PEndpoint>
{
private readonly SteamId hostSteamId;
private double timeout;
private double heartbeatTimer;
private double connectionStatusTimer;
private long sentBytes, receivedBytes;
private readonly List<IncomingInitializationMessage> incomingInitializationMessages = new List<IncomingInitializationMessage>();
private readonly List<IReadMessage> incomingDataMessages = new List<IReadMessage>();
private readonly MessageFragmenter fragmenter = new();
private readonly MessageDefragmenter defragmenter = new();
public SteamP2PClientPeer(SteamP2PEndpoint endpoint, Callbacks callbacks) : base(endpoint, callbacks, Option<int>.None())
private P2PSocket? socket;
private static P2PEndpoint GetPrimaryEndpoint(ImmutableArray<P2PEndpoint> allEndpoints)
{
var steamEndpointOption = allEndpoints.OfType<SteamP2PEndpoint>().FirstOrNone();
var eosEndpointOption = allEndpoints.OfType<EosP2PEndpoint>().FirstOrNone();
if (SteamManager.IsInitialized)
{
if (steamEndpointOption.TryUnwrap(out var steamEndpoint)) { return steamEndpoint; }
}
if (EosInterface.Core.IsInitialized)
{
if (eosEndpointOption.TryUnwrap(out var eosEndpoint)) { return eosEndpoint; }
}
throw new Exception($"Couldn't pick out a primary endpoint: {string.Join(", ", allEndpoints.Select(e => e.GetType().Name))}");
}
public P2PClientPeer(ImmutableArray<P2PEndpoint> allEndpoints, Callbacks callbacks)
: base(
GetPrimaryEndpoint(allEndpoints),
allEndpoints.Cast<Endpoint>().ToImmutableArray(),
callbacks,
Option.None)
{
ServerConnection = null;
isActive = false;
if (!(ServerEndpoint is SteamP2PEndpoint steamIdEndpoint))
{
throw new InvalidCastException("endPoint is not SteamId");
}
hostSteamId = steamIdEndpoint.SteamId;
}
public override void Start()
{
if (isActive) { return; }
ContentPackageOrderReceived = false;
steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint);
//TODO: wait for GetAuthSessionTicketResponse_t
ServerConnection = ServerEndpoint.MakeConnectionFromEndpoint();
if (steamAuthTicket == null)
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData);
var socketCreateResult = ServerEndpoint switch
{
throw new Exception("GetAuthSessionTicket returned null");
}
Steamworks.SteamNetworking.ResetActions();
Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection;
Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed;
Steamworks.SteamNetworking.AllowP2PPacketRelay(true);
ServerConnection = new SteamP2PConnection(hostSteamId);
ServerConnection.SetAccountInfo(new AccountInfo(hostSteamId));
var headers = new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.ConnectionStarted
EosP2PEndpoint => EosP2PSocket.Create(socketCallbacks),
SteamP2PEndpoint steamP2PEndpoint => SteamConnectSocket.Create(steamP2PEndpoint, socketCallbacks),
_ => throw new Exception($"Invalid server endpoint: {ServerEndpoint.GetType()} {ServerEndpoint}")
};
SendMsgInternal(headers, null);
socket = socketCreateResult.TryUnwrapSuccess(out var s)
? s
: throw new Exception($"Failed to create socket for {ServerEndpoint}: {socketCreateResult}");
TaskPool.Add(
$"{nameof(P2PClientPeer)}.GetAuthTicket",
AuthenticationTicket.Create(ServerEndpoint),
t =>
{
if (!t.TryGetResult(out Option<AuthenticationTicket> authenticationTicket))
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
return;
}
authTicket = authenticationTicket;
initializationStep = ConnectionInitialization.SteamTicketAndVersion;
var headers = new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.ConnectionStarted
};
SendMsgInternal(headers, null);
});
initializationStep = ConnectionInitialization.AuthInfoAndVersion;
timeout = NetworkConnection.TimeoutThreshold;
heartbeatTimer = 1.0;
connectionStatusTimer = 0.0;
isActive = true;
}
private void OnIncomingConnection(Steamworks.SteamId steamId)
private bool OnIncomingConnection(P2PEndpoint remoteEndpoint)
{
if (!isActive) { return; }
if (remoteEndpoint == ServerEndpoint)
{
return true;
}
if (steamId == hostSteamId.Value)
if (initializationStep != ConnectionInitialization.Password &&
initializationStep != ConnectionInitialization.ContentPackageOrder &&
initializationStep != ConnectionInitialization.Success)
{
Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId);
}
else if (initializationStep != ConnectionInitialization.Password &&
initializationStep != ConnectionInitialization.ContentPackageOrder &&
initializationStep != ConnectionInitialization.Success)
{
DebugConsole.ThrowError("Connection from incorrect SteamID was rejected: " +
$"expected {hostSteamId}," +
$"got {new SteamId(steamId)}");
DebugConsole.AddWarning(
"Connection from incorrect endpoint was rejected: " +
$"expected {ServerEndpoint}, " +
$"got {remoteEndpoint}");
}
return false;
}
private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error)
private void OnConnectionClosed(P2PEndpoint remoteEndpoint, PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
if (remoteEndpoint != ServerEndpoint) { return; }
if (steamId != hostSteamId.Value) { return; }
Close(PeerDisconnectPacket.SteamP2PError(error));
Close(peerDisconnectPacket);
}
private void OnP2PData(ulong steamId, byte[] data, int dataLength)
private void OnP2PData(P2PEndpoint senderEndpoint, IReadMessage inc)
{
if (!isActive) { return; }
if (steamId != hostSteamId.Value) { return; }
receivedBytes += inc.LengthBytes;
if (senderEndpoint != ServerEndpoint) { return; }
timeout = Screen.Selected == GameMain.GameScreen
? NetworkConnection.TimeoutThresholdInGame
: NetworkConnection.TimeoutThreshold;
try
{
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
ProcessP2PData(inc);
}
catch (Exception e)
{
string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
private void ProcessP2PData(IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (!packetHeader.IsServerMessage()) { return; }
@@ -138,9 +144,8 @@ namespace Barotrauma.Networking
{
if (!initialization.HasValue) { return; }
var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
var relayPacket = INetSerializableStruct.Read<P2PInitializationRelayPacket>(inc);
SteamManager.JoinLobby(relayPacket.LobbyID, false);
if (initializationStep != ConnectionInitialization.Success)
{
incomingInitializationMessages.Add(new IncomingInitializationMessage
@@ -150,6 +155,14 @@ namespace Barotrauma.Networking
});
}
}
else if (packetHeader.IsDataFragment())
{
var completeMessageOption = defragmenter.ProcessIncomingFragment(INetSerializableStruct.Read<MessageFragment>(inc));
if (!completeMessageOption.TryUnwrap(out var completeMessage)) { return; }
int completeMessageLengthBits = completeMessage.Length * 8;
incomingDataMessages.Add(new ReadWriteMessage(completeMessage.ToArray(), 0, completeMessageLengthBits, copyBuf: false));
}
else if (packetHeader.IsHeartbeatMessage())
{
return; //TODO: implement heartbeats
@@ -177,41 +190,7 @@ namespace Barotrauma.Networking
heartbeatTimer -= deltaTime;
if (initializationStep != ConnectionInitialization.Password &&
initializationStep != ConnectionInitialization.ContentPackageOrder &&
initializationStep != ConnectionInitialization.Success)
{
connectionStatusTimer -= deltaTime;
if (connectionStatusTimer <= 0.0)
{
if (Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value) is { } state)
{
if (state.P2PSessionError != Steamworks.P2PSessionError.None)
{
Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
}
}
else
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
}
connectionStatusTimer = 1.0f;
}
}
for (int i = 0; i < 100; i++)
{
if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; }
var packet = Steamworks.SteamNetworking.ReadP2PPacket();
if (packet is { SteamId: var steamId, Data: var data })
{
OnP2PData(steamId, data, data.Length);
if (!isActive) { return; }
receivedBytes += data.Length;
}
}
socket?.ProcessIncomingMessages();
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);
@@ -258,8 +237,7 @@ namespace Barotrauma.Networking
analyticsTag: "NoContentPackages");
return;
}
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
OnInitializationComplete();
}
else
{
@@ -287,6 +265,21 @@ namespace Barotrauma.Networking
if (!isActive) { return; }
byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _);
if (bufAux.Length > MessageFragment.MaxSize)
{
var fragments = fragmenter.FragmentMessage(msg.Buffer.AsSpan()[..msg.LengthBytes]);
foreach (var fragment in fragments)
{
var fragmentHeaders = new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsDataFragment,
Initialization = null
};
SendMsgInternal(fragmentHeaders, fragment);
}
return;
}
var headers = new PeerPacketHeaders
{
@@ -346,8 +339,6 @@ namespace Barotrauma.Networking
{
if (!isActive) { return; }
SteamManager.LeaveLobby();
isActive = false;
var headers = new PeerPacketHeaders
@@ -360,11 +351,9 @@ namespace Barotrauma.Networking
Thread.Sleep(100);
Steamworks.SteamNetworking.ResetActions();
Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value);
if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
steamAuthTicket = Option.None;
socket?.CloseConnection(ServerEndpoint);
socket?.Dispose();
socket = null;
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
@@ -374,33 +363,62 @@ namespace Barotrauma.Networking
IWriteMessage msgToSend = new WriteOnlyMessage();
msgToSend.WriteNetSerializableStruct(headers);
body?.Write(msgToSend);
ForwardToSteamP2P(msgToSend, headers.DeliveryMethod);
ForwardToRemotePeer(msgToSend, headers.DeliveryMethod);
}
private void ForwardToSteamP2P(IWriteMessage msg, DeliveryMethod deliveryMethod)
private void ForwardToRemotePeer(IWriteMessage msg, DeliveryMethod deliveryMethod)
{
if (!isActive) { return; }
if (socket is null) { return; }
heartbeatTimer = 5.0;
int length = msg.LengthBytes;
bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, deliveryMethod.ToSteam());
if (length + 4 >= MsgConstants.MTU)
{
DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)");
deliveryMethod = DeliveryMethod.Reliable;
}
bool success = socket.SendMessage(ServerEndpoint, msg, deliveryMethod);
sentBytes += length;
if (successSend) { return; }
if (success) { return; }
if (deliveryMethod is DeliveryMethod.Unreliable)
{
DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)");
successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, DeliveryMethod.Reliable.ToSteam());
success = socket.SendMessage(ServerEndpoint, msg, DeliveryMethod.Reliable);
sentBytes += length;
}
if (!successSend)
if (!success)
{
DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)");
}
}
protected override async Task<Option<AccountId>> GetAccountId()
{
if (SteamManager.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); }
if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids)
{
return Option.None;
}
var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]);
if (!externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds)
|| externalAccountIds is not { Length: > 0 })
{
return Option.None;
}
return Option.Some(externalAccountIds[0]);
}
#if DEBUG
public override void ForceTimeOut()
{
timeout = 0.0f;
@@ -0,0 +1,557 @@
#nullable enable
using Barotrauma.Extensions;
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Barotrauma.Networking
{
sealed class P2POwnerPeer : ClientPeer<PipeEndpoint>
{
private P2PSocket? socket;
private readonly ImmutableDictionary<AuthenticationTicketKind, Authenticator> authenticators;
private readonly P2PEndpoint selfPrimaryEndpoint;
private AccountInfo selfAccountInfo;
private long sentBytes, receivedBytes;
private sealed class RemotePeer
{
public enum AuthenticationStatus
{
NotAuthenticated,
AuthenticationPending,
SuccessfullyAuthenticated
}
public readonly P2PEndpoint Endpoint;
public AccountInfo AccountInfo;
public readonly record struct DisconnectInfo(
double TimeToGiveUp,
PeerDisconnectPacket Packet);
public Option<DisconnectInfo> PendingDisconnect;
public AuthenticationStatus AuthStatus;
public readonly record struct UnauthedMessage(byte[] Bytes, int LengthBytes);
public readonly List<UnauthedMessage> UnauthedMessages;
public RemotePeer(P2PEndpoint endpoint)
{
Endpoint = endpoint;
AccountInfo = AccountInfo.None;
PendingDisconnect = Option.None;
AuthStatus = AuthenticationStatus.NotAuthenticated;
UnauthedMessages = new List<UnauthedMessage>();
}
}
private readonly List<RemotePeer> remotePeers = new();
public P2POwnerPeer(Callbacks callbacks, int ownerKey, ImmutableArray<P2PEndpoint> allEndpoints) :
base(new PipeEndpoint(), allEndpoints.Cast<Endpoint>().ToImmutableArray(), callbacks, Option<int>.Some(ownerKey))
{
ServerConnection = null;
isActive = false;
var selfSteamEndpoint = allEndpoints.FirstOrNone(e => e is SteamP2PEndpoint);
var selfEosEndpoint = allEndpoints.FirstOrNone(e => e is EosP2PEndpoint);
var selfPrimaryEndpointOption = selfSteamEndpoint.Fallback(selfEosEndpoint);
if (!selfPrimaryEndpointOption.TryUnwrap(out var selfPrimaryEndpointNotNull))
{
throw new Exception("Could not determine endpoint for P2POwnerPeer");
}
selfPrimaryEndpoint = selfPrimaryEndpointNotNull;
selfAccountInfo = AccountInfo.None;
authenticators = Authenticator.GetAuthenticatorsForHost(Option.Some<Endpoint>(selfPrimaryEndpoint));
}
public override void Start()
{
if (isActive) { return; }
initializationStep = ConnectionInitialization.AuthInfoAndVersion;
ServerConnection = new PipeConnection(Option.None)
{
Status = NetworkConnectionStatus.Connected
};
remotePeers.Clear();
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData);
var socketCreateResult = DualStackP2PSocket.Create(socketCallbacks);
socket = socketCreateResult.TryUnwrapSuccess(out var s)
? s
: throw new Exception($"Failed to create dual-stack socket: {socketCreateResult}");
TaskPool.Add("P2POwnerPeer.GetAccountId", GetAccountId(), t =>
{
if (t.TryGetResult(out Option<AccountId> accountIdOption) && accountIdOption.TryUnwrap(out var accountId))
{
selfAccountInfo = new AccountInfo(accountId);
}
if (selfAccountInfo.IsNone)
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
}
});
isActive = true;
}
private bool OnIncomingConnection(P2PEndpoint remoteEndpoint)
{
if (!isActive) { return false; }
if (remotePeers.None(p => p.Endpoint == remoteEndpoint))
{
remotePeers.Add(new RemotePeer(remoteEndpoint));
}
return true;
}
private void OnConnectionClosed(P2PEndpoint remoteEndpoint, PeerDisconnectPacket disconnectPacket)
{
var remotePeer
= remotePeers.Find(p => p.Endpoint == remoteEndpoint);
if (remotePeer is null) { return; }
CommunicatePeerDisconnectToServerProcess(
remotePeer,
remotePeer.PendingDisconnect.Select(d => d.Packet).Fallback(disconnectPacket));
}
private void OnP2PData(P2PEndpoint senderEndpoint, IReadMessage inc)
{
if (!isActive) { return; }
receivedBytes += inc.LengthBytes;
var remotePeer = remotePeers.Find(p => p.Endpoint == senderEndpoint);
if (remotePeer is null) { return; }
if (remotePeer.PendingDisconnect.IsSome()) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
PacketHeader packetHeader = peerPacketHeaders.PacketHeader;
if (packetHeader.IsConnectionInitializationStep())
{
ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing");
if (initialization == ConnectionInitialization.AuthInfoAndVersion
&& remotePeer.AuthStatus == RemotePeer.AuthenticationStatus.NotAuthenticated)
{
StartAuthTask(inc, remotePeer);
}
}
if (remotePeer.AuthStatus == RemotePeer.AuthenticationStatus.AuthenticationPending)
{
remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(inc.Buffer, inc.LengthBytes));
}
else
{
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = remotePeer.Endpoint.StringRepresentation,
AccountInfo = remotePeer.AccountInfo
});
outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes);
ForwardToServerProcess(outMsg);
}
}
private void StartAuthTask(IReadMessage inc, RemotePeer remotePeer)
{
remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.AuthenticationPending;
var packet = INetSerializableStruct.Read<ClientAuthTicketAndVersionPacket>(inc);
void failAuth()
{
CommunicateDisconnectToRemotePeer(remotePeer, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationFailed));
}
if (!packet.AuthTicket.TryUnwrap(out var authenticationTicket))
{
failAuth();
return;
}
if (!authenticators.TryGetValue(authenticationTicket.Kind, out var authenticator))
{
failAuth();
return;
}
TaskPool.Add($"P2POwnerPeer.VerifyRemotePeerAccountId",
authenticator.VerifyTicket(authenticationTicket),
t =>
{
if (!t.TryGetResult(out AccountInfo accountInfo)
|| accountInfo.IsNone)
{
failAuth();
return;
}
remotePeer.AccountInfo = accountInfo;
remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.SuccessfullyAuthenticated;
foreach (var unauthedMessage in remotePeer.UnauthedMessages)
{
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = remotePeer.Endpoint.StringRepresentation,
AccountInfo = accountInfo
});
msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.LengthBytes);
ForwardToServerProcess(msg);
}
remotePeer.UnauthedMessages.Clear();
});
}
public override void Update(float deltaTime)
{
if (!isActive) { return; }
if (ChildServerRelay.HasShutDown || ChildServerRelay.Process is not { HasExited: false })
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
GameMain.MainMenuScreen.Select();
return false;
};
return;
}
if (selfAccountInfo.IsNone) { return; }
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
if (remotePeers[i].PendingDisconnect.TryUnwrap(out var pendingDisconnect) && pendingDisconnect.TimeToGiveUp < Timing.TotalTime)
{
CommunicatePeerDisconnectToServerProcess(remotePeers[i], pendingDisconnect.Packet);
}
}
socket?.ProcessIncomingMessages();
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);
foreach (var incBuf in ChildServerRelay.Read())
{
ChildServerRelay.DisposeLocalHandles();
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection);
HandleServerMessage(inc);
}
}
private void HandleServerMessage(IReadMessage inc)
{
if (!isActive) { return; }
var recipientInfo = INetSerializableStruct.Read<P2PServerToOwnerHeader>(inc);
if (!recipientInfo.Endpoint.TryUnwrap(out var recipientEndpoint)) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (recipientEndpoint != selfPrimaryEndpoint)
{
HandleMessageForRemotePeer(peerPacketHeaders, recipientEndpoint, inc);
}
else
{
HandleMessageForOwner(peerPacketHeaders, inc);
}
}
private static byte[] GetRemainingBytes(IReadMessage msg)
{
return msg.Buffer[msg.BytePosition..msg.LengthBytes];
}
private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, P2PEndpoint recipientEndpoint, IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders;
if (!packetHeader.IsServerMessage())
{
DebugConsole.ThrowError("Received non-server message meant for remote peer");
return;
}
RemotePeer? peer = remotePeers.Find(p => p.Endpoint == recipientEndpoint);
if (peer is null) { return; }
if (packetHeader.IsDisconnectMessage())
{
var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
CommunicateDisconnectToRemotePeer(peer, packet);
return;
}
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = deliveryMethod,
PacketHeader = packetHeader,
Initialization = initialization
});
if (packetHeader.IsConnectionInitializationStep())
{
var initRelayPacket = new P2PInitializationRelayPacket
{
LobbyID = 0,
Message = new PeerPacketMessage
{
Buffer = GetRemainingBytes(inc)
}
};
outMsg.WriteNetSerializableStruct(initRelayPacket);
}
else
{
byte[] userMessage = GetRemainingBytes(inc);
outMsg.WriteBytes(userMessage, 0, userMessage.Length);
}
ForwardToRemotePeer(deliveryMethod, recipientEndpoint, outMsg);
}
private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc)
{
var (_, packetHeader, _) = peerPacketHeaders;
if (packetHeader.IsDisconnectMessage())
{
DebugConsole.ThrowError("Received disconnect message from owned server");
return;
}
if (!packetHeader.IsServerMessage())
{
DebugConsole.ThrowError("Received non-server message from owned server");
return;
}
if (packetHeader.IsHeartbeatMessage())
{
return; //no timeout since we're using pipes, ignore this message
}
if (packetHeader.IsConnectionInitializationStep())
{
if (selfAccountInfo.IsNone) { throw new InvalidOperationException($"Cannot initialize {nameof(P2POwnerPeer)} because {nameof(selfAccountInfo)} is not defined"); }
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = selfPrimaryEndpoint.StringRepresentation,
AccountInfo = selfAccountInfo
});
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.AuthInfoAndVersion
});
outMsg.WriteNetSerializableStruct(new P2PInitializationOwnerPacket(
Name: GameMain.Client.Name,
AccountId: selfAccountInfo.AccountId.Fallback(default(AccountId)!)));
ForwardToServerProcess(outMsg);
}
else
{
OnInitializationComplete();
PeerPacketMessage packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection);
callbacks.OnMessageReceived.Invoke(msg);
}
}
private void CommunicateDisconnectToRemotePeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
{
if (peer.PendingDisconnect.IsNone())
{
peer.PendingDisconnect = Option.Some(
new RemotePeer.DisconnectInfo(
Timing.TotalTime + 3f,
peerDisconnectPacket));
}
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(peerDisconnectPacket);
ForwardToRemotePeer(DeliveryMethod.Reliable, peer.Endpoint, outMsg);
}
private void CommunicatePeerDisconnectToServerProcess(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
{
if (!remotePeers.Remove(peer)) { return; }
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = peer.Endpoint.StringRepresentation,
AccountInfo = peer.AccountInfo
});
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(peerDisconnectPacket);
if (peer.AccountInfo.AccountId.TryUnwrap(out var accountId))
{
authenticators.Values.ForEach(authenticator => authenticator.EndAuthSession(accountId));
}
ForwardToServerProcess(outMsg);
socket?.CloseConnection(peer.Endpoint);
}
public override void SendPassword(string password)
{
//owner doesn't send passwords
}
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
isActive = false;
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
CommunicateDisconnectToRemotePeer(remotePeers[i], peerDisconnectPacket);
}
Thread.Sleep(100);
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
CommunicatePeerDisconnectToServerProcess(remotePeers[i], peerDisconnectPacket);
}
socket?.Dispose();
socket = null;
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
{
if (!isActive) { return; }
IWriteMessage msgToSend = new WriteOnlyMessage();
byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _);
msgToSend.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = selfPrimaryEndpoint.StringRepresentation,
AccountInfo = selfAccountInfo
});
msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = deliveryMethod,
PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None
});
msgToSend.WriteNetSerializableStruct(new PeerPacketMessage
{
Buffer = msgData
});
ForwardToServerProcess(msgToSend);
}
protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
{
//not currently used by P2POwnerPeer
throw new NotImplementedException();
}
private static void ForwardToServerProcess(IWriteMessage msg)
{
byte[] bufToSend = new byte[msg.LengthBytes];
msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan());
ChildServerRelay.Write(bufToSend);
}
private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, P2PEndpoint recipient, IWriteMessage outMsg)
{
if (socket is null) { return; }
int length = outMsg.LengthBytes;
if (length + 4 >= MsgConstants.MTU)
{
DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)");
deliveryMethod = DeliveryMethod.Reliable;
}
var success = socket.SendMessage(recipient, outMsg, deliveryMethod);
sentBytes += length;
if (success) { return; }
if (deliveryMethod is DeliveryMethod.Unreliable)
{
DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)");
success = socket.SendMessage(recipient, outMsg, DeliveryMethod.Reliable);
sentBytes += length;
}
if (!success)
{
DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)");
}
}
protected override async Task<Option<AccountId>> GetAccountId()
{
if (SteamManager.IsInitialized) { return SteamManager.GetSteamId().Select(id => (AccountId)id); }
if (EosInterface.IdQueries.GetLoggedInPuids() is not { Length: > 0 } puids)
{
return Option.None;
}
var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]);
if (!externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds)
|| externalAccountIds is not { Length: > 0 })
{
return Option.None;
}
return Option.Some(externalAccountIds[0]);
}
#if DEBUG
public override void ForceTimeOut()
{
//TODO: reimplement?
}
#endif
}
}
@@ -1,492 +0,0 @@
#nullable enable
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Threading;
using Barotrauma.Extensions;
namespace Barotrauma.Networking
{
sealed class SteamP2POwnerPeer : ClientPeer
{
private readonly SteamId selfSteamID;
private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0));
private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64);
private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64);
private long sentBytes, receivedBytes;
private sealed class RemotePeer
{
public readonly SteamId SteamId;
public Option<SteamId> OwnerSteamId;
public double? DisconnectTime;
public bool Authenticating;
public bool Authenticated;
public readonly struct UnauthedMessage
{
public readonly SteamId Sender;
public readonly byte[] Bytes;
public readonly int Length;
public UnauthedMessage(SteamId sender, byte[] bytes)
{
Sender = sender;
Bytes = bytes;
Length = bytes.Length;
}
}
public readonly List<UnauthedMessage> UnauthedMessages;
public RemotePeer(SteamId steamId)
{
SteamId = steamId;
OwnerSteamId = Option<SteamId>.None();
DisconnectTime = null;
Authenticating = false;
Authenticated = false;
UnauthedMessages = new List<UnauthedMessage>();
}
}
private List<RemotePeer> remotePeers = null!;
public SteamP2POwnerPeer(Callbacks callbacks, int ownerKey) : base(new PipeEndpoint(), callbacks, Option<int>.Some(ownerKey))
{
ServerConnection = null;
isActive = false;
selfSteamID = SteamManager.GetSteamId().TryUnwrap(out var steamId)
? steamId
: throw new InvalidOperationException("Steamworks not initialized");
}
public override void Start()
{
if (isActive) { return; }
initializationStep = ConnectionInitialization.SteamTicketAndVersion;
ServerConnection = new PipeConnection(selfSteamID)
{
Status = NetworkConnectionStatus.Connected
};
remotePeers = new List<RemotePeer>();
Steamworks.SteamNetworking.ResetActions();
Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection;
Steamworks.SteamUser.OnValidateAuthTicketResponse += OnAuthChange;
Steamworks.SteamNetworking.AllowP2PPacketRelay(true);
isActive = true;
}
private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status)
{
RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
if (remotePeer == null) { return; }
if (status == Steamworks.AuthResponse.OK)
{
if (remotePeer.Authenticated) { return; }
SteamId ownerSteamId = new SteamId(ownerId);
remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId);
remotePeer.Authenticated = true;
remotePeer.Authenticating = false;
foreach (var unauthedMessage in remotePeer.UnauthedMessages)
{
IWriteMessage msg = new WriteOnlyMessage();
WriteSteamId(msg, unauthedMessage.Sender);
WriteSteamId(msg, ownerSteamId);
msg.WriteBytes(unauthedMessage.Bytes, 0, unauthedMessage.Length);
ForwardToServerProcess(msg);
}
remotePeer.UnauthedMessages.Clear();
}
else
{
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status));
}
}
private void OnIncomingConnection(Steamworks.SteamId steamId)
{
if (!isActive) { return; }
if (remotePeers.None(p => p.SteamId.Value == steamId))
{
remotePeers.Add(new RemotePeer(new SteamId(steamId)));
}
Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId); //accept all connections, the server will figure things out later
}
private void OnP2PData(ulong steamId, IReadMessage inc)
{
if (!isActive) { return; }
RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
if (remotePeer == null) { return; }
if (remotePeer.DisconnectTime != null) { return; }
try
{
ProcessP2PData(steamId, remotePeer, inc);
}
catch (Exception e)
{
string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc)
{
var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep())
{
remotePeer.DisconnectTime = null;
ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing");
if (initialization == ConnectionInitialization.SteamTicketAndVersion)
{
remotePeer.Authenticating = true;
var packet = INetSerializableStruct.Read<ClientSteamTicketAndVersionPacket>(inc);
packet.SteamAuthTicket.TryUnwrap(out var ticket);
Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId);
if (authSessionStartState != Steamworks.BeginAuthResult.OK)
{
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
return;
}
}
}
var steamUserId = new SteamId(steamId);
if (remotePeer.Authenticating)
{
remotePeer.UnauthedMessages.Add(new RemotePeer.UnauthedMessage(steamUserId, inc.Buffer));
}
else
{
IWriteMessage outMsg = new WriteOnlyMessage();
WriteSteamId(outMsg, steamUserId);
WriteSteamId(outMsg, remotePeer.OwnerSteamId.Fallback(steamUserId));
outMsg.WriteBytes(inc.Buffer, 0, inc.LengthBytes);
ForwardToServerProcess(outMsg);
}
}
public override void Update(float deltaTime)
{
if (!isActive) { return; }
if (ChildServerRelay.HasShutDown || !ChildServerRelay.IsProcessAlive)
{
var gameClient = GameMain.Client;
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
gameClient?.CreateServerCrashMessage();
return;
}
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
if (remotePeers[i].DisconnectTime != null && remotePeers[i].DisconnectTime < Timing.TotalTime)
{
ClosePeerSession(remotePeers[i]);
}
}
for (int i = 0; i < 100; i++)
{
if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; }
var packet = Steamworks.SteamNetworking.ReadP2PPacket();
if (packet is { SteamId: var steamId, Data: var data })
{
OnP2PData(steamId, new ReadWriteMessage(data, 0, data.Length * 8, false));
receivedBytes += data.Length;
}
}
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);
while (ChildServerRelay.Read(out byte[] incBuf))
{
ChildServerRelay.DisposeLocalHandles();
IReadMessage inc = new ReadOnlyMessage(incBuf, false, 0, incBuf.Length, ServerConnection);
HandleDataMessage(inc);
}
}
private void HandleDataMessage(IReadMessage inc)
{
if (!isActive) { return; }
SteamId recipientSteamId = ReadSteamId(inc);
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (recipientSteamId != selfSteamID)
{
HandleMessageForRemotePeer(peerPacketHeaders, recipientSteamId, inc);
}
else
{
HandleMessageForOwner(peerPacketHeaders, inc);
}
}
private static byte[] GetRemainingBytes(IReadMessage msg)
{
return msg.Buffer[msg.BytePosition..msg.LengthBytes];
}
private void HandleMessageForRemotePeer(PeerPacketHeaders peerPacketHeaders, SteamId recipientSteamId, IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = peerPacketHeaders;
if (!packetHeader.IsServerMessage())
{
DebugConsole.ThrowError("Received non-server message meant for remote peer");
return;
}
RemotePeer? peer = remotePeers.Find(p => p.SteamId == recipientSteamId);
if (peer is null) { return; }
if (packetHeader.IsDisconnectMessage())
{
var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
DisconnectPeer(peer, packet);
return;
}
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = deliveryMethod,
PacketHeader = packetHeader,
Initialization = initialization
});
if (packetHeader.IsConnectionInitializationStep())
{
var initRelayPacket = new SteamP2PInitializationRelayPacket
{
LobbyID = SteamManager.CurrentLobbyID,
Message = new PeerPacketMessage
{
Buffer = GetRemainingBytes(inc)
}
};
outMsg.WriteNetSerializableStruct(initRelayPacket);
}
else
{
byte[] userMessage = GetRemainingBytes(inc);
outMsg.WriteBytes(userMessage, 0, userMessage.Length);
}
ForwardToRemotePeer(deliveryMethod, recipientSteamId, outMsg);
}
private void HandleMessageForOwner(PeerPacketHeaders peerPacketHeaders, IReadMessage inc)
{
var (_, packetHeader, _) = peerPacketHeaders;
if (packetHeader.IsDisconnectMessage())
{
DebugConsole.ThrowError("Received disconnect message from owned server");
return;
}
if (!packetHeader.IsServerMessage())
{
DebugConsole.ThrowError("Received non-server message from owned server");
return;
}
if (packetHeader.IsHeartbeatMessage())
{
return; //no timeout since we're using pipes, ignore this message
}
if (packetHeader.IsConnectionInitializationStep())
{
IWriteMessage outMsg = new WriteOnlyMessage();
WriteSteamId(outMsg, selfSteamID);
WriteSteamId(outMsg, selfSteamID);
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsConnectionInitializationStep,
Initialization = ConnectionInitialization.SteamTicketAndVersion
});
outMsg.WriteNetSerializableStruct(new SteamP2PInitializationOwnerPacket
{
OwnerName = GameMain.Client.Name
});
ForwardToServerProcess(outMsg);
}
else
{
if (initializationStep != ConnectionInitialization.Success)
{
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
}
PeerPacketMessage packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, ServerConnection);
callbacks.OnMessageReceived.Invoke(msg);
}
}
private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
{
peer.DisconnectTime ??= Timing.TotalTime + 1.0;
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(peerDisconnectPacket);
Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable);
sentBytes += outMsg.LengthBytes;
}
private void ClosePeerSession(RemotePeer peer)
{
Steamworks.SteamNetworking.CloseP2PSessionWithUser(peer.SteamId.Value);
remotePeers.Remove(peer);
}
public override void SendPassword(string password)
{
//owner doesn't send passwords
}
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
isActive = false;
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
DisconnectPeer(remotePeers[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
}
Thread.Sleep(100);
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
ClosePeerSession(remotePeers[i]);
}
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
SteamManager.LeaveLobby();
Steamworks.SteamNetworking.ResetActions();
Steamworks.SteamUser.OnValidateAuthTicketResponse -= OnAuthChange;
}
public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
{
if (!isActive) { return; }
IWriteMessage msgToSend = new WriteOnlyMessage();
byte[] msgData = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _);
WriteSteamId(msgToSend, selfSteamID);
WriteSteamId(msgToSend, selfSteamID);
msgToSend.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = deliveryMethod,
PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None
});
msgToSend.WriteNetSerializableStruct(new PeerPacketMessage
{
Buffer = msgData
});
ForwardToServerProcess(msgToSend);
}
protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
{
//not currently used by SteamP2POwnerPeer
throw new NotImplementedException();
}
private static void ForwardToServerProcess(IWriteMessage msg)
{
byte[] bufToSend = new byte[msg.LengthBytes];
msg.Buffer[..msg.LengthBytes].CopyTo(bufToSend.AsSpan());
ChildServerRelay.Write(bufToSend);
}
private void ForwardToRemotePeer(DeliveryMethod deliveryMethod, SteamId recipent, IWriteMessage outMsg)
{
byte[] buf = outMsg.PrepareForSending(compressPastThreshold: false, out _, out int length);
if (length + 4 >= MsgConstants.MTU)
{
DebugConsole.Log($"WARNING: message length comes close to exceeding MTU, forcing reliable send ({length} bytes)");
deliveryMethod = DeliveryMethod.Reliable;
}
bool successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, deliveryMethod.ToSteam());
sentBytes += length;
if (successSend) { return; }
if (deliveryMethod is DeliveryMethod.Unreliable)
{
DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)");
successSend = Steamworks.SteamNetworking.SendP2PPacket(recipent.Value, buf, length, 0, DeliveryMethod.Reliable.ToSteam());
sentBytes += length;
}
if (!successSend)
{
DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)");
}
}
#if DEBUG
public override void ForceTimeOut()
{
//TODO: reimplement?
}
#endif
}
}