Unstable v0.19.5.0

This commit is contained in:
Juan Pablo Arce
2022-09-14 12:47:17 -03:00
parent 3f2c843247
commit 1fd2a51bbb
158 changed files with 5702 additions and 4813 deletions
@@ -134,6 +134,14 @@ namespace Barotrauma.Networking
return Permissions.HasFlag(permission);
}
public void ResetVotes()
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
}
partial void DisposeProjSpecific()
{
if (VoipQueue != null)
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,8 @@
using Barotrauma.Steam;
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
@@ -10,37 +12,14 @@ namespace Barotrauma.Networking
public ImmutableArray<ServerContentPackage> ServerContentPackages { get; set; } =
ImmutableArray<ServerContentPackage>.Empty;
public delegate void MessageCallback(IReadMessage message);
public delegate void DisconnectCallback(bool disableReconnect);
public delegate void DisconnectMessageCallback(string message);
public delegate void PasswordCallback(int salt, int retries);
public delegate void InitializationCompleteCallback();
[Obsolete("TODO: delete in nr3-layer-1-2-cleanup")]
public readonly struct Callbacks
public readonly record struct Callbacks(
Callbacks.MessageCallback OnMessageReceived,
Callbacks.DisconnectCallback OnDisconnect,
Callbacks.InitializationCompleteCallback OnInitializationComplete)
{
public readonly MessageCallback OnMessageReceived;
public readonly DisconnectCallback OnDisconnect;
public readonly DisconnectMessageCallback OnDisconnectMessageReceived;
public readonly PasswordCallback OnRequestPassword;
public readonly InitializationCompleteCallback OnInitializationComplete;
public Callbacks(MessageCallback onMessageReceived,
DisconnectCallback onDisconnect,
DisconnectMessageCallback onDisconnectMessageReceived,
PasswordCallback onRequestPassword,
InitializationCompleteCallback onInitializationComplete)
{
OnMessageReceived = onMessageReceived;
OnDisconnect = onDisconnect;
OnDisconnectMessageReceived = onDisconnectMessageReceived;
OnRequestPassword = onRequestPassword;
OnInitializationComplete = onInitializationComplete;
}
public delegate void MessageCallback(IReadMessage message);
public delegate void DisconnectCallback(PeerDisconnectPacket disconnectPacket);
public delegate void InitializationCompleteCallback();
}
protected readonly Callbacks callbacks;
@@ -51,6 +30,8 @@ namespace Barotrauma.Networking
protected readonly bool isOwner;
protected readonly Option<int> ownerKey;
protected bool isActive;
public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey)
{
ServerEndpoint = serverEndpoint;
@@ -60,7 +41,7 @@ namespace Barotrauma.Networking
}
public abstract void Start();
public abstract void Close(string? msg = null, bool disableReconnect = false);
public abstract void Close(PeerDisconnectPacket peerDisconnectPacket);
public abstract void Update(float deltaTime);
public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true);
public abstract void SendPassword(string password);
@@ -71,6 +52,12 @@ namespace Barotrauma.Networking
public bool ContentPackageOrderReceived { get; protected set; }
protected int passwordSalt;
protected Steamworks.AuthTicket? steamAuthTicket;
private GUIMessageBox? passwordMsgBox;
public bool WaitingForPassword
=> isActive && initializationStep == ConnectionInitialization.Password
&& passwordMsgBox != null
&& GUIMessageBox.MessageBoxes.Contains(passwordMsgBox);
public struct IncomingInitializationMessage
{
@@ -112,8 +99,9 @@ namespace Barotrauma.Networking
}
case ConnectionInitialization.ContentPackageOrder:
{
if (initializationStep == ConnectionInitialization.SteamTicketAndVersion ||
initializationStep == ConnectionInitialization.Password)
if (initializationStep
is ConnectionInitialization.SteamTicketAndVersion
or ConnectionInitialization.Password)
{
initializationStep = ConnectionInitialization.ContentPackageOrder;
}
@@ -155,10 +143,57 @@ namespace Barotrauma.Networking
var passwordPacket = INetSerializableStruct.Read<ServerPeerPasswordPacket>(inc.Message);
if (WaitingForPassword) { return; }
passwordPacket.Salt.TryUnwrap(out passwordSalt);
passwordPacket.RetriesLeft.TryUnwrap(out var retries);
callbacks.OnRequestPassword.Invoke(passwordSalt, retries);
LocalizedString pwMsg = TextManager.Get("PasswordRequired");
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);
var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) })
{
Censor = true
};
if (retries > 0)
{
var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center);
incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y);
passwordHolder.Recalculate();
}
passwordMsgBox.Content.Recalculate();
passwordMsgBox.Content.RectTransform.MinSize = new Point(0, passwordMsgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height));
passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y));
var okButton = passwordMsgBox.Buttons[0];
okButton.OnClicked += (_, __) =>
{
SendPassword(passwordBox.Text);
return true;
};
okButton.OnClicked += passwordMsgBox.Close;
var cancelButton = passwordMsgBox.Buttons[1];
cancelButton.OnClicked = (_, __) =>
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
passwordMsgBox?.Close(); passwordMsgBox = null;
return true;
};
passwordBox.OnEnterPressed += (_, __) =>
{
okButton.OnClicked.Invoke(okButton, okButton.UserData);
return true;
};
passwordBox.Select();
break;
}
}
@@ -9,7 +9,6 @@ namespace Barotrauma.Networking
{
internal sealed class LidgrenClientPeer : ClientPeer
{
private bool isActive;
private NetClient? netClient;
private readonly NetPeerConfiguration netPeerConfiguration;
@@ -94,7 +93,7 @@ namespace Barotrauma.Networking
if (isOwner && !(ChildServerRelay.Process is { HasExited: false }))
{
Close();
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
@@ -140,8 +139,10 @@ namespace Barotrauma.Networking
var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success)
if (packetHeader.IsConnectionInitializationStep())
{
if (initializationStep == ConnectionInitialization.Success) { return; }
ReadConnectionInitializationStep(new IncomingInitializationMessage
{
InitializationStep = initialization ?? throw new Exception("Initialization step missing"),
@@ -170,8 +171,9 @@ namespace Barotrauma.Networking
{
case NetConnectionStatus.Disconnected:
string disconnectMsg = inc.ReadString();
Close(disconnectMsg);
callbacks.OnDisconnectMessageReceived.Invoke(disconnectMsg);
var peerDisconnectPacket =
PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg);
Close(peerDisconnectPacket.Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown)));
break;
}
}
@@ -198,7 +200,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body);
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -206,13 +208,13 @@ namespace Barotrauma.Networking
isActive = false;
netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value);
netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation());
netClient = null;
steamAuthTicket?.Cancel();
steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
@@ -10,7 +10,6 @@ namespace Barotrauma.Networking
{
internal sealed class SteamP2PClientPeer : ClientPeer
{
private bool isActive;
private readonly SteamId hostSteamId;
private double timeout;
private double heartbeatTimer;
@@ -97,8 +96,7 @@ namespace Barotrauma.Networking
if (steamId != hostSteamId.Value) { return; }
Close($"SteamP2P connection failed: {error}");
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}");
Close(PeerDisconnectPacket.SteamP2PError(error));
}
private void OnP2PData(ulong steamId, byte[] data, int dataLength)
@@ -117,8 +115,10 @@ namespace Barotrauma.Networking
if (!packetHeader.IsServerMessage()) { return; }
if (packetHeader.IsConnectionInitializationStep() && initialization.HasValue)
if (packetHeader.IsConnectionInitializationStep())
{
if (!initialization.HasValue) { return; }
var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
SteamManager.JoinLobby(relayPacket.LobbyID, false);
@@ -127,7 +127,7 @@ namespace Barotrauma.Networking
incomingInitializationMessages.Add(new IncomingInitializationMessage
{
InitializationStep = initialization.Value,
Message = relayPacket.Message.GetReadMessage()
Message = relayPacket.Message.GetReadMessageUncompressed()
});
}
}
@@ -138,13 +138,12 @@ namespace Barotrauma.Networking
else if (packetHeader.IsDisconnectMessage())
{
PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
Close(packet.Message);
callbacks.OnDisconnectMessageReceived.Invoke(packet.Message);
Close(packet);
}
else
{
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
incomingDataMessages.Add(packet.GetReadMessage());
incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!));
}
}
@@ -170,14 +169,12 @@ namespace Barotrauma.Networking
{
if (state.P2PSessionError != Steamworks.P2PSessionError.None)
{
Close($"SteamP2P error code: {state.P2PSessionError}");
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state.P2PSessionError}");
Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
}
}
else
{
Close("SteamP2P connection could not be established");
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PError.ToString());
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
}
connectionStatusTimer = 1.0f;
@@ -212,8 +209,7 @@ namespace Barotrauma.Networking
if (timeout < 0.0)
{
Close("Timed out");
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PTimeOut.ToString());
Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut));
return;
}
@@ -221,20 +217,26 @@ namespace Barotrauma.Networking
{
if (incomingDataMessages.Count > 0)
{
var incomingMessage = incomingDataMessages.First();
byte incomingHeader = incomingMessage.LengthBytes > 0 ? incomingMessage.PeekByte() : (byte)0;
if (ContentPackageOrderReceived)
void initializationError(string errorMsg, string analyticsTag)
{
#warning: TODO: do not allow completing initialization until content package order has been received?
string errorMsg = $"Error during connection initialization: completed initialization before receiving content package order. Incoming header: {incomingHeader}";
GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:ContentPackageOrderNotReceived", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError(errorMsg);
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
if (!ContentPackageOrderReceived)
{
initializationError(
errorMsg: "Error during connection initialization: completed initialization before receiving content package order.",
analyticsTag: "ContentPackageOrderNotReceived");
return;
}
if (ServerContentPackages.Length == 0)
{
string errorMsg = $"Error during connection initialization: list of content packages enabled on the server was empty when completing initialization. Incoming header: {incomingHeader}";
GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError(errorMsg);
initializationError(
errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.",
analyticsTag: "NoContentPackages");
return;
}
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
@@ -320,7 +322,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body);
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -334,12 +336,7 @@ namespace Barotrauma.Networking
PacketHeader = PacketHeader.IsDisconnectMessage,
Initialization = null
};
var body = new PeerDisconnectPacket
{
Message = msg ?? "Disconnected"
};
SendMsgInternal(headers, body);
SendMsgInternal(headers, peerDisconnectPacket);
Thread.Sleep(100);
@@ -349,7 +346,7 @@ namespace Barotrauma.Networking
steamAuthTicket?.Cancel();
steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
@@ -9,8 +9,6 @@ namespace Barotrauma.Networking
{
sealed class SteamP2POwnerPeer : ClientPeer
{
private bool isActive;
private readonly SteamId selfSteamID;
private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0));
@@ -68,6 +66,7 @@ namespace Barotrauma.Networking
: throw new InvalidOperationException("Steamworks not initialized");
}
public override void Start()
{
if (isActive) { return; }
@@ -93,22 +92,13 @@ namespace Barotrauma.Networking
private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status)
{
RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
DebugConsole.Log($"{steamId} validation: {status}, {remotePeer != null}");
if (remotePeer == null) { return; }
if (remotePeer.Authenticated)
{
if (status != Steamworks.AuthResponse.OK)
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication status changed: {status}");
}
return;
}
if (status == Steamworks.AuthResponse.OK)
{
if (remotePeer.Authenticated) { return; }
SteamId ownerSteamId = new SteamId(ownerId);
remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId);
remotePeer.Authenticated = true;
@@ -126,7 +116,7 @@ namespace Barotrauma.Networking
}
else
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication failed: {status}");
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status));
}
}
@@ -171,7 +161,7 @@ namespace Barotrauma.Networking
Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId);
if (authSessionStartState != Steamworks.BeginAuthResult.OK)
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam auth session failed to start: {authSessionStartState}");
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
return;
}
}
@@ -197,9 +187,9 @@ namespace Barotrauma.Networking
{
if (!isActive) { return; }
if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true))
if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false }))
{
Close();
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
@@ -279,7 +269,7 @@ namespace Barotrauma.Networking
if (packetHeader.IsDisconnectMessage())
{
var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
DisconnectPeer(peer, packet.Message);
DisconnectPeer(peer, packet);
return;
}
@@ -366,30 +356,20 @@ namespace Barotrauma.Networking
}
}
private void DisconnectPeer(RemotePeer peer, string msg)
private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
{
if (!string.IsNullOrWhiteSpace(msg))
{
peer.DisconnectTime ??= Timing.TotalTime + 1.0;
peer.DisconnectTime ??= Timing.TotalTime + 1.0;
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(new PeerDisconnectPacket
{
Message = msg
});
Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable);
sentBytes += outMsg.LengthBytes;
}
else
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
ClosePeerSession(peer);
}
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)
@@ -403,7 +383,7 @@ namespace Barotrauma.Networking
//owner doesn't send passwords
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -411,7 +391,7 @@ namespace Barotrauma.Networking
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString());
DisconnectPeer(remotePeers[i], peerDisconnectPacket);
}
Thread.Sleep(100);
@@ -423,7 +403,7 @@ namespace Barotrauma.Networking
ChildServerRelay.ClosePipes();
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
SteamManager.LeaveLobby();
Steamworks.SteamNetworking.ResetActions();
@@ -1,517 +0,0 @@
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Barotrauma.Networking
{
class ServerInfo
{
public Endpoint Endpoint;
#region TODO: genericize
public int QueryPort;
public UInt64 LobbyID;
public Steamworks.Data.NetPingLocation? PingLocation;
#endregion
public bool OwnerVerified;
private string serverName;
public string ServerName
{
get { return serverName; }
set
{
serverName = value;
if (serverName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); }
}
}
public string ServerMessage;
public bool GameStarted;
public int PlayerCount;
public int MaxPlayers;
public bool HasPassword;
public bool PingChecked;
public int Ping = -1;
//null value means that the value isn't known (the server may be using
//an old version of the game that didn't report these values or the FetchRules query to Steam may not have finished yet)
// TODO: death to Nullable<T>!!!!
public SelectionMode? ModeSelectionMode;
public SelectionMode? SubSelectionMode;
public bool? AllowSpectating;
public bool? VoipEnabled;
public bool? KarmaEnabled;
public bool? FriendlyFireEnabled;
public bool? AllowRespawn;
public YesNoMaybe? TraitorsEnabled;
public Identifier GameMode;
public PlayStyle? PlayStyle;
public bool Recent;
public bool Favorite;
public bool? RespondedToSteamQuery = null;
public Steamworks.Friend? SteamFriend;
public Steamworks.SteamMatchmakingPingResponse MatchmakingPingResponse;
public string GameVersion;
public List<string> ContentPackageNames
{
get;
private set;
} = new List<string>();
public List<string> ContentPackageHashes
{
get;
private set;
} = new List<string>();
public List<ulong> ContentPackageWorkshopIds
{
get;
private set;
} = new List<ulong>();
public void CreatePreviewWindow(GUIFrame frame)
{
if (frame == null) { return; }
frame.ClearChildren();
var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont)
{
ToolTip = ServerName,
CanBeFocused = false
};
title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f));
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight),
"", null, "GUIServerListFavoriteTickBox")
{
Selected = Favorite,
ToolTip = TextManager.Get(Favorite ? "removefromfavorites" : "addtofavorites"),
OnSelected = (tickbox) =>
{
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(this);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(this);
}
tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites");
return true;
}
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"),
string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion))
{
CanBeFocused = false
};
bool hidePlaystyleBanner = !PlayStyle.HasValue;
if (!hidePlaystyleBanner)
{
PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious;
Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle];
float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height;
var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform),
playStyleBannerSprite, null, true);
var playStyleName = new GUITextBlock(
new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform)
{ RelativeOffset = new Vector2(0.0f, 0.06f) },
TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"),
TextManager.Get("servertag." + playStyle)), textColor: Color.White,
font: GUIStyle.SmallFont, textAlignment: Alignment.Center,
color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader");
playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint();
playStyleName.RectTransform.IsFixedSize = true;
}
var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
Endpoint.ServerTypeString,
textAlignment: Alignment.TopLeft)
{
CanBeFocused = false
};
serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f));
var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform))
{
Stretch = true
};
// playstyle tags -----------------------------------------------------------------------------
var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f,
CanBeFocused = true
};
var playStyleTags = GetPlayStyleTags();
foreach (string tag in playStyleTags)
{
if (!ServerListScreen.PlayStyleIcons.ContainsKey(tag)) { continue; }
new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform),
ServerListScreen.PlayStyleIcons[tag], scaleToFit: true)
{
ToolTip = TextManager.Get("servertagdescription." + tag),
Color = ServerListScreen.PlayStyleIconColors[tag]
};
}
playStyleContainer.Recalculate();
// -----------------------------------------------------------------------------
float elementHeight = 0.075f;
// Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null);
var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true };
var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true)
{
CanBeFocused = false
};
serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); };
msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); };
var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode"));
new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform),
TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value),
textAlignment: Alignment.Right);
GUITextBlock playStyleText = null;
if (hidePlaystyleBanner && PlayStyle.HasValue)
{
PlayStyle playStyle = PlayStyle.Value;
playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle"));
new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right);
}
var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection"));
new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right);
var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection"));
new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(!ModeSelectionMode.HasValue ? "Unknown" : ModeSelectionMode.Value.ToString()), textAlignment: Alignment.Right);
if (gameMode.TextSize.X + gameMode.GetChild<GUITextBlock>().TextSize.X > gameMode.Rect.Width ||
subSelection.TextSize.X + subSelection.GetChild<GUITextBlock>().TextSize.X > subSelection.Rect.Width ||
modeSelection.TextSize.X + modeSelection.GetChild<GUITextBlock>().TextSize.X > modeSelection.Rect.Width)
{
gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont;
gameMode.GetChild<GUITextBlock>().Font = subSelection.GetChild<GUITextBlock>().Font = modeSelection.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
if (playStyleText != null)
{
playStyleText.Font = playStyleText.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
}
}
var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating"))
{
CanBeFocused = false
};
if (!AllowSpectating.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowSpectating.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
allowSpectating.Selected = AllowSpectating.Value;
var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning"))
{
CanBeFocused = false
};
if (!AllowRespawn.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowRespawn.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
allowRespawn.Selected = AllowRespawn.Value;
/*var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled"))
{
CanBeFocused = false
};
if (!VoipEnabled.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), voipEnabledTickBox.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
voipEnabledTickBox.Selected = VoipEnabled.Value;*/
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform))
{
ScrollBarVisible = true,
OnSelected = (component, o) => false
};
if (ContentPackageNames.Count == 0)
{
new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center)
{
CanBeFocused = false
};
}
else
{
for (int i = 0; i < ContentPackageNames.Count; i++)
{
var packageText = new GUITickBox(
new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform)
{ MinSize = new Point(0, 15) },
ContentPackageNames[i])
{
Enabled = false
};
packageText.Box.Enabled = true;
packageText.TextBlock.Enabled = true;
if (i < ContentPackageHashes.Count)
{
if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i]))
{
packageText.TextColor = GUIStyle.Green;
packageText.Selected = true;
}
//workshop download link found
else if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0)
{
packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]);
}
else //no package or workshop download link found (TODO: update text to say that they could be downloaded through the server)
{
packageText.TextColor = GameMain.VanillaContent.NameMatches(ContentPackageNames[i]) ? GUIStyle.Red : GUIStyle.Yellow;
packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage",
("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i]));
}
}
}
}
// -----------------------------------------------------------------------------
foreach (GUIComponent c in content.Children)
{
if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; }
}
}
public IEnumerable<string> GetPlayStyleTags()
{
List<string> tags = new List<string>();
if (KarmaEnabled.HasValue)
{
tags.Add(KarmaEnabled.Value ? "karma.true" : "karma.false");
}
if (TraitorsEnabled.HasValue)
{
tags.Add(TraitorsEnabled.Value == YesNoMaybe.Maybe ?
"traitors.maybe" :
(TraitorsEnabled.Value == YesNoMaybe.Yes ? "traitors.true" : "traitors.false"));
}
if (VoipEnabled.HasValue)
{
tags.Add(VoipEnabled.Value ? "voip.true" : "voip.false");
}
if (FriendlyFireEnabled.HasValue)
{
tags.Add(FriendlyFireEnabled.Value ? "friendlyfire.true" : "friendlyfire.false");
}
if (ContentPackageNames.Count > 0)
{
tags.Add(ContentPackageNames.Count > 1 || !GameMain.VanillaContent.NameMatches(ContentPackageNames[0]) ? "modded.true" : "modded.false");
}
return tags;
}
public static ServerInfo FromXElement(XElement element)
{
string endpointStr
= element.GetAttributeString("Endpoint", null)
?? element.GetAttributeString("OwnerID", null)
?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}";
if (!(Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint))) { return null; }
ServerInfo info = new ServerInfo
{
ServerName = element.GetAttributeString("ServerName", ""),
ServerMessage = element.GetAttributeString("ServerMessage", ""),
Endpoint = endpoint,
QueryPort = !string.IsNullOrEmpty(element.GetAttributeString("QueryPort", string.Empty)) ? element.GetAttributeInt("QueryPort", 0) : 0,
GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty),
GameVersion = element.GetAttributeString("GameVersion", ""),
MaxPlayers = Math.Min(element.GetAttributeInt("MaxPlayers", 0), NetConfig.MaxPlayers),
HasPassword = element.GetAttributeBool("HasPassword", false),
RespondedToSteamQuery = null
};
if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; }
if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; }
if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; }
if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = modeSelectionTemp; }
if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; }
if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; }
if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; }
return info;
}
public void QueryLiveInfo(Action<Networking.ServerInfo> onServerRulesReceived, Action<Networking.ServerInfo> onQueryDone)
{
if (!SteamManager.IsInitialized) { return; }
if (QueryPort != 0 && Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ipAddress } })
{
if (MatchmakingPingResponse is { QueryActive: true })
{
MatchmakingPingResponse.Cancel();
}
MatchmakingPingResponse = new Steamworks.SteamMatchmakingPingResponse(
(server) =>
{
ServerName = server.Name;
RespondedToSteamQuery = true;
PlayerCount = server.Players;
MaxPlayers = server.MaxPlayers;
HasPassword = server.Passworded;
PingChecked = true;
Ping = server.Ping;
LobbyID = 0;
TaskPool.Add("QueryServerRules (QueryLiveInfo)", server.QueryRulesAsync(),
(t) =>
{
onQueryDone(this);
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + ServerName);
return;
}
t.TryGetResult(out Dictionary<string, string> rules);
SteamManager.AssignServerRulesToServerInfo(rules, this);
onServerRulesReceived(this);
});
},
() =>
{
RespondedToSteamQuery = false;
});
MatchmakingPingResponse.HQueryPing(ipAddress, QueryPort);
}
else if (Endpoint is SteamP2PEndpoint { SteamId: var ownerId })
{
SteamFriend ??= new Steamworks.Friend(ownerId.Value);
if (LobbyID == 0)
{
TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(),
(t) =>
{
onQueryDone(this);
if ((SteamFriend?.IsPlayingThisGame ?? false) && ((SteamFriend?.GameInfo?.Lobby?.Id ?? 0) != 0))
{
LobbyID = SteamFriend?.GameInfo?.Lobby?.Id.Value ?? 0;
Steamworks.SteamMatchmaking.OnLobbyDataChanged += UpdateInfoFromSteamworksLobby;
SteamFriend?.GameInfo?.Lobby?.Refresh();
}
else
{
RespondedToSteamQuery = false;
}
});
}
else
{
onQueryDone(this);
}
}
}
private void UpdateInfoFromSteamworksLobby(Steamworks.Data.Lobby lobby)
{
if (lobby.Id != LobbyID) { return; }
Steamworks.SteamMatchmaking.OnLobbyDataChanged -= UpdateInfoFromSteamworksLobby;
if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { return; }
bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword);
int.TryParse(lobby.GetData("playercount"), out int currPlayers);
int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers);
if (!SteamId.Parse(lobby.GetData("lobbyowner")).TryUnwrap(out var ownerId)) { return; }
if (!(Endpoint is SteamP2PEndpoint { SteamId: var id }) || id != ownerId) { return; }
ServerName = lobby.GetData("name");
PlayerCount = currPlayers;
MaxPlayers = maxPlayers;
HasPassword = hasPassword;
RespondedToSteamQuery = true;
PingChecked = false;
OwnerVerified = true;
SteamManager.AssignLobbyDataToServerInfo(lobby, this);
}
public XElement ToXElement()
{
if (Endpoint is null)
{
return null; //can't save this one since it's not set up correctly
}
XElement element = new XElement("ServerInfo");
element.SetAttributeValue("ServerName", ServerName);
element.SetAttributeValue("ServerMessage", ServerMessage);
element.SetAttributeValue("Endpoint", Endpoint.ToString());
element.SetAttributeValue("GameMode", GameMode);
element.SetAttributeValue("GameVersion", GameVersion ?? "");
element.SetAttributeValue("MaxPlayers", MaxPlayers);
if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); }
if (TraitorsEnabled.HasValue) { element.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.Value.ToString()); }
if (SubSelectionMode.HasValue) { element.SetAttributeValue("SubSelectionMode", SubSelectionMode.Value.ToString()); }
if (ModeSelectionMode.HasValue) { element.SetAttributeValue("ModeSelectionMode", ModeSelectionMode.Value.ToString()); }
if (VoipEnabled.HasValue) { element.SetAttributeValue("VoipEnabled", VoipEnabled.Value.ToString()); }
if (KarmaEnabled.HasValue) { element.SetAttributeValue("KarmaEnabled", KarmaEnabled.Value.ToString()); }
if (FriendlyFireEnabled.HasValue) { element.SetAttributeValue("FriendlyFireEnabled", FriendlyFireEnabled.Value.ToString()); }
element.SetAttributeValue("HasPassword", HasPassword.ToString());
return element;
}
public override bool Equals(object obj)
{
return obj is ServerInfo other ? Equals(other) : base.Equals(obj);
}
public bool Equals(ServerInfo other)
{
return
other.Endpoint == Endpoint &&
(other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0);
}
/// <summary>
/// This class is trash, so punish its use by making it horribly inefficient in hashsets
/// Doing anything else here would make it cause even more bugs
/// </summary>
public override int GetHashCode() => 0;
public bool MatchesByEndpoint(ServerInfo other)
{
return other.Endpoint == Endpoint;
}
}
}
@@ -0,0 +1,11 @@
#nullable enable
namespace Barotrauma
{
abstract class FriendProvider
{
public abstract ServerListScreen.FriendInfo[] RetrieveFriends();
public abstract void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize);
public abstract string GetUserName();
}
}
@@ -0,0 +1,67 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.Networking;
using Barotrauma.Steam;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
class SteamFriendProvider : FriendProvider
{
private static ServerListScreen.FriendInfo FromSteamFriend(Steamworks.Friend steamFriend)
=> new ServerListScreen.FriendInfo(
steamFriend.Name,
new SteamId(steamFriend.Id),
steamFriend.State switch
{
Steamworks.FriendState.Offline => ServerListScreen.FriendInfo.Status.Offline,
Steamworks.FriendState.Invisible => ServerListScreen.FriendInfo.Status.Offline,
_ when steamFriend.IsPlayingThisGame => ServerListScreen.FriendInfo.Status.PlayingBarotrauma,
_ when steamFriend.GameInfo is { GameID: var gameId } && gameId > 0 => ServerListScreen.FriendInfo.Status.PlayingAnotherGame,
_ => ServerListScreen.FriendInfo.Status.NotPlaying
})
{
ServerName = steamFriend.GetRichPresence("servername"),
ConnectCommand = steamFriend.GetRichPresence("connect") is { } connectCmd
? ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCmd))
: Option<ConnectCommand>.None()
};
public override ServerListScreen.FriendInfo[] RetrieveFriends()
=> SteamManager.IsInitialized
? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToArray()
: Array.Empty<ServerListScreen.FriendInfo>();
public override void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize)
{
if (!(friend.Id is SteamId steamId)) { return; }
Func<Steamworks.SteamId, Task<Steamworks.Data.Image?>> avatarFunc = avatarSize switch
{
ServerListScreen.AvatarSize.Small => Steamworks.SteamFriends.GetSmallAvatarAsync,
ServerListScreen.AvatarSize.Medium => Steamworks.SteamFriends.GetMediumAvatarAsync,
ServerListScreen.AvatarSize.Large => Steamworks.SteamFriends.GetLargeAvatarAsync,
};
TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(steamId.Value), task =>
{
if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; }
if (!(img is { } avatarImage)) { return; }
if (friend.Avatar.TryUnwrap(out var prevAvatar))
{
prevAvatar.Remove();
}
#warning TODO: create an avatar atlas?
var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height);
avatarTexture.SetData(avatarImage.Data);
friend.Avatar = Option<Sprite>.Some(new Sprite(avatarTexture, null, null));
});
}
public override string GetUserName()
=> SteamManager.GetUsername();
}
}
@@ -0,0 +1,196 @@
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using Steamworks.Data;
using Color = Microsoft.Xna.Framework.Color;
using Socket = System.Net.Sockets.Socket;
namespace Barotrauma.Networking
{
static class PingUtils
{
private static readonly Dictionary<IPAddress, int> activePings = new Dictionary<IPAddress, int>();
private static bool steamPingInfoReady;
public static void QueryPingData()
{
steamPingInfoReady = false;
if (SteamManager.IsInitialized)
{
TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task =>
{
steamPingInfoReady = true;
});
}
}
public static void GetServerPing(ServerInfo serverInfo, Action<ServerInfo> onPingDiscovered)
{
if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; }
switch (serverInfo.Endpoint)
{
case LidgrenEndpoint { NetEndpoint: { Address: var address } }:
GetIPAddressPing(serverInfo, address, onPingDiscovered);
break;
case SteamP2PEndpoint steamP2PEndpoint:
TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})",
EstimateSteamLobbyPing(serverInfo),
t =>
{
if (!t.TryGetResult(out Option<int> ping)) { return; }
serverInfo.Ping = ping;
onPingDiscovered(serverInfo);
});
break;
}
}
private readonly ref struct LobbyDataChangedEventHandler
{
private readonly Action<Lobby> action;
public LobbyDataChangedEventHandler(Action<Lobby> action)
{
this.action = action;
Steamworks.SteamMatchmaking.OnLobbyDataChanged += action;
}
public void Dispose()
{
Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action;
}
}
public static async Task<Lobby?> GetSteamLobbyForUser(SteamId steamId)
{
var steamFriend = new Steamworks.Friend(steamId.Value);
await steamFriend.RequestInfoAsync();
var friendLobby = steamFriend.GameInfo?.Lobby;
if (!(friendLobby is { } lobby)) { return null; }
bool waiting = true;
Lobby loadedLobby = default;
void finishWaiting(Steamworks.Data.Lobby l)
{
loadedLobby = l;
waiting = false;
}
using (new LobbyDataChangedEventHandler(finishWaiting))
{
lobby.Refresh();
for (int i = 0;; i++)
{
if (!waiting) { break; }
if (i >= 100) { return null; }
}
}
return loadedLobby;
}
private static async Task<Option<int>> EstimateSteamLobbyPing(ServerInfo serverInfo)
{
if (!(serverInfo.Endpoint is SteamP2PEndpoint { SteamId: var ownerId })) { return Option<int>.None(); }
while (!steamPingInfoReady) { await Task.Delay(50); }
Lobby lobby;
if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src))
{
lobby = src.Lobby;
}
else
{
var friendLobby = await GetSteamLobbyForUser(ownerId);
if (friendLobby is null) { return Option<int>.None(); }
lobby = friendLobby.Value;
}
var pingLocation = NetPingLocation.TryParseFromString(lobby.GetData("pinglocation"));
if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue)
{
int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value);
return ping >= 0 ? Option<int>.Some(ping) : Option<int>.None();
}
else
{
return Option<int>.None();
}
}
private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action<ServerInfo> onPingDiscovered)
{
lock (activePings)
{
if (activePings.ContainsKey(address)) { return; }
activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0);
}
serverInfo.Ping = Option<int>.None();
TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000),
rtt =>
{
if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
onPingDiscovered(serverInfo);
lock (activePings)
{
activePings.Remove(address);
}
});
}
private static async Task<Option<int>> PingServerAsync(IPAddress ipAddress, int timeOut)
{
await Task.Yield();
bool shouldGo = false;
while (!shouldGo)
{
lock (activePings)
{
shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25;
}
await Task.Delay(25);
}
if (ipAddress == null) { return Option<int>.None(); }
//don't attempt to ping if the address is IPv6 and it's not supported
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
Ping ping = new Ping();
byte[] buffer = new byte[32];
try
{
PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true));
return pingReply.Status switch
{
IPStatus.Success => Option<int>.Some((int)pingReply.RoundtripTime),
_ => Option<int>.None(),
};
}
catch (Exception ex)
{
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
#if DEBUG
DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
#endif
return Option<int>.None();
}
}
}
}
@@ -0,0 +1,509 @@
#nullable enable
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Steam;
namespace Barotrauma.Networking
{
sealed class ServerInfo : ISerializableEntity
{
public abstract class DataSource
{
public static Option<DataSource> Parse(XElement element)
=> ReflectionUtils.ParseDerived<DataSource, XElement>(element);
public abstract void Write(XElement element);
}
public Endpoint Endpoint { get; private set; }
public Option<DataSource> MetadataSource = Option<DataSource>.None();
[Serialize("", IsPropertySaveable.Yes)]
public string ServerName { get; set; } = "";
[Serialize("", IsPropertySaveable.Yes)]
public string ServerMessage { get; set; } = "";
public int PlayerCount { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int MaxPlayers { get; set; }
public bool GameStarted { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool HasPassword { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier GameMode { get; set; }
[Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)]
public SelectionMode ModeSelectionMode { get; set; }
[Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)]
public SelectionMode SubSelectionMode { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowSpectating { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool VoipEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool KarmaEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool FriendlyFireEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowRespawn { get; set; }
[Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)]
public YesNoMaybe TraitorsEnabled { get; set; }
[Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)]
public PlayStyle PlayStyle { get; set; }
public Version GameVersion { get; set; } = new Version(0,0,0,0);
public Option<int> Ping = Option<int>.None();
public bool Checked = false;
public readonly struct ContentPackageInfo
{
public readonly string Name;
public readonly string Hash;
public readonly Option<ContentPackageId> Id;
public ContentPackageInfo(string name, string hash, Option<ContentPackageId> id)
{
Name = name;
Hash = hash;
Id = id;
}
public ContentPackageInfo(ContentPackage pkg)
{
Name = pkg.Name;
Hash = pkg.Hash.StringRepresentation;
Id = pkg.UgcId;
}
}
public ImmutableArray<ContentPackageInfo> ContentPackages;
public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name));
public ServerInfo(Endpoint endpoint)
{
SerializableProperties = SerializableProperty.GetProperties(this);
Endpoint = endpoint;
ContentPackages = ImmutableArray<ContentPackageInfo>.Empty;
}
public static ServerInfo FromServerConnection(NetworkConnection connection, ServerSettings serverSettings)
{
var serverInfo = new ServerInfo(connection.Endpoint)
{
GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty,
GameStarted = Screen.Selected != GameMain.NetLobbyScreen,
GameVersion = GameMain.Version,
PlayerCount = GameMain.Client.ConnectedClients.Count,
ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ContentPackageInfo(p)).ToImmutableArray(),
Ping = GameMain.Client.Ping,
// -------------------------------------
// Settings that cannot be copied via
// SerializableProperty because they do
// not implement the attribute
ServerName = serverSettings.ServerName,
ServerMessage = serverSettings.ServerMessageText,
// -------------------------------------
// Settings that cannot be copied via
// SerializableProperty due to name mismatch
HasPassword = serverSettings.HasPassword,
VoipEnabled = serverSettings.VoiceChatEnabled,
FriendlyFireEnabled = serverSettings.AllowFriendlyFire,
// -------------------------------------
Checked = true
};
var serverInfoSerializableProperties
= SerializableProperty.GetProperties(serverInfo);
var serverSettingsSerializableProperties
= SerializableProperty.GetProperties(serverSettings);
var intersection = serverInfoSerializableProperties.Keys
.Where(serverSettingsSerializableProperties.ContainsKey);
foreach (var key in intersection)
{
var propToGet = serverSettingsSerializableProperties[key];
var propToSet = serverInfoSerializableProperties[key];
if (!propToGet.PropertyInfo.CanRead) { continue; }
if (!propToSet.PropertyInfo.CanWrite) { continue; }
propToSet.SetValue(
serverInfo,
propToGet.GetValue(serverSettings));
}
return serverInfo;
}
public void CreatePreviewWindow(GUIFrame frame)
{
frame.ClearChildren();
var serverListScreen = GameMain.ServerListScreen;
var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont)
{
ToolTip = ServerName,
CanBeFocused = false
};
title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f));
bool isFavorite = serverListScreen.IsFavorite(this);
static LocalizedString favoriteTickBoxToolTip(bool isFavorite)
=> TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites");
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight),
"", null, "GUIServerListFavoriteTickBox")
{
UserData = this,
Selected = isFavorite,
ToolTip = favoriteTickBoxToolTip(isFavorite),
OnSelected = tickbox =>
{
ServerInfo info = (ServerInfo)tickbox.UserData;
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(info);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(info);
}
tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected);
return true;
}
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"),
GameVersion.ToString()))
{
CanBeFocused = false
};
PlayStyle playStyle = PlayStyle;
Sprite? playStyleBannerSprite = GUIStyle.GetComponentStyle($"PlayStyleBanner.{playStyle}")?.GetSprite(GUIComponent.ComponentState.None);
GUIComponent playStyleBanner;
Color playStyleBannerColor;
if (playStyleBannerSprite != null)
{
float playStyleBannerAspectRatio = (float)playStyleBannerSprite.SourceRect.Width / (float)playStyleBannerSprite.SourceRect.Height;
playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), frame.RectTransform, scaleBasis: ScaleBasis.BothWidth),
playStyleBannerSprite, null, true);
playStyleBannerColor = playStyleBannerSprite.SourceElement.GetAttributeColor("bannercolor", Color.Black);
}
else
{
playStyleBanner = new GUIFrame(new RectTransform((1.0f, 0.2f), frame.RectTransform), style: null)
{
Color = Color.Black,
DisabledColor = Color.Black,
OutlineColor = Color.Black,
PressedColor = Color.Black,
SelectedColor = Color.Black,
HoverColor = Color.Black
};
playStyleBannerColor = Color.Black;
}
var playStyleName = new GUITextBlock(
new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform)
{ RelativeOffset = new Vector2(0.0f, 0.06f) },
TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"),
TextManager.Get($"servertag.{playStyle}")), textColor: Color.White,
font: GUIStyle.SmallFont, textAlignment: Alignment.Center,
color: playStyleBannerColor, style: "GUISlopedHeader");
playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint();
playStyleName.RectTransform.IsFixedSize = true;
var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
Endpoint?.ServerTypeString ?? string.Empty,
textAlignment: Alignment.TopLeft)
{
CanBeFocused = false
};
serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f));
var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform))
{
Stretch = true
};
// playstyle tags -----------------------------------------------------------------------------
var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f,
CanBeFocused = true
};
var playStyleTags = GetPlayStyleTags();
foreach (var tag in playStyleTags)
{
var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}")
?.GetSprite(GUIComponent.ComponentState.None);
if (playStyleIcon is null) { continue; }
new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform),
playStyleIcon, scaleToFit: true)
{
ToolTip = TextManager.Get($"servertagdescription.{tag}"),
Color = Color.White
};
}
playStyleContainer.Recalculate();
// -----------------------------------------------------------------------------
float elementHeight = 0.075f;
// Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null);
var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true };
var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage ?? string.Empty, font: GUIStyle.SmallFont, wrap: true)
{
CanBeFocused = false
};
serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); };
msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); };
var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode"));
new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform),
TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value),
textAlignment: Alignment.Right);
GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle"));
new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right);
var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection"));
new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(SubSelectionMode.ToString()), textAlignment: Alignment.Right);
var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection"));
new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(ModeSelectionMode.ToString()), textAlignment: Alignment.Right);
if (gameMode.TextSize.X + gameMode.GetChild<GUITextBlock>().TextSize.X > gameMode.Rect.Width ||
subSelection.TextSize.X + subSelection.GetChild<GUITextBlock>().TextSize.X > subSelection.Rect.Width ||
modeSelection.TextSize.X + modeSelection.GetChild<GUITextBlock>().TextSize.X > modeSelection.Rect.Width)
{
gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont;
gameMode.GetChild<GUITextBlock>().Font = subSelection.GetChild<GUITextBlock>().Font = modeSelection.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
playStyleText.Font = playStyleText.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
}
var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating"))
{
CanBeFocused = false
};
allowSpectating.Selected = AllowSpectating;
var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning"))
{
CanBeFocused = false
};
allowRespawn.Selected = AllowRespawn;
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform))
{
ScrollBarVisible = true,
OnSelected = (component, o) => false
};
if (ContentPackages.Length == 0)
{
new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center)
{
CanBeFocused = false
};
}
else
{
foreach (var package in ContentPackages)
{
var packageText = new GUITickBox(
new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform)
{ MinSize = new Point(0, 15) },
package.Name)
{
CanBeFocused = false
};
if (!string.IsNullOrEmpty(package.Hash))
{
if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == package.Hash))
{
packageText.TextColor = GUIStyle.Green;
packageText.Selected = true;
}
//workshop download link found
else if (package.Id is Some<ContentPackageId> { Value: var ugcId } && ugcId is SteamWorkshopId)
{
packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name);
}
else //no package or workshop download link found
{
packageText.TextColor = GameMain.VanillaContent.NameMatches(package.Name) ? GUIStyle.Red : GUIStyle.Yellow;
packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage",
("[contentpackage]", package.Name), ("[hash]", package.Hash));
}
}
}
}
// -----------------------------------------------------------------------------
foreach (GUIComponent c in content.Children)
{
if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; }
}
}
public IEnumerable<Identifier> GetPlayStyleTags()
{
yield return $"Karma.{KarmaEnabled}".ToIdentifier();
yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier();
yield return $"VoIP.{VoipEnabled}".ToIdentifier();
yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier();
yield return $"Modded.{ContentPackages.Any()}".ToIdentifier();
}
public void UpdateInfo(Func<string, string?> valueGetter)
{
ServerMessage = valueGetter("message") ?? "";
GameVersion = Version.TryParse(valueGetter("version"), out var version)
? version
: GameMain.Version;
if (int.TryParse(valueGetter("playercount"), out int playerCount)) { PlayerCount = playerCount; }
if (int.TryParse(valueGetter("maxplayernum"), out int maxPlayers)) { MaxPlayers = maxPlayers; }
if (Enum.TryParse(valueGetter("modeselectionmode"), out SelectionMode modeSelectionMode)) { ModeSelectionMode = modeSelectionMode; }
if (Enum.TryParse(valueGetter("subselectionmode"), out SelectionMode subSelectionMode)) { SubSelectionMode = subSelectionMode; }
HasPassword = getBool("haspassword");
GameStarted = getBool("gamestarted");
KarmaEnabled = getBool("karmaenabled");
FriendlyFireEnabled = getBool("friendlyfireenabled");
AllowSpectating = getBool("allowspectating");
AllowRespawn = getBool("allowrespawn");
VoipEnabled = getBool("voicechatenabled");
GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty;
if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; }
if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; }
ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray();
bool getBool(string key)
{
string? data = valueGetter(key);
return bool.TryParse(data, out var result) && result;
}
}
private static ContentPackageInfo[] ExtractContentPackageInfo(Func<string, string?> valueGetter)
{
string? joinedNames = valueGetter("contentpackage");
string? joinedHashes = valueGetter("contentpackagehash");
string? joinedWorkshopIds = valueGetter("contentpackageid");
string[] contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty<string>() : joinedNames.Split(',');
string[] contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty<string>() : joinedHashes.Split(',');
#warning TODO: genericize
ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray();
if (contentPackageNames.Length != contentPackageHashes.Length
|| contentPackageHashes.Length != contentPackageIds.Length)
{
return Array.Empty<ContentPackageInfo>();
}
return contentPackageNames
.Zip(contentPackageHashes, (name, hash) => (name, hash))
.Zip(contentPackageIds, (t1, id) =>
new ContentPackageInfo(
t1.name,
t1.hash,
Option<ContentPackageId>.Some(new SteamWorkshopId(id))))
.ToArray();
}
public static Option<ServerInfo> FromXElement(XElement element)
{
string endpointStr
= element.GetAttributeString("Endpoint", null)
?? element.GetAttributeString("OwnerID", null)
?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}";
if (!Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) { return Option<ServerInfo>.None(); }
var gameVersionStr = element.GetAttributeString("GameVersion", "");
if (!Version.TryParse(gameVersionStr, out var gameVersion)) { gameVersion = GameMain.Version; }
var info = new ServerInfo(endpoint)
{
GameVersion = gameVersion
};
SerializableProperty.DeserializeProperties(info, element);
info.MetadataSource = DataSource.Parse(element);
return Option<ServerInfo>.Some(info);
}
public XElement ToXElement()
{
XElement element = new XElement(GetType().Name);
element.SetAttributeValue("Endpoint", Endpoint.ToString());
element.SetAttributeValue("GameVersion", GameVersion.ToString());
SerializableProperty.SerializeProperties(this, element, saveIfDefault: true);
if (MetadataSource.TryUnwrap(out var dataSource))
{
dataSource.Write(element);
}
return element;
}
public override bool Equals(object? obj)
{
return obj is ServerInfo other && Equals(other);
}
public bool Equals(ServerInfo other)
=> other.Endpoint == Endpoint;
public override int GetHashCode() => Endpoint.GetHashCode();
string ISerializableEntity.Name => "ServerInfo";
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
}
}
@@ -0,0 +1,35 @@
#nullable enable
using System;
using System.Collections.Immutable;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
class CompositeServerProvider : ServerProvider
{
private readonly ImmutableArray<ServerProvider> providers;
public CompositeServerProvider(params ServerProvider[] providers)
{
this.providers = providers.ToImmutableArray();
}
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
int providersFinished = 0;
void ackFinishedProvider()
{
providersFinished++;
if (providersFinished == providers.Length)
{
onQueryCompleted();
}
}
providers.ForEach(p => p.RetrieveServers(onServerDataReceived, ackFinishedProvider));
}
public override void Cancel()
=> providers.ForEach(p => p.Cancel());
}
}
@@ -0,0 +1,17 @@
#nullable enable
using System;
using Barotrauma.Networking;
namespace Barotrauma
{
abstract class ServerProvider
{
public void RetrieveServers(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
Cancel();
RetrieveServersImpl(onServerDataReceived, onQueryCompleted);
}
protected abstract void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted);
public abstract void Cancel();
}
}
@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Steam;
namespace Barotrauma
{
sealed class SteamDedicatedServerProvider : ServerProvider
{
public class DataSource : ServerInfo.DataSource
{
public readonly UInt16 QueryPort;
public DataSource(UInt16 queryPort)
{
QueryPort = queryPort;
}
/// Method is invoked via reflection,
/// see <see cref="ServerInfo.DataSource.Parse" />
public new static Option<DataSource> Parse(XElement element)
=> element.TryGetAttributeInt("QueryPort", out var result)
? result switch
{
var invalidPort when invalidPort <= 0 || invalidPort > UInt16.MaxValue => Option<DataSource>.None(),
var queryPort => Option<DataSource>.Some(new DataSource((UInt16)queryPort))
}
: Option<DataSource>.None();
public override void Write(XElement element) => element.SetAttributeValue("QueryPort", QueryPort);
}
private static Option<ServerInfo> InfoFromListEntry(Steamworks.Data.ServerInfo entry) =>
entry.Name.IsNullOrEmpty()
? Option<ServerInfo>.None()
: Option<ServerInfo>.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort))
{
ServerName = entry.Name,
HasPassword = entry.Passworded,
PlayerCount = entry.Players,
MaxPlayers = entry.MaxPlayers,
MetadataSource = Option<ServerInfo.DataSource>.Some(new DataSource((UInt16)entry.QueryPort))
});
private static void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action<ServerInfo> onServerDataReceived)
{
TaskPool.Add($"QueryServerRules (GetServers, {entry.Name}, {entry.Address})", entry.QueryRulesAsync(),
t =>
{
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}");
return;
}
if (!t.TryGetResult(out Dictionary<string, string> rules)) { return; }
if (rules is null) { return; }
if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; }
serverInfo.UpdateInfo(key =>
{
if (rules.TryGetValue(key, out var val)) { return val; }
return null;
});
serverInfo.Checked = true; //rules != null;
onServerDataReceived(serverInfo);
});
}
private static void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action<ServerInfo> onServerDataReceived)
{
//TODO: do we still want to list unresponsive servers?
if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; }
onServerDataReceived(serverInfo);
}
private Steamworks.ServerList.Internet? serverQuery;
private CoroutineHandle? queryCoroutine;
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
if (!SteamManager.IsInitialized)
{
onQueryCompleted();
return;
}
// All lambdas in here must only capture this call's
// query, not the provider's latest query
var selfServerQuery = new Steamworks.ServerList.Internet();
serverQuery = selfServerQuery;
ConcurrentQueue<Steamworks.Data.ServerInfo> responsiveServers =
new ConcurrentQueue<Steamworks.Data.ServerInfo>();
ConcurrentQueue<Steamworks.Data.ServerInfo> unresponsiveServers =
new ConcurrentQueue<Steamworks.Data.ServerInfo>();
selfServerQuery.OnResponsiveServer = responsiveServers.Enqueue;
selfServerQuery.OnUnresponsiveServer = unresponsiveServers.Enqueue;
void dequeue(int? limit = null)
{
for (int i = 0; (!limit.HasValue || i < limit) && responsiveServers.TryDequeue(out var serverInfo); i++)
{
HandleResponsiveServer(serverInfo, onServerDataReceived);
}
for (int i = 0; (!limit.HasValue || i < limit) && unresponsiveServers.TryDequeue(out var serverInfo); i++)
{
HandleUnresponsiveServer(serverInfo, onServerDataReceived);
}
}
IEnumerable<CoroutineStatus> dequeueCoroutine()
{
while (true)
{
dequeue(limit: 20);
yield return new WaitForSeconds(0.1f, ignorePause: true);
}
}
var selfQueryCoroutine = CoroutineManager.StartCoroutine(dequeueCoroutine(),
$"{nameof(SteamDedicatedServerProvider)}.{nameof(RetrieveServers)}.{nameof(dequeueCoroutine)}");
queryCoroutine = selfQueryCoroutine;
TaskPool.Add("RunServerQuery", selfServerQuery.RunQueryAsync(timeoutSeconds: 30f),
t =>
{
try
{
// Clear the callbacks because it's too late now, we want to get this over with
selfServerQuery.OnResponsiveServer = null;
selfServerQuery.OnUnresponsiveServer = null;
CoroutineManager.StopCoroutines(selfQueryCoroutine);
dequeue();
if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); }
selfServerQuery.Dispose();
}
finally
{
onQueryCompleted();
}
});
}
public override void Cancel()
{
if (queryCoroutine != null) { CoroutineManager.StopCoroutines(queryCoroutine); }
serverQuery?.Dispose();
serverQuery = null;
}
}
}
@@ -0,0 +1,107 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Steam;
namespace Barotrauma
{
sealed class SteamP2PServerProvider : ServerProvider
{
public class DataSource : ServerInfo.DataSource
{
public readonly Steamworks.Data.Lobby Lobby;
public override void Write(XElement element) { /* do nothing */ }
public DataSource(Steamworks.Data.Lobby lobby)
{
Lobby = lobby;
}
}
private object? queryRef = null;
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
if (!SteamManager.IsInitialized)
{
onQueryCompleted();
return;
}
// All lambdas and local methods in here must only capture
// this call's query, not the provider's latest query
var selfQueryRef = new object();
queryRef = selfQueryRef;
Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery()
.FilterDistanceWorldwide()
.WithMaxResults(50);
// Steamworks is unable to retrieve more than 50 lobbies per request
// (see https://partner.steamgames.com/doc/features/multiplayer/matchmaking#3)
// To work around this, we'll make up to 10 requests, asking to ignore
// all previous results in each subsequent request.
#warning TODO: do something less horrible here?
int requestCount = 0;
HashSet<SteamId> retrieved = new HashSet<SteamId>();
void startQuery()
{
if (requestCount >= 10) { return; }
requestCount++;
TaskPool.Add($"LobbyQuery.RequestAsync ({requestCount})", lobbyQuery.RequestAsync(), onRequestComplete);
}
void onRequestComplete(Task t)
{
// If queryRef != selfQueryRef, this query was cancelled
if (!ReferenceEquals(selfQueryRef, queryRef)) { return; }
if (!t.TryGetResult(out Steamworks.Data.Lobby[] lobbies)
|| lobbies is null
|| lobbies.Length == 0)
{
onQueryCompleted();
return;
}
foreach (var lobby in lobbies)
{
string lobbyOwnerStr = lobby.GetData("lobbyowner");
lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr);
string serverName = lobby.GetData("name");
if (string.IsNullOrEmpty(serverName)) { continue; }
var ownerId = SteamId.Parse(lobbyOwnerStr);
if (!ownerId.TryUnwrap(out var lobbyOwnerId)) { continue; }
if (retrieved.Contains(lobbyOwnerId)) { continue; }
retrieved.Add(lobbyOwnerId);
var serverInfo = new ServerInfo(new SteamP2PEndpoint(lobbyOwnerId))
{
ServerName = serverName,
MetadataSource = Option<ServerInfo.DataSource>.Some(new DataSource(lobby))
};
serverInfo.UpdateInfo(key => lobby.GetData(key));
serverInfo.Checked = true;
onServerDataReceived(serverInfo);
}
startQuery();
}
startQuery();
}
public override void Cancel()
{
queryRef = null;
}
}
}
@@ -125,8 +125,6 @@ namespace Barotrauma.Networking
public void ClientRead(IReadMessage incMsg)
{
cachedServerListInfo = null;
NetFlags requiredFlags = (NetFlags)incMsg.ReadByte();
if (requiredFlags.HasFlag(NetFlags.Name))
@@ -146,7 +144,6 @@ namespace Barotrauma.Networking
AllowFileTransfers = incMsg.ReadBoolean();
incMsg.ReadPadBits();
TickRate = incMsg.ReadRangedInteger(1, 60);
GameMain.NetworkMember.TickRate = TickRate;
if (requiredFlags.HasFlag(NetFlags.Properties))
{
@@ -241,7 +238,8 @@ namespace Barotrauma.Networking
outMsg.WriteSingle(levelDifficulty ?? -1000.0f);
outMsg.WriteBoolean(useRespawnShuttle ?? UseRespawnShuttle);
outMsg.WriteBoolean(useRespawnShuttle != null);
outMsg.WriteBoolean(useRespawnShuttle ?? false);
outMsg.WriteBoolean(autoRestart != null);
outMsg.WriteBoolean(autoRestart ?? false);
@@ -1057,15 +1055,7 @@ namespace Barotrauma.Networking
}
settingsFrame = null;
}
return false;
}
private ServerInfo cachedServerListInfo = null;
public ServerInfo GetServerListInfo()
{
cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this);
return cachedServerListInfo;
}
}
}
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using System;
namespace Barotrauma
{
@@ -111,6 +112,23 @@ namespace Barotrauma
}
}
public void ResetVotes(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
client.ResetVotes();
}
foreach (VoteType voteType in Enum.GetValues(typeof(VoteType)))
{
SetVoteCountYes(voteType, 0);
SetVoteCountNo(voteType, 0);
SetVoteCountMax(voteType, 0);
}
UpdateVoteTexts(connectedClients, VoteType.Mode);
UpdateVoteTexts(connectedClients, VoteType.Sub);
}
public void ClientWrite(IWriteMessage msg, VoteType voteType, object data)
{
msg.WriteByte((byte)voteType);