Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2024-03-28 14:26:18 -03:00
271 changed files with 13174 additions and 3021 deletions
@@ -1,10 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Barotrauma.Extensions;
#if SERVER
using PipeType = System.IO.Pipes.AnonymousPipeClientStream;
@@ -121,6 +121,7 @@ namespace Barotrauma.Networking
readCancellationToken?.Cancel();
return Option<int>.None();
}
try
{
if (readTask.IsCompleted || readTask.Wait(timeOutMilliseconds, readCancellationToken.Token))
@@ -327,7 +328,36 @@ namespace Barotrauma.Networking
writeManualResetEvent.Set();
}
public static bool Read(out byte[] msg)
private static readonly Stopwatch stopwatch = new Stopwatch();
private const int MaxMilliseconds = 8;
public static IEnumerable<byte[]> Read()
{
stopwatch.Restart();
// To avoid the stopwatch somehow experiencing magical overhead that makes it not even
// start the loop within 8ms, use this bool to force at least one iteration.
bool hasIteratedAtLeastOnce = false;
// If it's taken more than 8 milliseconds to read dequeued messages, take
// a break from reading and allow all of the other logic to run for a tick.
// Otherwise the server may overwhelm the host client with redundant messages
// that are being read too slowly.
while (!hasIteratedAtLeastOnce || stopwatch.ElapsedMilliseconds < MaxMilliseconds)
{
hasIteratedAtLeastOnce = true;
if (!ReadSingleMessage(out var msg))
{
// No more messages available to dequeue, we don't need
// to reach 8 milliseconds to know we're done here
break;
}
yield return msg;
}
stopwatch.Stop();
}
private static bool ReadSingleMessage(out byte[] msg)
{
if (HasShutDown) { msg = null; return false; }
@@ -67,6 +67,7 @@ namespace Barotrauma.Networking
PERMISSIONS, //tell the client which special permissions they have (if any)
ACHIEVEMENT, //give the client a steam achievement
ACHIEVEMENT_STAT, //increment stat for an achievement
CHEATS_ENABLED, //tell the clients whether cheats are on or off
CAMPAIGN_SETUP_INFO,
@@ -151,7 +152,7 @@ namespace Barotrauma.Networking
ServerCrashed,
ServerFull,
AuthenticationRequired,
SteamAuthenticationFailed,
AuthenticationFailed,
SessionTaken,
TooManyFailedLogins,
InvalidName,
@@ -1,24 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class AccountId
{
public abstract string StringRepresentation { get; }
public static Option<AccountId> Parse(string str)
=> ReflectionUtils.ParseDerived<AccountId, string>(str);
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(AccountId a, AccountId b)
=> a.Equals(b);
public static bool operator !=(AccountId a, AccountId b)
=> !(a == b);
}
}
@@ -1,121 +0,0 @@
#nullable enable
using System;
namespace Barotrauma.Networking
{
sealed class SteamId : AccountId
{
public readonly UInt64 Value;
public override string StringRepresentation { get; }
/// Based on information found here: https://developer.valvesoftware.com/wiki/SteamID
/// ------------------------------------------------------------------------------------
/// A SteamID is a 64-bit value (16 hexadecimal digits) that's broken up as follows:
///
/// | a | b | c | d |
/// Most significant - | 01 | 1 | 00001 | 0546779D | - Least significant
///
/// a) 8 bits representing the universe the account belongs to.
/// b) 4 bits representing the type of account. Typically 1.
/// c) 20 bits representing the instance of the account. Typically 1.
/// d) 32 bits representing the account number.
///
/// The account number is additionally broken up as follows:
///
/// | e | f |
/// Most significant - | 0000010101000110011101111001110 | 1 | - Least significant
///
/// e) These are the 31 most significant bits of the account number.
/// f) This is the least significant bit of the account number, discriminated under the name Y for some reason.
///
/// Barotrauma supports two textual representations of SteamIDs:
/// 1. STEAM40: Given this name as it represents 40 of the 64 bits in the ID. The account type and instance both
/// have an implied value of 1. The format is "STEAM_{universe}:{Y}:{restOfAccountNumber}".
/// 2. STEAM64: If STEAM40 does not suffice to represent an ID (i.e. the account type or instance were different
/// from 1), we use "STEAM64_{fullId}" where fullId is the 64-bit decimal representation of the full
/// ID.
private const string steam64Prefix = "STEAM64_";
private const string steam40Prefix = "STEAM_";
private const UInt64 usualAccountInstance = 1;
private const UInt64 usualAccountType = 1;
static UInt64 ExtractBits(UInt64 id, int offset, int numberOfBits)
=> (id >> offset) & ((1ul << numberOfBits) - 1ul);
static UInt64 ExtractY(UInt64 id)
=> ExtractBits(id, offset: 0, numberOfBits: 1);
static UInt64 ExtractAccountNumberRemainder(UInt64 id)
=> ExtractBits(id, offset: 1, numberOfBits: 31);
static UInt64 ExtractAccountInstance(UInt64 id)
=> ExtractBits(id, offset: 32, numberOfBits: 20);
static UInt64 ExtractAccountType(UInt64 id)
=> ExtractBits(id, offset: 52, numberOfBits: 4);
static UInt64 ExtractUniverse(UInt64 id)
=> ExtractBits(id, offset: 56, numberOfBits: 8);
public SteamId(UInt64 value)
{
Value = value;
if (ExtractAccountInstance(Value) == usualAccountInstance
&& ExtractAccountType(Value) == usualAccountType)
{
UInt64 y = ExtractY(Value);
UInt64 accountNumberRemainder = ExtractAccountNumberRemainder(Value);
UInt64 universe = ExtractUniverse(Value);
StringRepresentation = $"{steam40Prefix}{universe}:{y}:{accountNumberRemainder}";
}
else
{
StringRepresentation = $"{steam64Prefix}{Value}";
}
}
public override string ToString() => StringRepresentation;
public new static Option<SteamId> Parse(string str)
{
if (str.IsNullOrWhiteSpace()) { return Option<SteamId>.None(); }
if (str.StartsWith(steam64Prefix, StringComparison.InvariantCultureIgnoreCase)) { str = str[steam64Prefix.Length..]; }
if (UInt64.TryParse(str, out UInt64 retVal) && ExtractAccountInstance(retVal) > 0)
{
return Option<SteamId>.Some(new SteamId(retVal));
}
if (!str.StartsWith(steam40Prefix, StringComparison.InvariantCultureIgnoreCase)) { return Option<SteamId>.None(); }
string[] split = str[steam40Prefix.Length..].Split(':');
if (split.Length != 3) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[0], out UInt64 universe)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[1], out UInt64 y)) { return Option<SteamId>.None(); }
if (!UInt64.TryParse(split[2], out UInt64 accountNumber)) { return Option<SteamId>.None(); }
return Option<SteamId>.Some(
new SteamId((universe << 56)
| usualAccountType << 52
| usualAccountInstance << 32
| (accountNumber << 1)
| y));
}
public override bool Equals(object? obj)
=> obj switch
{
SteamId otherId => this == otherId,
_ => false
};
public override int GetHashCode()
=> Value.GetHashCode();
public static bool operator ==(SteamId a, SteamId b)
=> a.Value == b.Value;
public static bool operator !=(SteamId a, SteamId b)
=> !(a == b);
}
}
@@ -18,15 +18,16 @@ namespace Barotrauma.Networking
/// Other user IDs that this user might be closely tied to,
/// such as the owner of the current copy of Barotrauma
/// </summary>
#warning TODO: make ImmutableArray once feature/inetserializablestruct-improvements gets merged to dev
public readonly AccountId[] OtherMatchingIds;
public readonly ImmutableArray<AccountId> OtherMatchingIds;
public bool IsNone => AccountId.IsNone() && OtherMatchingIds.Length == 0;
public AccountInfo(AccountId accountId, params AccountId[] otherIds) : this(Option<AccountId>.Some(accountId), otherIds) { }
public AccountInfo(Option<AccountId> accountId, params AccountId[] otherIds)
{
AccountId = accountId;
OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToArray();
OtherMatchingIds = otherIds.Where(id => !accountId.ValueEquals(id)).ToImmutableArray();
}
public bool Matches(AccountId accountId)
@@ -1,26 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
abstract class Address
{
public abstract string StringRepresentation { get; }
public static Option<Address> Parse(string str)
=> ReflectionUtils.ParseDerived<Address, string>(str);
public abstract bool IsLocalHost { get; }
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public override string ToString() => StringRepresentation;
public static bool operator ==(Address a, Address b)
=> a.Equals(b);
public static bool operator !=(Address a, Address b)
=> !(a == b);
}
}
@@ -1,79 +0,0 @@
#nullable enable
using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
namespace Barotrauma.Networking
{
sealed class LidgrenAddress : Address
{
public readonly IPAddress NetAddress;
public override string StringRepresentation
=> NetAddress.ToString();
public override bool IsLocalHost => IPAddress.IsLoopback(NetAddress);
public LidgrenAddress(IPAddress netAddress)
{
if (IPAddress.IsLoopback(netAddress)) { netAddress = IPAddress.Loopback; }
if (netAddress.IsIPv4MappedToIPv6) { netAddress = netAddress.MapToIPv4(); }
NetAddress = netAddress;
}
public new static Option<LidgrenAddress> Parse(string endpointStr)
{
if (endpointStr.Equals("localhost", StringComparison.OrdinalIgnoreCase))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(IPAddress.Loopback));
}
else if (IPAddress.TryParse(endpointStr, out IPAddress? netEndpoint))
{
return Option<LidgrenAddress>.Some(new LidgrenAddress(netEndpoint!));
}
return Option<LidgrenAddress>.None();
}
public static Option<LidgrenAddress> ParseHostName(string endpointStr)
{
try
{
var resolvedAddresses = Dns.GetHostAddresses(endpointStr);
return resolvedAddresses.Any()
? Option<LidgrenAddress>.Some(new LidgrenAddress(resolvedAddresses.First()))
: Option<LidgrenAddress>.None();
}
catch (SocketException)
{
return Option<LidgrenAddress>.None();
}
catch (ArgumentOutOfRangeException)
{
return Option<LidgrenAddress>.None();
}
}
public override bool Equals(object? obj)
=> obj switch
{
LidgrenAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> NetAddress.GetHashCode();
public static bool operator ==(LidgrenAddress a, LidgrenAddress b)
{
var addressA = a.NetAddress.MapToIPv6();
var addressB = b.NetAddress.MapToIPv6();
if (IPAddress.IsLoopback(addressA) && IPAddress.IsLoopback(addressB)) { return true; }
return addressA.Equals(addressB);
}
public static bool operator !=(LidgrenAddress a, LidgrenAddress b)
=> !(a == b);
}
}
@@ -1,22 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class PipeAddress : Address
{
public override string StringRepresentation => "PIPE";
public override bool IsLocalHost => true;
public override bool Equals(object? obj)
=> obj is PipeAddress;
public override int GetHashCode() => 1;
public static bool operator ==(PipeAddress a, PipeAddress b)
=> true;
public static bool operator !=(PipeAddress a, PipeAddress b)
=> !(a == b);
}
}
@@ -1,37 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class SteamP2PAddress : Address
{
public readonly SteamId SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override bool IsLocalHost => false;
public SteamP2PAddress(SteamId steamId)
{
SteamId = steamId;
}
public new static Option<SteamP2PAddress> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PAddress(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PAddress otherAddress => this == otherAddress,
_ => false
};
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PAddress a, SteamP2PAddress b)
=> a.SteamId == b.SteamId;
public static bool operator !=(SteamP2PAddress a, SteamP2PAddress b)
=> !(a == b);
}
}
@@ -1,16 +0,0 @@
#nullable enable
namespace Barotrauma.Networking
{
sealed class UnknownAddress : Address
{
public override string StringRepresentation => "Hidden";
public override bool IsLocalHost => false;
public override bool Equals(object? obj)
=> ReferenceEquals(obj, this);
public override int GetHashCode() => 1;
}
}
@@ -0,0 +1,65 @@
#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Barotrauma.Extensions;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
public enum AuthenticationTicketKind
{
SteamAuthTicketForSteamHost = 0,
SteamAuthTicketForEosHost = 1,
EgsOwnershipToken = 2
}
[NetworkSerialize(ArrayMaxSize = UInt16.MaxValue)]
readonly record struct AuthenticationTicket(
AuthenticationTicketKind Kind,
ImmutableArray<byte> Data) : INetSerializableStruct
{
public static async Task<Option<AuthenticationTicket>> Create(Endpoint serverEndpoint)
{
if (SteamManager.IsInitialized && SteamManager.GetSteamId().TryUnwrap(out var steamId))
{
if (serverEndpoint is EosP2PEndpoint)
{
var authTicket = await SteamManager.GetAuthTicketForEosHostAuth();
return authTicket
.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None)
.Select(data => new AuthenticationTicket(AuthenticationTicketKind.SteamAuthTicketForEosHost, data.ToImmutableArray()));
}
else
{
var authTicket = SteamManager.GetAuthSessionTicketForSteamHost(serverEndpoint);
var steamIdBytes = BitConverter.GetBytes(steamId.Value);
return authTicket
.Bind(t => t.Data != null ? Option.Some(t.Data) : Option.None)
.Select(data => new AuthenticationTicket(
AuthenticationTicketKind.SteamAuthTicketForSteamHost,
steamIdBytes.Concat(data).ToImmutableArray()));
}
}
if (EosInterface.IdQueries.GetLoggedInPuids() is { Length: > 0 } puids)
{
var externalAccountIdsResult = await EosInterface.IdQueries.GetSelfExternalAccountIds(puids[0]);
if (externalAccountIdsResult.TryUnwrapSuccess(out var externalAccountIds))
{
var epicAccountIdOption = externalAccountIds.OfType<EpicAccountId>().FirstOrNone();
if (epicAccountIdOption.TryUnwrap(out var epicAccountId))
{
return (await EosInterface.Ownership.GetGameOwnershipToken(epicAccountId))
.Select(t => new AuthenticationTicket(
AuthenticationTicketKind.EgsOwnershipToken,
Encoding.UTF8.GetBytes(t.Jwt.ToString()).ToImmutableArray()));
}
}
}
return Option.None;
}
}
@@ -0,0 +1,38 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
abstract class Authenticator
{
public abstract Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket);
public abstract void EndAuthSession(AccountId accountId);
public static ImmutableDictionary<AuthenticationTicketKind, Authenticator> GetAuthenticatorsForHost(Option<Endpoint> ownerEndpointOption)
{
var authenticators = new Dictionary<AuthenticationTicketKind, Authenticator>();
if (EosInterface.Core.IsInitialized)
{
// Every kind of host should be able to do EOS ID Token authentication if they have EOS enabled
authenticators.Add(AuthenticationTicketKind.EgsOwnershipToken, new EgsOwnershipTokenAuthenticator());
if (ownerEndpointOption.TryUnwrap(out var ownerEndpoint) && ownerEndpoint is EosP2PEndpoint)
{
// EOS P2P hosts do not have access to Steamworks
authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForEosHost, new SteamAuthTicketForEosHostAuthenticator());
}
}
if (!(ownerEndpointOption.TryUnwrap(out var ownerEndpoint2) && ownerEndpoint2 is EosP2PEndpoint) && SteamManager.IsInitialized)
{
// Steam P2P hosts and dedicated servers have access to Steamworks
authenticators.Add(AuthenticationTicketKind.SteamAuthTicketForSteamHost, new SteamAuthTicketForSteamHostAuthenticator());
}
return authenticators.ToImmutableDictionary();
}
}
@@ -0,0 +1,21 @@
using System.Text;
using System.Threading.Tasks;
namespace Barotrauma.Networking;
sealed class EgsOwnershipTokenAuthenticator : Authenticator
{
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
var jwtOption = JsonWebToken.Parse(Encoding.UTF8.GetString(ticket.Data.AsSpan()));
if (!jwtOption.TryUnwrap(out var jwt)) { return AccountInfo.None; }
var ownershipToken = new EosInterface.Ownership.Token(jwt);
var accountIdOption = await ownershipToken.Verify();
if (!accountIdOption.TryUnwrap(out var accountId)) { return AccountInfo.None; }
return new AccountInfo(accountId);
}
public override void EndAuthSession(AccountId accountId) { /* do nothing */ }
}
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using RestSharp;
namespace Barotrauma.Networking;
sealed class SteamAuthTicketForEosHostAuthenticator : Authenticator
{
#warning FIXME change URL back to the non-experimental one once this passes QA
private const string consentServerUrl = "https://barotraumagame.com/baromaster/experimental/";
private const string consentServerFile = "getOwnerSteamId.php";
private const int RemoteRequestVersion = 1;
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
string ticketData = ToolBoxCore.ByteArrayToHexString(ticket.Data);
var client = new RestClient(consentServerUrl);
var request = new RestRequest(consentServerFile, Method.GET);
request.AddParameter("authticket", ticketData);
request.AddParameter("request_version", RemoteRequestVersion);
var response = await client.ExecuteAsync(request, Method.GET);
if (!response.IsSuccessful) { return AccountInfo.None; }
try
{
var jsonDoc = JsonDocument.Parse(response.Content);
Option<SteamId> steamId = Option.None;
Option<SteamId> ownerSteamId = Option.None;
foreach (var property in jsonDoc.RootElement.EnumerateObject())
{
if (!property.Name.ToIdentifier().Contains("SteamId")) { continue; }
var accountIdOption = SteamId.Parse(property.Value.GetString() ?? "");
if (accountIdOption.IsNone()) { continue; }
if (property.Name.ToIdentifier() == "SteamId")
{
steamId = accountIdOption;
}
else if (property.Name.ToIdentifier() == "OwnerSteamId")
{
ownerSteamId = accountIdOption;
}
}
var otherIds = ownerSteamId.TryUnwrap(out var id) ? new AccountId[] { id } : Array.Empty<AccountId>();
return new AccountInfo(steamId.Select(static id => (AccountId)id), otherIds);
}
catch
{
return AccountInfo.None;
}
}
public override void EndAuthSession(AccountId accountId) { /* do nothing */ }
}
@@ -0,0 +1,77 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.Steam;
namespace Barotrauma.Networking;
#if CLIENT
using SteamAuthSessionInterface = Steamworks.SteamUser;
#else
using SteamAuthSessionInterface = Steamworks.SteamServer;
#endif
sealed class SteamAuthTicketForSteamHostAuthenticator : Authenticator
{
private static Steamworks.BeginAuthResult BeginAuthSession(Steamworks.AuthTicket authTicket, SteamId clientSteamId)
{
if (!SteamManager.IsInitialized) { return Steamworks.BeginAuthResult.ServerNotConnectedToSteam; }
if (authTicket.Data is null) { return Steamworks.BeginAuthResult.InvalidTicket; }
DebugConsole.Log("Authenticating Steam client " + clientSteamId);
Steamworks.BeginAuthResult startResult = SteamAuthSessionInterface.BeginAuthSession(authTicket.Data, clientSteamId.Value);
if (startResult != Steamworks.BeginAuthResult.OK)
{
DebugConsole.Log($"Steam authentication failed: failed to start auth session ({startResult})");
}
return startResult;
}
private static void EndAuthSession(SteamId clientSteamId)
{
if (!SteamManager.IsInitialized) { return; }
DebugConsole.Log($"Ending auth session with Steam client {clientSteamId}");
SteamAuthSessionInterface.EndAuthSession(clientSteamId.Value);
}
public override async Task<AccountInfo> VerifyTicket(AuthenticationTicket ticket)
{
if (ticket.Data.Length < 8) { return AccountInfo.None; }
var ticketData = ticket.Data.ToArray();
var steamAuthTicket = new Steamworks.AuthTicket { Data = ticketData[8..] };
var steamId = new SteamId(BitConverter.ToUInt64(ticketData.AsSpan()[..8]));
using var janitor = Janitor.Start();
(Steamworks.AuthResponse AuthResponse, SteamId OwnerSteamId)? authResult = null;
void onValidateAuthTicketResponse(Steamworks.SteamId clientId, Steamworks.SteamId ownerClientId, Steamworks.AuthResponse response)
{
if (clientId != steamId.Value) { response = Steamworks.AuthResponse.AuthTicketInvalid; }
authResult = (response, new SteamId(ownerClientId));
}
SteamAuthSessionInterface.OnValidateAuthTicketResponse += onValidateAuthTicketResponse;
janitor.AddAction(() => SteamAuthSessionInterface.OnValidateAuthTicketResponse -= onValidateAuthTicketResponse);
var beginAuthSessionResult = BeginAuthSession(steamAuthTicket, steamId);
if (beginAuthSessionResult != Steamworks.BeginAuthResult.OK) { return AccountInfo.None; }
while (authResult is null)
{
await Task.Delay(32);
}
if (authResult.Value.AuthResponse != Steamworks.AuthResponse.OK) { return AccountInfo.None; }
return new AccountInfo(steamId, authResult.Value.OwnerSteamId);
}
public override void EndAuthSession(AccountId accountId)
{
if (accountId is not SteamId steamId) { return; }
SteamAuthSessionInterface.EndAuthSession(steamId.Value);
}
}
@@ -0,0 +1,32 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class EosP2PEndpoint : P2PEndpoint
{
public EosInterface.ProductUserId ProductUserId => new EosInterface.ProductUserId((Address as EosP2PAddress)!.EosStringRepresentation);
public EosP2PEndpoint(EosInterface.ProductUserId puid) : this(new EosP2PAddress(puid.Value)) { }
public EosP2PEndpoint(EosP2PAddress address) : base(address) { }
public override string StringRepresentation => (Address as EosP2PAddress)!.StringRepresentation;
public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer");
public override int GetHashCode()
=> (Address as EosP2PAddress)!.GetHashCode();
public override bool Equals(object? obj)
=> obj is EosP2PEndpoint otherEndpoint
&& ProductUserId == otherEndpoint.ProductUserId;
public new static Option<EosP2PEndpoint> Parse(string endpointStr)
=> EosP2PAddress.Parse(endpointStr).Select(eosAddress => new EosP2PEndpoint(eosAddress));
public const string SocketName = "Barotrauma.EosP2PSocket";
public override P2PConnection MakeConnectionFromEndpoint()
=> new EosP2PConnection(this);
}
@@ -60,7 +60,7 @@ namespace Barotrauma.Networking
=> NetEndpoint.GetHashCode();
public static bool operator ==(LidgrenEndpoint a, LidgrenEndpoint b)
=> a.Address.Equals(b.Address) && a.Port == b.Port;
=> a.NetEndpoint.EquivalentTo(b.NetEndpoint);
public static bool operator !=(LidgrenEndpoint a, LidgrenEndpoint b)
=> !(a == b);
@@ -0,0 +1,12 @@
#nullable enable
namespace Barotrauma.Networking;
abstract class P2PEndpoint : Endpoint
{
protected P2PEndpoint(P2PAddress address) : base(address) { }
public abstract P2PConnection MakeConnectionFromEndpoint();
public new static Option<P2PEndpoint> Parse(string str)
=> Endpoint.Parse(str).Bind(ep => ep is P2PEndpoint pep ? Option.Some(pep) : Option.None);
}
@@ -2,36 +2,27 @@
namespace Barotrauma.Networking
{
sealed class SteamP2PEndpoint : Endpoint
sealed class SteamP2PEndpoint : P2PEndpoint
{
public readonly SteamId SteamId;
public SteamId SteamId => (Address as SteamP2PAddress)!.SteamId;
public override string StringRepresentation => SteamId.StringRepresentation;
public override LocalizedString ServerTypeString { get; } = TextManager.Get("SteamP2PServer");
public override LocalizedString ServerTypeString { get; } = TextManager.Get("PlayerHostedServer");
public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId))
{
SteamId = steamId;
}
public new static Option<SteamP2PEndpoint> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId));
public override bool Equals(object? obj)
=> obj switch
{
SteamP2PEndpoint otherEndpoint => this == otherEndpoint,
_ => false
};
public SteamP2PEndpoint(SteamId steamId) : base(new SteamP2PAddress(steamId)) { }
public override int GetHashCode()
=> SteamId.GetHashCode();
public static bool operator ==(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> a.SteamId == b.SteamId;
public override bool Equals(object? obj)
=> obj is SteamP2PEndpoint otherEndpoint
&& this.SteamId == otherEndpoint.SteamId;
public static bool operator !=(SteamP2PEndpoint a, SteamP2PEndpoint b)
=> !(a == b);
public new static Option<SteamP2PEndpoint> Parse(string endpointStr)
=> SteamId.Parse(endpointStr).Select(steamId => new SteamP2PEndpoint(steamId));
public override P2PConnection MakeConnectionFromEndpoint()
=> new SteamP2PConnection(this);
}
}
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Networking;
sealed class MessageDefragmenter
{
private readonly Dictionary<ushort, MessageFragment[]> partialMessages = new Dictionary<ushort, MessageFragment[]>();
public Option<ImmutableArray<byte>> ProcessIncomingFragment(MessageFragment fragment)
{
if (!partialMessages.ContainsKey(fragment.FragmentId.MessageId))
{
partialMessages[fragment.FragmentId.MessageId] = new MessageFragment[fragment.FragmentId.FragmentCount];
}
else if (partialMessages[fragment.FragmentId.MessageId].Length != fragment.FragmentId.FragmentCount)
{
DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " +
$"with a mismatched expected fragment count");
return Option.None;
}
var fragmentBuffer = partialMessages[fragment.FragmentId.MessageId];
if (fragment.FragmentId.FragmentIndex >= fragmentBuffer.Length)
{
DebugConsole.AddWarning($"Got a fragment for message {fragment.FragmentId.MessageId} " +
$"with an index greater than or equal to the expected fragment count ({fragment.FragmentId.FragmentIndex} >= {fragmentBuffer.Length})");
return Option.None;
}
fragmentBuffer[fragment.FragmentId.FragmentIndex] = fragment;
if (fragmentBuffer.All(f => !f.Data.IsDefault && f.FragmentId.MessageId == fragment.FragmentId.MessageId))
{
partialMessages.Remove(fragment.FragmentId.MessageId);
return Option.Some(fragmentBuffer.SelectMany(f => f.Data).ToImmutableArray());
}
return Option.None;
}
}
@@ -0,0 +1,17 @@
using System.Collections.Immutable;
namespace Barotrauma.Networking;
[NetworkSerialize(ArrayMaxSize = MaxSize)]
readonly record struct MessageFragment(
MessageFragment.Id FragmentId,
ImmutableArray<byte> Data) : INetSerializableStruct
{
public const int MaxSize = 1100;
[NetworkSerialize]
public readonly record struct Id(
ushort FragmentIndex,
ushort FragmentCount,
ushort MessageId) : INetSerializableStruct;
}
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma.Networking;
sealed class MessageFragmenter
{
private UInt16 nextFragmentedMessageId = 0;
private readonly List<MessageFragment> fragments = new List<MessageFragment>();
public ImmutableArray<MessageFragment> FragmentMessage(ReadOnlySpan<byte> bytes)
{
UInt16 msgId = nextFragmentedMessageId;
nextFragmentedMessageId++;
int roundedByteCount = bytes.Length;
roundedByteCount += (MessageFragment.MaxSize - (roundedByteCount % MessageFragment.MaxSize)) % MessageFragment.MaxSize;
int fragmentCount = roundedByteCount / MessageFragment.MaxSize;
fragments.Clear();
fragments.EnsureCapacity(fragmentCount);
for (int i = 0; i < fragmentCount; i++)
{
var subset = bytes[(i * MessageFragment.MaxSize)..];
if (subset.Length > MessageFragment.MaxSize) { subset = subset[..MessageFragment.MaxSize]; }
fragments.Add(new MessageFragment(
FragmentId: new MessageFragment.Id(
FragmentIndex: (ushort)i,
FragmentCount: (ushort)fragmentCount,
MessageId: msgId),
Data: subset.ToArray().ToImmutableArray()));
}
return fragments.ToImmutableArray();
}
}
@@ -10,7 +10,11 @@ namespace Barotrauma.Networking
{
public static class MsgConstants
{
public const int MTU = 1200; //TODO: determine dynamically
// MTU currently set to the upper limit of what EOS P2P can do
// TODO: determine dynamically so other protocols can use a larger MTU,
// as well as handle a client with a lower MTU set outside of our control
public const int MTU = 1170;
public const int CompressionThreshold = 1000;
public const int InitialBufferSize = 256;
public const int BufferOverAllocateAmount = 4;
@@ -0,0 +1,8 @@
#nullable enable
namespace Barotrauma.Networking;
sealed class EosP2PConnection : P2PConnection<EosP2PEndpoint>
{
public EosP2PConnection(EosP2PEndpoint endpoint) : base(endpoint) { }
}
@@ -2,7 +2,7 @@
namespace Barotrauma.Networking
{
sealed class LidgrenConnection : NetworkConnection
sealed class LidgrenConnection : NetworkConnection<LidgrenEndpoint>
{
public readonly NetConnection NetConnection;
@@ -8,6 +8,13 @@ namespace Barotrauma.Networking
Disconnected = 0x2
}
abstract class NetworkConnection<T> : NetworkConnection where T : Endpoint
{
protected NetworkConnection(T endpoint) : base(endpoint) { }
public new T Endpoint => (base.Endpoint as T)!;
}
abstract class NetworkConnection
{
public const double TimeoutThreshold = 60.0; //full minute for timeout because loading screens can take quite a while
@@ -23,7 +30,7 @@ namespace Barotrauma.Networking
get; set;
}
public NetworkConnection(Endpoint endpoint)
protected NetworkConnection(Endpoint endpoint)
{
Endpoint = endpoint;
}
@@ -35,9 +42,9 @@ namespace Barotrauma.Networking
public void SetAccountInfo(AccountInfo newInfo)
{
AccountInfo = newInfo;
if (AccountInfo.IsNone) { AccountInfo = newInfo; }
}
public sealed override string ToString()
=> Endpoint.StringRepresentation;
}
@@ -0,0 +1,30 @@
#nullable enable
namespace Barotrauma.Networking;
abstract class P2PConnection<T> : P2PConnection where T : P2PEndpoint
{
protected P2PConnection(T endpoint) : base(endpoint) { }
public new T Endpoint => (base.Endpoint as T)!;
}
abstract class P2PConnection : NetworkConnection<P2PEndpoint>
{
protected P2PConnection(P2PEndpoint endpoint) : base(endpoint)
{
Heartbeat();
}
public double Timeout = 0.0;
public void Decay(float deltaTime)
{
Timeout -= deltaTime;
}
public void Heartbeat()
{
Timeout = TimeoutThreshold;
}
}
@@ -23,11 +23,11 @@ namespace Barotrauma.Networking
=> !(a == b);
}
sealed class PipeConnection : NetworkConnection
sealed class PipeConnection : NetworkConnection<PipeEndpoint>
{
public PipeConnection(AccountId accountId) : base(new PipeEndpoint())
public PipeConnection(Option<AccountId> accountId) : base(new PipeEndpoint())
{
SetAccountInfo(new AccountInfo(Option<AccountId>.Some(accountId)));
SetAccountInfo(new AccountInfo(accountId));
}
}
}
@@ -1,24 +1,9 @@
namespace Barotrauma.Networking
{
sealed class SteamP2PConnection : NetworkConnection
sealed class SteamP2PConnection : P2PConnection<SteamP2PEndpoint>
{
public double Timeout = 0.0;
public SteamP2PConnection(SteamId steamId) : this(new SteamP2PEndpoint(steamId)) { }
public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint)
{
Heartbeat();
}
public void Decay(float deltaTime)
{
Timeout -= deltaTime;
}
public void Heartbeat()
{
Timeout = TimeoutThreshold;
}
public SteamP2PConnection(SteamP2PEndpoint endpoint) : base(endpoint) { }
}
}
@@ -1,57 +0,0 @@
using System;
namespace Barotrauma.Networking
{
public enum DeliveryMethod : int
{
Unreliable = 0x0,
Reliable = 0x1,
ReliableOrdered = 0x2
}
public enum ConnectionInitialization : int
{
//used by all peer implementations
SteamTicketAndVersion = 0x1,
ContentPackageOrder = 0x2,
Password = 0x3,
Success = 0x0,
//used only by SteamP2P implementations
ConnectionStarted = 0x4
}
[Flags]
public enum PacketHeader : int
{
//used by all peer implementations
None = 0x0,
IsCompressed = 0x1,
IsConnectionInitializationStep = 0x2,
//used only by SteamP2P implementations
IsDisconnectMessage = 0x4,
IsServerMessage = 0x8,
IsHeartbeatMessage = 0x10
}
public static class NetworkEnumExtensions
{
public static bool IsCompressed(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsCompressed);
#warning TODO: remove?
public static bool IsConnectionInitializationStep(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsConnectionInitializationStep);
public static bool IsDisconnectMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsDisconnectMessage);
public static bool IsServerMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsServerMessage);
public static bool IsHeartbeatMessage(this PacketHeader h)
=> h.HasFlag(PacketHeader.IsHeartbeatMessage);
}
}
@@ -52,8 +52,7 @@ namespace Barotrauma.Networking
deliveryMethod switch
{
DeliveryMethod.Unreliable => NetDeliveryMethod.Unreliable,
DeliveryMethod.Reliable => NetDeliveryMethod.ReliableUnordered,
DeliveryMethod.ReliableOrdered => NetDeliveryMethod.ReliableOrdered,
DeliveryMethod.Reliable => NetDeliveryMethod.ReliableOrdered,
_ => NetDeliveryMethod.Unreliable
};
@@ -61,7 +60,6 @@ namespace Barotrauma.Networking
deliveryMethod switch
{
DeliveryMethod.Reliable => Steamworks.P2PSend.Reliable,
DeliveryMethod.ReliableOrdered => Steamworks.P2PSend.Reliable,
_ => Steamworks.P2PSend.Unreliable
};
}
@@ -25,34 +25,42 @@ namespace Barotrauma.Networking
}
[NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
internal struct ClientSteamTicketAndVersionPacket : INetSerializableStruct
internal struct ClientAuthTicketAndVersionPacket : INetSerializableStruct
{
public string Name;
public Option<int> OwnerKey;
#warning TODO: do something about the type of this
// It probably should be Option<SteamId> but we shouldn't build support for
// writing SteamIDs to INetSerializableStruct; we should consider adding
// attributes to give custom behaviors to specific members of a struct
public Option<AccountId> SteamId;
public Option<byte[]> SteamAuthTicket;
public Option<AccountId> AccountId;
public Option<AuthenticationTicket> AuthTicket;
public string GameVersion;
public Identifier Language;
}
[NetworkSerialize]
internal struct SteamP2PInitializationRelayPacket : INetSerializableStruct
internal readonly record struct P2POwnerToServerHeader
(string? EndpointStr, AccountInfo AccountInfo) : INetSerializableStruct
{
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
}
[NetworkSerialize]
internal readonly record struct P2PServerToOwnerHeader
(string? EndpointStr) : INetSerializableStruct
{
public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
}
[NetworkSerialize]
internal struct P2PInitializationRelayPacket : INetSerializableStruct
{
public ulong LobbyID;
public PeerPacketMessage Message;
}
[NetworkSerialize]
internal struct SteamP2PInitializationOwnerPacket : INetSerializableStruct
{
public string OwnerName;
}
internal readonly record struct P2PInitializationOwnerPacket(
string Name,
AccountId AccountId)
: INetSerializableStruct;
[NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
@@ -68,7 +76,7 @@ namespace Barotrauma.Networking
public byte[] Buffer;
public readonly int Length => Buffer.Length;
public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length, copyBuf: false);
public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length * 8, copyBuf: false);
public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn);
}
@@ -140,7 +148,8 @@ namespace Barotrauma.Networking
DisconnectReason.ExcessiveDesyncOldEvent => ServerMessage,
DisconnectReason.ExcessiveDesyncRemovedEvent => ServerMessage,
DisconnectReason.SyncTimeout => ServerMessage,
_ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ConnectionLost"))
DisconnectReason.AuthenticationFailed => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ChatMsg.DisconnectReason.AuthenticationRequired")),
_ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback($"{TextManager.Get("ConnectionLost")} ({DisconnectReason})")
};
public LocalizedString ReconnectMessage
@@ -178,15 +187,12 @@ namespace Barotrauma.Networking
or DisconnectReason.TooManyFailedLogins
or DisconnectReason.InvalidVersion);
private const string lidgrenSeparator = ":hankey:";
/// <summary>
/// This exists because Lidgren is a piece of shit and
/// doesn't readily support sending anything other than
/// a string through a disconnect packet, so this thing
/// needs a sufficiently nasty string representation that
/// can be decoded with some certainty that it won't get
/// mangled by user input.
/// This exists because Lidgren doesn't readily support
/// sending anything other than a string through a disconnect
/// packet, so this thing needs a sufficiently nasty string
/// representation that can be decoded with some certainty
/// that it won't get mangled by user input.
/// </summary>
public string ToLidgrenStringRepresentation()
{
@@ -194,7 +200,7 @@ namespace Barotrauma.Networking
=> Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
return DisconnectReason
+ lidgrenSeparator
+ NetworkMagicStrings.LidgrenDisconnectSeparator
+ strToBase64(AdditionalInformation);
}
@@ -209,16 +215,16 @@ namespace Barotrauma.Networking
case Lidgren.Network.NetConnection.NoResponseMessage:
case "Connection timed out":
case "Reconnecting":
return Option<PeerDisconnectPacket>.Some(WithReason(DisconnectReason.Timeout));
return Option.Some(WithReason(DisconnectReason.Timeout));
}
static string base64ToStr(string base64)
=> Encoding.UTF8.GetString(Convert.FromBase64String(base64));
string[] split = str.Split(lidgrenSeparator);
if (split.Length != 2) { return Option<PeerDisconnectPacket>.None(); }
if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option<PeerDisconnectPacket>.None(); }
return Option<PeerDisconnectPacket>.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1])));
string[] split = str.Split(NetworkMagicStrings.LidgrenDisconnectSeparator);
if (split.Length != 2) { return Option.None; }
if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None; }
return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1])));
}
public static PeerDisconnectPacket Custom(string customMessage)
@@ -247,12 +253,12 @@ namespace Barotrauma.Networking
public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
DisconnectReason.AuthenticationFailed,
$"{nameof(Steamworks.BeginAuthResult)}.{error}");
public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error)
=> new PeerDisconnectPacket(
DisconnectReason.SteamAuthenticationFailed,
DisconnectReason.AuthenticationFailed,
$"{nameof(Steamworks.AuthResponse)}.{error}");
}
@@ -0,0 +1,23 @@
namespace Barotrauma.Networking;
public readonly record struct ServerListContentPackageInfo(
string Name, string Hash, Option<ContentPackageId> Id)
{
public ServerListContentPackageInfo(ContentPackage pkg)
: this(pkg.Name, pkg.Hash.StringRepresentation, pkg.UgcId) {}
public static Option<ServerListContentPackageInfo> ParseSingleEntry(string singleEntry)
{
if (singleEntry.SplitEscaped(',') is not { Count: 3 } split) { return Option.None; }
return Option.Some(
new ServerListContentPackageInfo(
split[0],
split[1],
ContentPackageId.Parse(split[2])));
}
public override string ToString()
=> new[] { Name, Hash, Id.Select(id => id.StringRepresentation).Fallback("") }
.JoinEscaped(',');
}
@@ -4,6 +4,7 @@ using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
@@ -296,7 +297,7 @@ namespace Barotrauma.Networking
if (typeName != null || property.PropertyType.IsEnum)
{
NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName);
UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5);
UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5);
if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message
if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); }
netProperties.Add(key, netPropertyData);
@@ -313,7 +314,7 @@ namespace Barotrauma.Networking
if (typeName != null || property.PropertyType.IsEnum)
{
NetPropertyData netPropertyData = new NetPropertyData(networkMember.KarmaManager, property, typeName);
UInt32 key = ToolBox.IdentifierToUint32Hash(netPropertyData.Name, md5);
UInt32 key = ToolBoxCore.IdentifierToUint32Hash(netPropertyData.Name, md5);
if (netProperties.ContainsKey(key)) { throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); }
netProperties.Add(key, netPropertyData);
}
@@ -1140,5 +1141,47 @@ namespace Barotrauma.Networking
msg.WriteUInt16((UInt16)subList.FindIndex(s => s.Name.Equals(submarineName, StringComparison.OrdinalIgnoreCase)));
}
}
public void UpdateServerListInfo(Action<Identifier, object> setter)
{
void set(string key, object obj) => setter(key.ToIdentifier(), obj);
set("ServerName", ServerName);
set("MaxPlayers", MaxPlayers);
set("HasPassword", HasPassword);
set("message", ServerMessageText);
set("version", GameMain.Version);
set("playercount", GameMain.NetworkMember.ConnectedClients.Count);
set("contentpackages", ContentPackageManager.EnabledPackages.All.Where(p => p.HasMultiplayerSyncedContent));
set("modeselectionmode", ModeSelectionMode);
set("subselectionmode", SubSelectionMode);
set("voicechatenabled", VoiceChatEnabled);
set("allowspectating", AllowSpectating);
set("allowrespawn", AllowRespawn);
set("traitors", TraitorProbability.ToString(CultureInfo.InvariantCulture));
set("friendlyfireenabled", AllowFriendlyFire);
set("karmaenabled", KarmaEnabled);
set("gamestarted", GameMain.NetworkMember.GameStarted);
set("gamemode", GameModeIdentifier);
set("playstyle", PlayStyle);
set("language", Language.ToString());
#if SERVER
set("eoscrossplay", EosInterface.Core.IsInitialized);
#else
set("eoscrossplay", EosInterface.IdQueries.IsLoggedIntoEosConnect || Eos.EosSessionManager.CurrentOwnedSession.IsSome());
#endif
if (GameMain.NetLobbyScreen?.SelectedSub != null)
{
set("submarine", GameMain.NetLobbyScreen.SelectedSub.Name);
}
if (Steamworks.SteamClient.IsLoggedOn)
{
string pingLocation = Steamworks.SteamNetworkingUtils.LocalPingLocation?.ToString();
if (!pingLocation.IsNullOrEmpty())
{
set("steampinglocation", pingLocation);
}
}
}
}
}