Unstable v0.19.5.0
This commit is contained in:
+11
@@ -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();
|
||||
}
|
||||
}
|
||||
+67
@@ -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; }
|
||||
}
|
||||
}
|
||||
+35
@@ -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());
|
||||
}
|
||||
}
|
||||
+17
@@ -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();
|
||||
}
|
||||
}
|
||||
+160
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user