using Barotrauma.Extensions; using Barotrauma.Networking; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using RestSharp; using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma { class ServerListScreen : Screen { //how often the client is allowed to refresh servers private TimeSpan AllowedRefreshInterval = new TimeSpan(0, 0, 3); private GUIFrame menu; private GUIListBox serverList; private GUIFrame serverPreview; private GUIButton joinButton; private ServerInfo selectedServer; //friends list private GUILayoutGroup friendsButtonHolder; private GUIButton friendsDropdownButton; private GUIListBox friendsDropdown; private class FriendInfo { public UInt64 SteamID; public string Name; public Sprite Sprite; public string StatusText; public bool PlayingThisGame; public bool PlayingAnotherGame; public string ConnectName; public string ConnectEndpoint; public UInt64 ConnectLobby; public bool InServer { get { return PlayingThisGame && !string.IsNullOrWhiteSpace(StatusText) && (!string.IsNullOrWhiteSpace(ConnectEndpoint) || ConnectLobby != 0); } } } private List friendsList; private GUIFrame friendPopup; private double friendsListUpdateTime; //favorite servers/history private const string recentServersFile = "Data/recentservers.xml"; private const string favoriteServersFile = "Data/favoriteservers.xml"; private List favoriteServers; private List recentServers; private readonly HashSet activePings = new HashSet(); private enum ServerListTab { All = 0, Favorites = 1, Recent = 2 }; private ServerListTab selectedTab; private ServerListTab SelectedTab { get { return selectedTab; } set { if (selectedTab == value) { return; } var tabVals = Enum.GetValues(typeof(ServerListTab)); for (int i = 0; i < tabVals.Length; i++) { tabButtons[i].Selected = false; } tabButtons[(int)value].Selected = true; selectedTab = value; FilterServers(); } } private GUIButton[] tabButtons; private static Sprite[] playStyleBanners; //server playstyle and tags public static Sprite[] PlayStyleBanners { get { if (playStyleBanners == null) { LoadPlayStyleBanners(); } return playStyleBanners; } } public static Color[] PlayStyleColors { get; private set; } public GUITextBox ClientNameBox { get; private set; } public static Dictionary PlayStyleIcons { get; private set; } public static Dictionary PlayStyleIconColors { get; private set; } private bool masterServerResponded; private IRestResponse masterServerResponse; private readonly float[] columnRelativeWidth = new float[] { 0.1f, 0.1f, 0.7f, 0.12f, 0.08f, 0.08f }; private readonly string[] columnLabel = new string[] { "ServerListCompatible", "ServerListHasPassword", "ServerListName", "ServerListRoundStarted", "ServerListPlayers", "ServerListPing" }; private GUILayoutGroup labelHolder; private readonly List labelTexts = new List(); //filters private GUITextBox searchBox; private GUITickBox filterSameVersion; private GUITickBox filterPassword; private GUITickBox filterIncompatible; private GUITickBox filterFull; private GUITickBox filterEmpty; private GUITickBox filterWhitelisted; private GUITickBox filterFriendlyFire; private GUITickBox filterKarma; private GUITickBox filterTraitor; private GUITickBox filterModded; private GUITickBox filterVoip; private List playStyleTickBoxes; private List gameModeTickBoxes; private string sortedBy; private GUIButton serverPreviewToggleButton; //a timer for preventing the client from spamming the refresh button faster than AllowedRefreshInterval private DateTime refreshDisableTimer; private bool waitingForRefresh; private bool steamPingInfoReady; private const float sidebarWidth = 0.2f; public ServerListScreen() { GameMain.Instance.OnResolutionChanged += CreateUI; CreateUI(); } private void CreateUI() { menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) }); var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center)) { RelativeSpacing = 0.02f, Stretch = true }; //------------------------------------------------------------------------------------- //Top row //------------------------------------------------------------------------------------- var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true }; var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), TextManager.Get("JoinServer"), font: GUI.LargeFont) { Padding = Vector4.Zero, ForceUpperCase = true, AutoScaleHorizontal = true }; var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false }; var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUI.SubHeadingFont); ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") { Text = GameMain.Config.PlayerName, MaxTextLength = Client.MaxNameLength, OverflowClip = true }; if (string.IsNullOrEmpty(ClientNameBox.Text)) { ClientNameBox.Text = SteamManager.GetUsername(); } ClientNameBox.OnTextChanged += (textbox, text) => { GameMain.Config.PlayerName = text; return true; }; var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); var tabVals = Enum.GetValues(typeof(ServerListTab)); tabButtons = new GUIButton[tabVals.Length]; foreach (ServerListTab tab in tabVals) { tabButtons[(int)tab] = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabButtonHolder.RectTransform), TextManager.Get("ServerListTab." + tab.ToString()), style: "GUITabButton") { OnClicked = (btn, usrdat) => { SelectedTab = tab; return false; } }; } var friendsButtonFrame = new GUIFrame(new RectTransform(new Vector2(0.31f, 2.0f), tabButtonHolder.RectTransform, Anchor.BottomRight), style: "InnerFrame") { IgnoreLayoutGroups = true }; friendsButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), friendsButtonFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopLeft) { RelativeSpacing = 0.01f, IsHorizontal = true }; friendsList = new List(); //------------------------------------------------------------------------------------- // Bottom row //------------------------------------------------------------------------------------- var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y), paddedFrame.RectTransform, Anchor.CenterRight)) { Stretch = true }; var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { OutlineColor = Color.Black }; GUILayoutGroup serverListContainer = null; GUIFrame filtersHolder = null; GUIButton filterToggle = null; void RecalculateHolder() { float listContainerSubtract = filtersHolder.Visible ? sidebarWidth : 0.0f; listContainerSubtract += serverPreview.Visible ? sidebarWidth : 0.0f; float toggleButtonsSubtract = 1.1f * filterToggle.Rect.Width / serverListHolder.Rect.Width; listContainerSubtract += filterToggle.Visible ? toggleButtonsSubtract : 0.0f; listContainerSubtract += serverPreviewToggleButton.Visible ? toggleButtonsSubtract : 0.0f; serverListContainer.RectTransform.RelativeSize = new Vector2(1.0f - listContainerSubtract, 1.0f); serverListHolder.Recalculate(); } // filters ------------------------------------------- filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black }; float elementHeight = 0.05f; var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUI.SubHeadingFont) { Padding = Vector4.Zero, AutoScaleHorizontal = true, CanBeFocused = false }; var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "..."); searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), ""); searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; }; var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft)) { ScrollBarVisible = true, Spacing = (int)(5 * GUI.Scale) }; filterToggle = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, style: "UIToggleButton") { OnClicked = (btn, userdata) => { filtersHolder.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); filtersHolder.Visible = !filtersHolder.Visible; filtersHolder.IgnoreLayoutGroups = !filtersHolder.Visible; RecalculateHolder(); btn.Children.ForEach(c => c.SpriteEffects = !filtersHolder.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; filterToggle.Children.ForEach(c => c.SpriteEffects = SpriteEffects.FlipHorizontally); List filterTextList = new List(); filterSameVersion = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterSameVersion")) { ToolTip = TextManager.Get("FilterSameVersion"), Selected = true, OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterSameVersion.TextBlock); filterPassword = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterPassword")) { ToolTip = TextManager.Get("FilterPassword"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterPassword.TextBlock); filterIncompatible = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterIncompatibleServers")) { ToolTip = TextManager.Get("FilterIncompatibleServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterIncompatible.TextBlock); filterFull = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterFullServers")) { ToolTip = TextManager.Get("FilterFullServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterFull.TextBlock); filterEmpty = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterEmptyServers")) { ToolTip = TextManager.Get("FilterEmptyServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterEmpty.TextBlock); filterWhitelisted = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("FilterWhitelistedServers")) { ToolTip = TextManager.Get("FilterWhitelistedServers"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterWhitelisted.TextBlock); // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUI.SubHeadingFont) { CanBeFocused = false }; filterKarma = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.karma.true")) { ToolTip = TextManager.Get("servertag.karma.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterKarma.TextBlock); filterTraitor = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.traitors.true")) { ToolTip = TextManager.Get("servertag.traitors.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterTraitor.TextBlock); filterFriendlyFire = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.friendlyfire.false")) { ToolTip = TextManager.Get("servertag.friendlyfire.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterFriendlyFire.TextBlock); filterVoip = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.voip.false")) { ToolTip = TextManager.Get("servertag.voip.false"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterVoip.TextBlock); filterModded = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag.modded.true")) { ToolTip = TextManager.Get("servertag.modded.true"), OnSelected = (tickBox) => { FilterServers(); return true; } }; filterTextList.Add(filterModded.TextBlock); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUI.SubHeadingFont) { CanBeFocused = false }; playStyleTickBoxes = new List(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), TextManager.Get("servertag." + playStyle)) { ToolTip = TextManager.Get("servertag." + playStyle), Selected = true, OnSelected = (tickBox) => { FilterServers(); return true; }, UserData = playStyle }; playStyleTickBoxes.Add(selectionTick); filterTextList.Add(selectionTick.TextBlock); } // Game mode Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUI.SubHeadingFont) { CanBeFocused = false }; gameModeTickBoxes = new List(); foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) continue; var selectionTick = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), mode.Name) { ToolTip = mode.Name, Selected = true, OnSelected = (tickBox) => { FilterServers(); return true; }, UserData = mode.Identifier }; gameModeTickBoxes.Add(selectionTick); filterTextList.Add(selectionTick.TextBlock); } filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); filterTextList.ForEach(t => t.Text = t.ToolTip); GUITextBlock.AutoScaleAndNormalize(filterTextList, defaultScale: 1.0f); if (filterTextList[0].TextScale < 0.8f) { filterTextList.ForEach(t => t.TextScale = 1.0f); filterTextList.ForEach(t => t.Text = ToolBox.LimitString(t.Text, t.Font, (int)(filters.Content.Rect.Width * 0.8f))); } }; // server list --------------------------------------------------------------------- serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true }; labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(0.99f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, isHorizontal: true, childAnchor: Anchor.BottomLeft) { Stretch = true }; for (int i = 0; i < columnRelativeWidth.Length; i++) { var btn = new GUIButton(new RectTransform(new Vector2(columnRelativeWidth[i], 1.0f), labelHolder.RectTransform), text: TextManager.Get(columnLabel[i]), textAlignment: Alignment.Center, style: "GUIButtonSmall") { ToolTip = TextManager.Get(columnLabel[i]), ForceUpperCase = true, UserData = columnLabel[i], OnClicked = SortList }; btn.Color *= 0.5f; labelTexts.Add(btn.TextBlock); new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) { CanBeFocused = false, UserData = "arrowup", Visible = false }; new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) { CanBeFocused = false, UserData = "arrowdown", SpriteEffects = SpriteEffects.FlipVertically, Visible = false }; } serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) { ScrollBarVisible = true, OnSelected = (btn, obj) => { if (obj is ServerInfo serverInfo) { joinButton.Enabled = true; selectedServer = serverInfo; if (!serverPreview.Visible) { serverPreview.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); serverPreviewToggleButton.Visible = true; serverPreviewToggleButton.IgnoreLayoutGroups = false; serverPreview.Visible = true; serverPreview.IgnoreLayoutGroups = false; RecalculateHolder(); } serverInfo.CreatePreviewWindow(serverPreview); btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); } return true; } }; //server preview panel -------------------------------------------------- serverPreviewToggleButton = new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), serverListHolder.RectTransform) { MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) }, style: "UIToggleButton") { Visible = false, OnClicked = (btn, userdata) => { serverPreview.RectTransform.RelativeSize = new Vector2(0.2f, 1.0f); serverPreview.Visible = !serverPreview.Visible; serverPreview.IgnoreLayoutGroups = !serverPreview.Visible; RecalculateHolder(); btn.Children.ForEach(c => c.SpriteEffects = serverPreview.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; serverPreview = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null) { Color = new Color(12, 14, 15, 255) * 0.5f, OutlineColor = Color.Black, IgnoreLayoutGroups = true, Visible = false }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null); var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("Back")) { OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu }; new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("ServerListRefresh")) { OnClicked = (btn, userdata) => { RefreshServers(); return true; } }; var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("serverlistdirectjoin")) { OnClicked = (btn, userdata) => { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { ClientNameBox.Flash(); ClientNameBox.Select(); GUI.PlayUISound(GUISoundType.PickItemFail); return false; } ShowDirectJoinPrompt(); return true; } }; joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform), TextManager.Get("ServerListJoin")) { OnClicked = (btn, userdata) => { if (selectedServer != null) { if (!string.IsNullOrWhiteSpace(selectedServer.IP) && !string.IsNullOrWhiteSpace(selectedServer.Port) && int.TryParse(selectedServer.Port, out _)) { JoinServer(selectedServer.IP + ":" + selectedServer.Port, selectedServer.ServerName); } else if (selectedServer.LobbyID != 0) { Steam.SteamManager.JoinLobby(selectedServer.LobbyID, true); } else { new GUIMessageBox("", TextManager.Get("ServerOffline")); return false; } } return true; }, Enabled = false }; buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f)); //-------------------------------------------------------- bottomRow.Recalculate(); serverListHolder.Recalculate(); serverListContainer.Recalculate(); labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); labelHolder.Recalculate(); serverList.Content.RectTransform.SizeChanged += () => { labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); labelHolder.Recalculate(); foreach (GUITextBlock labelText in labelTexts) { labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); } RecalculateHolder(); }; button.SelectedColor = button.Color; refreshDisableTimer = DateTime.Now; //recent and favorite servers ReadServerMemFromFile(recentServersFile, ref recentServers); ReadServerMemFromFile(favoriteServersFile, ref favoriteServers); recentServers.ForEach(s => s.Recent = true); favoriteServers.ForEach(s => s.Favorite = true); SelectedTab = ServerListTab.All; tabButtons[(int)selectedTab].Selected = true; RecalculateHolder(); } private static void LoadPlayStyleBanners() { //playstyle banners playStyleBanners = new Sprite[Enum.GetValues(typeof(PlayStyle)).Length]; PlayStyleColors = new Color[Enum.GetValues(typeof(PlayStyle)).Length]; PlayStyleIcons = new Dictionary(); PlayStyleIconColors = new Dictionary(); XDocument playStylesDoc = XMLExtensions.TryLoadXml("Content/UI/Server/PlayStyles.xml"); XElement rootElement = playStylesDoc.Root; foreach (var element in rootElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "playstylebanner": if (Enum.TryParse(element.GetAttributeString("identifier", ""), out PlayStyle playStyle)) { PlayStyleBanners[(int)playStyle] = new Sprite(element, lazyLoad: true); PlayStyleColors[(int)playStyle] = element.GetAttributeColor("color", Color.White); } break; case "playstyleicon": string identifier = element.GetAttributeString("identifier", ""); if (string.IsNullOrEmpty(identifier)) { continue; } PlayStyleIcons[identifier] = new Sprite(element, lazyLoad: true); PlayStyleIconColors[identifier] = element.GetAttributeColor("color", Color.White); break; } } } private void ReadServerMemFromFile(string file, ref List servers) { if (servers == null) { servers = new List(); } if (!File.Exists(file)) { return; } XDocument doc = XMLExtensions.TryLoadXml(file); if (doc == null) { DebugConsole.NewMessage("Failed to load file \"" + file + "\". Attempting to recreate the file..."); try { doc = new XDocument(new XElement("servers")); doc.Save(file); DebugConsole.NewMessage("Recreated \"" + file + "\"."); } catch (Exception e) { DebugConsole.ThrowError("Failed to recreate the file \"" + file + "\".", e); } return; } foreach (XElement element in doc.Root.Elements()) { if (element.Name != "ServerInfo") { continue; } servers.Add(ServerInfo.FromXElement(element)); } } private void WriteServerMemToFile(string file, List servers) { if (servers == null) { return; } XDocument doc = new XDocument(); XElement rootElement = new XElement("servers"); doc.Add(rootElement); foreach (ServerInfo info in servers) { rootElement.Add(info.ToXElement()); } doc.SaveSafe(file); } public ServerInfo UpdateServerInfoWithServerSettings(object endpoint, ServerSettings serverSettings) { UInt64 steamId = 0; string ip = ""; string port = ""; if (endpoint is UInt64 id) { steamId = id; } else if (endpoint is string strEndpoint) { string[] address = strEndpoint.Split(':'); if (address.Length == 1) { ip = strEndpoint; port = NetConfig.DefaultPort.ToString(); } else { ip = string.Join(":", address.Take(address.Length - 1)); port = address[address.Length - 1]; } } bool isInfoNew = false; ServerInfo info = serverList.Content.FindChild(d => (d.UserData is ServerInfo serverInfo) && serverInfo != null && (steamId != 0 ? steamId == serverInfo.OwnerID : (ip == serverInfo.IP && port == serverInfo.Port)))?.UserData as ServerInfo; if (info == null) { isInfoNew = true; info = new ServerInfo(); } info.ServerName = serverSettings.ServerName; info.ServerMessage = serverSettings.ServerMessageText; info.OwnerID = steamId; info.LobbyID = SteamManager.CurrentLobbyID; info.IP = ip; info.Port = port; info.GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? ""; info.GameStarted = Screen.Selected != GameMain.NetLobbyScreen; info.GameVersion = GameMain.Version.ToString(); info.MaxPlayers = serverSettings.MaxPlayers; info.PlayStyle = PlayStyle.SomethingDifferent; info.RespondedToSteamQuery = true; info.UsingWhiteList = serverSettings.Whitelist.Enabled; info.TraitorsEnabled = serverSettings.TraitorsEnabled; info.SubSelectionMode = serverSettings.SubSelectionMode; info.ModeSelectionMode = serverSettings.ModeSelectionMode; info.VoipEnabled = serverSettings.VoiceChatEnabled; info.FriendlyFireEnabled = serverSettings.AllowFriendlyFire; info.KarmaEnabled = serverSettings.KarmaEnabled; info.PlayerCount = GameMain.Client.ConnectedClients.Count; info.PingChecked = false; info.HasPassword = serverSettings.HasPassword; info.OwnerVerified = true; if (isInfoNew) { AddToServerList(info); } return info; } public void AddToRecentServers(ServerInfo info) { if (!string.IsNullOrEmpty(info.IP)) { //don't add localhost to recent servers if (IPAddress.TryParse(info.IP, out IPAddress ip) && IPAddress.IsLoopback(ip)) { return; } } info.Recent = true; ServerInfo existingInfo = recentServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (existingInfo == null) { recentServers.Add(info); } else { int index = recentServers.IndexOf(existingInfo); recentServers[index] = info; } WriteServerMemToFile(recentServersFile, recentServers); } public void AddToFavoriteServers(ServerInfo info) { info.Favorite = true; ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (existingInfo == null) { favoriteServers.Add(info); } else { int index = favoriteServers.IndexOf(existingInfo); favoriteServers[index] = info; } WriteServerMemToFile(favoriteServersFile, favoriteServers); } public void RemoveFromFavoriteServers(ServerInfo info) { info.Favorite = false; ServerInfo existingInfo = favoriteServers.Find(serverInfo => info.OwnerID == serverInfo.OwnerID && (info.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (existingInfo != null) { favoriteServers.Remove(existingInfo); WriteServerMemToFile(favoriteServersFile, favoriteServers); } } private bool SortList(GUIButton button, object obj) { if (!(obj is string sortBy)) { return false; } SortList(sortBy, toggle: true); return true; } private void SortList(string sortBy, bool toggle) { GUIButton button = labelHolder.GetChildByUserData(sortBy) as GUIButton; if (button == null) { return; } sortedBy = sortBy; var arrowUp = button.GetChildByUserData("arrowup"); var arrowDown = button.GetChildByUserData("arrowdown"); //disable arrow buttons in other labels foreach (var child in button.Parent.Children) { if (child != button) { child.GetChildByUserData("arrowup").Visible = false; child.GetChildByUserData("arrowdown").Visible = false; } } bool ascending = arrowUp.Visible; if (toggle) { ascending = !ascending; } arrowUp.Visible = ascending; arrowDown.Visible = !ascending; serverList.Content.RectTransform.SortChildren((c1, c2) => { ServerInfo s1 = c1.GUIComponent.UserData as ServerInfo; ServerInfo s2 = c2.GUIComponent.UserData as ServerInfo; if (s1 == null && s2 == null) { return 0; } else if (s1 == null) { return ascending ? 1 : -1; } else if (s2 == null) { return ascending ? -1 : 1; } switch (sortBy) { case "ServerListCompatible": bool? s1Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s1.GameVersion); if (!s1.ContentPackageHashes.Any()) { s1Compatible = null; } if (s1Compatible.HasValue) { s1Compatible = s1Compatible.Value && s1.ContentPackagesMatch(GameMain.SelectedPackages); }; bool? s2Compatible = NetworkMember.IsCompatible(GameMain.Version.ToString(), s2.GameVersion); if (!s2.ContentPackageHashes.Any()) { s2Compatible = null; } if (s2Compatible.HasValue) { s2Compatible = s2Compatible.Value && s2.ContentPackagesMatch(GameMain.SelectedPackages); }; //convert to int to make sorting easier //1 Compatible //0 Unknown //-1 Incompatible int s1CompatibleInt = s1Compatible.HasValue ? (s1Compatible.Value ? 1 : -1) : 0; int s2CompatibleInt = s2Compatible.HasValue ? (s2Compatible.Value ? 1 : -1) : 0; return s2CompatibleInt.CompareTo(s1CompatibleInt) * (ascending ? 1 : -1); case "ServerListHasPassword": if (s1.HasPassword == s2.HasPassword) { return 0; } return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); case "ServerListName": return string.Compare(s1.ServerName, s2.ServerName) * (ascending ? 1 : -1); case "ServerListRoundStarted": if (s1.GameStarted == s2.GameStarted) { return 0; } return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); case "ServerListPlayers": return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); case "ServerListPing": return s2.Ping.CompareTo(s1.Ping) * (ascending ? 1 : -1); default: return 0; } }); } public override void Select() { base.Select(); SelectedTab = ServerListTab.All; Steamworks.SteamMatchmaking.ResetActions(); if (GameMain.Client != null) { GameMain.Client.Disconnect(); GameMain.Client = null; } RefreshServers(); } public override void Deselect() { base.Deselect(); } public override void Update(double deltaTime) { base.Update(deltaTime); UpdateFriendsList(); if (PlayerInput.PrimaryMouseButtonClicked()) { friendPopup = null; if (friendsDropdown != null && friendsDropdownButton != null && !friendsDropdown.Rect.Contains(PlayerInput.MousePosition) && !friendsDropdownButton.Rect.Contains(PlayerInput.MousePosition)) { friendsDropdown.Visible = false; } } } private void FilterServers() { serverList.Content.RemoveChild(serverList.Content.FindChild("noresults")); foreach (GUIComponent child in serverList.Content.Children) { if (!(child.UserData is ServerInfo)) continue; ServerInfo serverInfo = (ServerInfo)child.UserData; Version remoteVersion = null; if (!string.IsNullOrEmpty(serverInfo.GameVersion)) { Version.TryParse(serverInfo.GameVersion, out remoteVersion); } //never show newer versions //(ignore revision number, it doesn't affect compatibility) if (remoteVersion != null && (remoteVersion.Major > GameMain.Version.Major || remoteVersion.Minor > GameMain.Version.Minor || remoteVersion.Build > GameMain.Version.Build)) { child.Visible = false; } else { bool incompatible = (!serverInfo.ContentPackageHashes.Any() && serverInfo.ContentPackagesMatch(GameMain.Config.SelectedContentPackages)) || (remoteVersion != null && !NetworkMember.IsCompatible(GameMain.Version, remoteVersion)); child.Visible = serverInfo.OwnerVerified && serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase) && (!filterSameVersion.Selected || (remoteVersion != null && NetworkMember.IsCompatible(remoteVersion, GameMain.Version))) && (!filterPassword.Selected || !serverInfo.HasPassword) && (!filterIncompatible.Selected || !incompatible) && (!filterFull.Selected || serverInfo.PlayerCount < serverInfo.MaxPlayers) && (!filterEmpty.Selected || serverInfo.PlayerCount > 0) && (!filterWhitelisted.Selected || serverInfo.UsingWhiteList == true) && (!filterKarma.Selected || serverInfo.KarmaEnabled == true) && (!filterFriendlyFire.Selected || serverInfo.FriendlyFireEnabled == false) && (!filterTraitor.Selected || serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) && (!filterVoip.Selected || serverInfo.VoipEnabled == false) && (!filterModded.Selected || serverInfo.GetPlayStyleTags().Any(t => t.Contains("modded.true"))) && ((selectedTab == ServerListTab.All && (serverInfo.LobbyID != 0 || !string.IsNullOrWhiteSpace(serverInfo.Port))) || (selectedTab == ServerListTab.Recent && serverInfo.Recent) || (selectedTab == ServerListTab.Favorites && serverInfo.Favorite)); } foreach (GUITickBox tickBox in playStyleTickBoxes) { var playStyle = (PlayStyle)tickBox.UserData; if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) { child.Visible = false; break; } } foreach (GUITickBox tickBox in gameModeTickBoxes) { var gameMode = (string)tickBox.UserData; if (!tickBox.Selected && serverInfo.GameMode != null && serverInfo.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase)) { child.Visible = false; break; } } } if (serverList.Content.Children.All(c => !c.Visible)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), serverList.Content.RectTransform), TextManager.Get("NoMatchingServers")) { UserData = "noresults" }; } serverList.UpdateScrollBarSize(); } private void ShowDirectJoinPrompt() { var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", new string[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") }, relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150)); msgBox.Content.ChildAnchor = Anchor.TopCenter; var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter) { IgnoreLayoutGroups = false, Stretch = true, RelativeSpacing = 0.05f }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform), TextManager.Get("ServerEndpoint"), textAlignment: Alignment.Center); var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform)); content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height))); content.RectTransform.IsFixedSize = true; msgBox.InnerFrame.RectTransform.MinSize = new Point(0, (int)((content.RectTransform.NonScaledSize.Y + msgBox.Content.RectTransform.Children.Sum(c => c.NonScaledSize.Y + msgBox.Content.AbsoluteSpacing)) * 1.1f)); var okButton = msgBox.Buttons[0]; okButton.Enabled = false; okButton.OnClicked = (btn, userdata) => { JoinServer(endpointBox.Text, ""); msgBox.Close(); return true; }; var favoriteButton = msgBox.Buttons[1]; favoriteButton.Enabled = false; favoriteButton.OnClicked = (button, userdata) => { UInt64 steamId = SteamManager.SteamIDStringToUInt64(endpointBox.Text); string ip = ""; int port = 0; if (steamId == 0) { string hostIP = endpointBox.Text; string[] address = hostIP.Split(':'); if (address.Length == 1) { ip = hostIP; port = NetConfig.DefaultPort; } else { ip = string.Join(":", address.Take(address.Length - 1)); if (!int.TryParse(address[address.Length - 1], out port)) { DebugConsole.ThrowError("Invalid port: " + address[address.Length - 1] + "!"); port = NetConfig.DefaultPort; } } } //TODO: add a better way to get the query port, right now we're just assuming that it'll always be the default ServerInfo serverInfo = new ServerInfo() { ServerName = "Server", OwnerID = steamId, IP = ip, Port = port.ToString(), QueryPort = NetConfig.DefaultQueryPort.ToString(), GameVersion = GameMain.Version.ToString(), PlayStyle = PlayStyle.Serious }; var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && info.OwnerID == serverInfo.OwnerID && (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (serverFrame != null) { serverInfo = serverFrame.UserData as ServerInfo; } else { AddToServerList(serverInfo); } AddToFavoriteServers(serverInfo); SelectedTab = ServerListTab.Favorites; FilterServers(); serverInfo.QueryLiveInfo(UpdateServerInfo); msgBox.Close(); return false; }; var cancelButton = msgBox.Buttons[2]; cancelButton.OnClicked = msgBox.Close; endpointBox.OnTextChanged += (textBox, text) => { okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text); return true; }; } private bool JoinFriend(GUIButton button, object userdata) { FriendInfo info = userdata as FriendInfo; if (info.InServer) { if (info.ConnectLobby != 0) { GameMain.Instance.ConnectLobby = info.ConnectLobby; GameMain.Instance.ConnectEndpoint = null; GameMain.Instance.ConnectName = null; } else { GameMain.Instance.ConnectLobby = 0; GameMain.Instance.ConnectEndpoint = info.ConnectEndpoint; GameMain.Instance.ConnectName = info.ConnectName; } } return false; } private bool OpenFriendPopup(GUIButton button, object userdata) { FriendInfo info = userdata as FriendInfo; if (info.InServer) { friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform), info.ConnectName ?? "[Unnamed]"); var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.TopRight), TextManager.Get("ServerListJoin")) { UserData = info }; joinButton.OnClicked = JoinFriend; Vector2 frameDims = joinButton.Font.MeasureString(info.ConnectName ?? "[Unnamed]"); frameDims.X /= 0.6f; frameDims.Y *= 1.5f; friendPopup.RectTransform.NonScaledSize = frameDims.ToPoint(); friendPopup.RectTransform.RelativeOffset = Vector2.Zero; friendPopup.RectTransform.AbsoluteOffset = PlayerInput.MousePosition.ToPoint(); friendPopup.RectTransform.RecalculateChildren(true); friendPopup.RectTransform.SetPosition(Anchor.TopLeft); } return false; } private enum AvatarSize { Small, Medium, Large } private void UpdateFriendsList() { if (!SteamManager.IsInitialized) { return; } if (friendsListUpdateTime > Timing.TotalTime) { return; } friendsListUpdateTime = Timing.TotalTime + 5.0; float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; if (friendsDropdown == null) { friendsDropdown = new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) { OutlineColor = Color.Black, Visible = false }; } friendsDropdown.ClearChildren(); AvatarSize avatarSize = AvatarSize.Large; if (friendsButtonHolder.RectTransform.Rect.Height <= 24) { avatarSize = AvatarSize.Small; } else if (friendsButtonHolder.RectTransform.Rect.Height <= 48) { avatarSize = AvatarSize.Medium; } List friends = Steamworks.SteamFriends.GetFriends().ToList(); for (int i = friendsList.Count - 1; i >= 0; i--) { var friend = friendsList[i]; if (!friends.Any(g => g.Id == friend.SteamID && g.IsOnline)) { friend.Sprite?.Remove(); friendsList.RemoveAt(i); } } foreach (var friend in friends) { if (!friend.IsOnline) { continue; } FriendInfo info = friendsList.Find(f => f.SteamID == friend.Id); if (info == null) { info = new FriendInfo() { SteamID = friend.Id }; friendsList.Insert(0, info); } if (info.Sprite == null) { Func> avatarFunc = null; switch (avatarSize) { case AvatarSize.Small: avatarFunc = Steamworks.SteamFriends.GetSmallAvatarAsync; break; case AvatarSize.Medium: avatarFunc = Steamworks.SteamFriends.GetMediumAvatarAsync; break; case AvatarSize.Large: avatarFunc = Steamworks.SteamFriends.GetLargeAvatarAsync; break; } TaskPool.Add(avatarFunc(friend.Id), (Task task) => { if (!task.Result.HasValue) { return; } var avatarImage = task.Result.Value; const int desaturatedWeight = 180; byte[] avatarData = (byte[])avatarImage.Data.Clone(); for (int i = 0; i < avatarData.Length; i += 4) { int luma = (avatarData[i + 0] * 299 + avatarData[i + 1] * 587 + avatarData[i + 2] * 114) / 1000; luma = (int)(luma * 0.7f + ((luma / 100.0f) * (luma / 255.0f) * 255.0f * 0.3f)); int chn0 = ((avatarData[i + 0] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); int chn1 = ((avatarData[i + 1] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); int chn2 = ((avatarData[i + 2] * (255 - desaturatedWeight)) / 255) + ((luma * desaturatedWeight) / 255); int chn3 = 255; chn0 = chn0 * chn3 / 255; chn1 = chn1 * chn3 / 255; chn2 = chn2 * chn3 / 255; avatarData[i + 0] = chn0 > 255 ? (byte)255 : (byte)chn0; avatarData[i + 1] = chn1 > 255 ? (byte)255 : (byte)chn1; avatarData[i + 2] = chn2 > 255 ? (byte)255 : (byte)chn2; avatarData[i + 3] = chn3 > 255 ? (byte)255 : (byte)chn3; } CrossThread.RequestExecutionOnMainThread(() => { //TODO: create an avatar atlas? var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height); avatarTexture.SetData(avatarData); info.Sprite = new Sprite(avatarTexture, null, null); }); }); } info.Name = friend.Name; info.ConnectName = null; info.ConnectEndpoint = null; info.ConnectLobby = 0; info.PlayingThisGame = friend.IsPlayingThisGame; info.PlayingAnotherGame = friend.GameInfo.HasValue; if (friend.IsPlayingThisGame) { info.StatusText = friend.GetRichPresence("status") ?? ""; string connectCommand = friend.GetRichPresence("connect") ?? ""; try { ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCommand), out info.ConnectName, out info.ConnectEndpoint, out info.ConnectLobby); } catch (IndexOutOfRangeException e) { #if DEBUG DebugConsole.ThrowError($"Failed to parse a Steam friend's connect command ({connectCommand})", e); #else DebugConsole.Log($"Failed to parse a Steam friend's connect command ({connectCommand})\n" + e.StackTrace); #endif info.ConnectName = null; info.ConnectEndpoint = null; info.ConnectLobby = 0; } } else { info.StatusText = TextManager.Get(info.PlayingAnotherGame ? "FriendPlayingAnotherGame" : "FriendNotPlaying"); } } friendsList.Sort((a, b) => { if (a.InServer && !b.InServer) { return -1; } if (b.InServer && !a.InServer) { return 1; } if (a.PlayingThisGame && !b.PlayingThisGame) { return -1; } if (b.PlayingThisGame && !a.PlayingThisGame) { return 1; } return 0; }); friendsButtonHolder.ClearChildren(); if (friendsList.Count > 0) { friendsDropdownButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, Anchor.BottomRight, Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight), "\u2022 \u2022 \u2022", style: "GUIButtonFriendsDropdown") { Font = GUI.GlobalFont, OnClicked = (button, udt) => { friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); friendsDropdown.RectTransform.RecalculateChildren(true); friendsDropdown.Visible = !friendsDropdown.Visible; return false; } }; } else { friendsDropdownButton = null; friendsDropdown.Visible = false; } int buttonCount = 0; for (int i = 0; i < friendsList.Count; i++) { var friend = friendsList[i]; buttonCount++; if (buttonCount <= 5) { string style = "GUIButtonFriendNotPlaying"; if (friend.InServer) { style = "GUIButtonFriendPlaying"; } else { style = friend.PlayingThisGame ? "GUIButtonFriendPlaying" : "GUIButtonFriendNotPlaying"; } var guiButton = new GUIButton(new RectTransform(Vector2.One, friendsButtonHolder.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: style) { UserData = friend, OnClicked = OpenFriendPopup }; guiButton.ToolTip = friend.Name + "\n" + friend.StatusText; if (friend.Sprite != null) { static Color BrightenColor(Color color) { Vector3 hls = ToolBox.RgbToHLS(color); hls.Y = hls.Y * 0.3f + 0.7f; hls.Z = hls.Z * 0.6f + 0.4f; return ToolBox.HLSToRGB(hls); } var imgColor = BrightenColor(guiButton.Color); var imgHoverColor = BrightenColor(guiButton.HoverColor); var imgSelectColor = BrightenColor(guiButton.SelectedColor); var imgPressColor = BrightenColor(guiButton.PressedColor); var guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) { Color = imgColor, HoverColor = imgHoverColor, SelectedColor = imgSelectColor, PressedColor = imgPressColor, CanBeFocused = false }; guiImage = new GUIImage(new RectTransform(Vector2.One * 0.925f, guiButton.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.025f, 0.025f) }, friend.Sprite, null, true) { Color = Color.White * 0.8f, HoverColor = Color.White * 0.8f, SelectedColor = Color.White * 0.8f, PressedColor = Color.White * 0.8f, BlendState = BlendState.Additive, CanBeFocused = false }; } } var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); var guiImage2TheSequel = new GUIImage(new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) } , friend.Sprite, null, true); var textBlock = new GUITextBlock(new RectTransform(Vector2.One * 0.8f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(1.0f / 7.7f, 0.0f) }, friend.Name + "\n" + friend.StatusText) { Font = GUI.SmallFont }; if (friend.PlayingThisGame) { textBlock.TextColor = GUI.Style.Green; } if (friend.PlayingAnotherGame) { textBlock.TextColor = GUI.Style.Blue; } if (friend.InServer) { var joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.6f), friendFrame.RectTransform, Anchor.CenterRight) { RelativeOffset = new Vector2(0.05f, 0.0f) }, TextManager.Get("ServerListJoin"), style: "GUIButtonJoinFriend") { UserData = friend }; joinButton.OnClicked = JoinFriend; } } friendsDropdown.RectTransform.NonScaledSize = new Point(friendsButtonHolder.Rect.Height * 5 * 166 / 100, friendsButtonHolder.Rect.Height * 4 * 166 / 100); friendsDropdown.RectTransform.AbsoluteOffset = new Point(friendsButtonHolder.Rect.X, friendsButtonHolder.Rect.Bottom); friendsDropdown.RectTransform.RecalculateChildren(true); friendsDropdown.ScrollBar.BarScrollValue = prevDropdownScroll; } private void RefreshServers() { if (waitingForRefresh) { return; } steamPingInfoReady = false; CoroutineManager.StopCoroutines("EstimateLobbyPing"); TaskPool.Add(Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), (task) => { steamPingInfoReady = true; }); friendsListUpdateTime = Timing.TotalTime - 1.0; UpdateFriendsList(); serverList.ClearChildren(); serverPreview.ClearChildren(); joinButton.Enabled = false; selectedServer = null; new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), TextManager.Get("RefreshingServerList"), textAlignment: Alignment.Center) { CanBeFocused = false }; CoroutineManager.StartCoroutine(WaitForRefresh()); } private IEnumerable WaitForRefresh() { waitingForRefresh = true; if (refreshDisableTimer > DateTime.Now) { yield return new WaitForSeconds((float)(refreshDisableTimer - DateTime.Now).TotalSeconds); } recentServers.Concat(favoriteServers).ForEach(si => si.OwnerVerified = false); if (GameMain.Config.UseSteamMatchmaking) { serverList.ClearChildren(); if (!SteamManager.GetServers(AddToServerList, ServerQueryFinished)) { serverList.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), TextManager.Get("ServerListNoSteamConnection"), textAlignment: Alignment.Center) { CanBeFocused = false }; } else { List knownServers = recentServers.Concat(favoriteServers).ToList(); foreach (ServerInfo info in knownServers) { AddToServerList(info); info.QueryLiveInfo(UpdateServerInfo); } } } else { CoroutineManager.StartCoroutine(SendMasterServerRequest()); waitingForRefresh = false; } refreshDisableTimer = DateTime.Now + AllowedRefreshInterval; yield return CoroutineStatus.Success; } private void UpdateServerList(string masterServerData) { serverList.ClearChildren(); if (masterServerData.Substring(0, 5).Equals("error", StringComparison.OrdinalIgnoreCase)) { DebugConsole.ThrowError("Error while connecting to master server (" + masterServerData + ")!"); return; } string[] lines = masterServerData.Split('\n'); List serverInfos = new List(); for (int i = 0; i < lines.Length; i++) { string[] arguments = lines[i].Split('|'); if (arguments.Length < 3) continue; string ip = arguments[0]; string port = arguments[1]; string serverName = arguments[2]; bool gameStarted = arguments.Length > 3 && arguments[3] == "1"; string currPlayersStr = arguments.Length > 4 ? arguments[4] : ""; string maxPlayersStr = arguments.Length > 5 ? arguments[5] : ""; bool hasPassWord = arguments.Length > 6 && arguments[6] == "1"; string gameVersion = arguments.Length > 7 ? arguments[7] : ""; string contentPackageNames = arguments.Length > 8 ? arguments[8] : ""; string contentPackageHashes = arguments.Length > 9 ? arguments[9] : ""; int.TryParse(currPlayersStr, out int playerCount); int.TryParse(maxPlayersStr, out int maxPlayers); var serverInfo = new ServerInfo() { IP = ip, Port = port, ServerName = serverName, GameStarted = gameStarted, PlayerCount = playerCount, MaxPlayers = maxPlayers, HasPassword = hasPassWord, GameVersion = gameVersion, OwnerVerified = true }; foreach (string contentPackageName in contentPackageNames.Split(',')) { if (string.IsNullOrEmpty(contentPackageName)) continue; serverInfo.ContentPackageNames.Add(contentPackageName); } foreach (string contentPackageHash in contentPackageHashes.Split(',')) { if (string.IsNullOrEmpty(contentPackageHash)) continue; serverInfo.ContentPackageHashes.Add(contentPackageHash); } serverInfos.Add(serverInfo); } serverList.Content.ClearChildren(); if (serverInfos.Count() == 0) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), serverList.Content.RectTransform), TextManager.Get("NoServers"), textAlignment: Alignment.Center) { CanBeFocused = false }; return; } foreach (ServerInfo serverInfo in serverInfos) { AddToServerList(serverInfo); } } private void AddToServerList(ServerInfo serverInfo) { var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && (info.LobbyID == serverInfo.LobbyID || (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && serverInfo.OwnerVerified)) && (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (serverFrame == null) { serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) }, style: "ListBoxElement") { UserData = serverInfo }; new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, //RelativeSpacing = 0.02f }; } else { int index = recentServers.IndexOf(serverFrame.UserData as ServerInfo); if (index >= 0) { recentServers[index] = serverInfo; serverInfo.Recent = true; } index = favoriteServers.IndexOf(serverFrame.UserData as ServerInfo); if (index >= 0) { favoriteServers[index] = serverInfo; serverInfo.Favorite = true; } } serverFrame.UserData = serverInfo; if (serverInfo.OwnerVerified) { var childrenToRemove = serverList.Content.FindChildren(c => (c.UserData is ServerInfo info) && info != serverInfo && (serverInfo.OwnerID != 0 ? info.OwnerID == serverInfo.OwnerID : info.IP == serverInfo.IP)).ToList(); foreach (var child in childrenToRemove) { serverList.Content.RemoveChild(child); } } UpdateServerInfo(serverInfo); SortList(sortedBy, toggle: false); FilterServers(); } private void UpdateServerInfo(ServerInfo serverInfo) { var serverFrame = serverList.Content.FindChild(d => (d.UserData is ServerInfo info) && (info.LobbyID == serverInfo.LobbyID || (info.LobbyID == 0 && info.OwnerID == serverInfo.OwnerID && serverInfo.OwnerVerified)) && (serverInfo.OwnerID != 0 ? true : (info.IP == serverInfo.IP && info.Port == serverInfo.Port))); if (serverFrame == null) return; var serverContent = serverFrame.Children.First() as GUILayoutGroup; serverContent.ClearChildren(); var compatibleBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[0], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") { CanBeFocused = false, Selected = (NetworkMember.IsCompatible(GameMain.Version.ToString(), serverInfo.GameVersion) ?? true) && serverInfo.ContentPackagesMatch(GameMain.SelectedPackages), UserData = "compatible" }; var passwordBox = new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[1], 0.5f), serverContent.RectTransform, Anchor.Center), label: "", style: "GUIServerListPasswordTickBox") { ToolTip = TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword"), Selected = serverInfo.HasPassword, CanBeFocused = false, UserData = "password" }; var serverName = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[2] * 1.1f, 1.0f), serverContent.RectTransform), #if !DEBUG serverInfo.ServerName, #else ((serverInfo.OwnerID != 0 || serverInfo.LobbyID != 0) ? "[STEAMP2P] " : "[LIDGREN] ") + serverInfo.ServerName, #endif style: "GUIServerListTextBox"); serverName.UserData = serverName.Text; serverName.RectTransform.SizeChanged += () => { serverName.Text = ToolBox.LimitString(serverName.Text, serverName.Font, serverName.Rect.Width); }; if (serverInfo.ContentPackageNames.Any()) { if (serverInfo.ContentPackageNames.Any(cp => !cp.Equals(GameMain.VanillaContent.Name, StringComparison.OrdinalIgnoreCase))) { serverName.TextColor = new Color(219, 125, 217); } } new GUITickBox(new RectTransform(new Vector2(columnRelativeWidth[3], 0.9f), serverContent.RectTransform, Anchor.Center), label: "") { ToolTip = TextManager.Get((serverInfo.GameStarted) ? "ServerListRoundStarted" : "ServerListRoundNotStarted"), Selected = serverInfo.GameStarted, CanBeFocused = false }; var serverPlayers = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[4], 1.0f), serverContent.RectTransform), serverInfo.PlayerCount + "/" + serverInfo.MaxPlayers, style: "GUIServerListTextBox", textAlignment: Alignment.Right) { ToolTip = TextManager.Get("ServerListPlayers") }; var serverPingText = new GUITextBlock(new RectTransform(new Vector2(columnRelativeWidth[5], 1.0f), serverContent.RectTransform), "?", style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) { ToolTip = TextManager.Get("ServerListPing") }; if (serverInfo.PingChecked) { serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); } else if (!string.IsNullOrEmpty(serverInfo.IP)) { try { GetServerPing(serverInfo, serverPingText); } catch (NullReferenceException ex) { DebugConsole.ThrowError("Ping is null", ex); } } else if (serverInfo.PingLocation != null) { CoroutineManager.StartCoroutine(EstimateLobbyPing(serverInfo, serverPingText), "EstimateLobbyPing"); } if (serverInfo.LobbyID == 0 && (string.IsNullOrWhiteSpace(serverInfo.IP) || string.IsNullOrWhiteSpace(serverInfo.Port))) { string toolTip = TextManager.Get("ServerOffline"); serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } else if (GameMain.Config.UseSteamMatchmaking && serverInfo.RespondedToSteamQuery.HasValue && serverInfo.RespondedToSteamQuery.Value == false) { string toolTip = TextManager.Get("ServerListNoSteamQueryResponse"); compatibleBox.Selected = false; serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } else if (string.IsNullOrEmpty(serverInfo.GameVersion) || !serverInfo.ContentPackageHashes.Any()) { compatibleBox.Selected = false; new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUI.Style.Orange * 0.85f, textAlignment: Alignment.Center) { ToolTip = TextManager.Get(string.IsNullOrEmpty(serverInfo.GameVersion) ? "ServerListUnknownVersion" : "ServerListUnknownContentPackage") }; } else if (!compatibleBox.Selected) { string toolTip = ""; if (serverInfo.GameVersion != GameMain.Version.ToString()) toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion); for (int i = 0; i < serverInfo.ContentPackageNames.Count; i++) { if (!GameMain.SelectedPackages.Any(cp => cp.MD5hash.Hash == serverInfo.ContentPackageHashes[i])) { if (toolTip != "") toolTip += "\n"; toolTip += TextManager.GetWithVariables("ServerListIncompatibleContentPackage", new string[2] { "[contentpackage]", "[hash]" }, new string[2] { serverInfo.ContentPackageNames[i], Md5Hash.GetShortHash(serverInfo.ContentPackageHashes[i]) }); } } serverContent.Children.ForEach(c => c.ToolTip = toolTip); serverName.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f; } serverContent.Recalculate(); if (serverInfo.Favorite) { AddToFavoriteServers(serverInfo); } SortList(sortedBy, toggle: false); FilterServers(); } private IEnumerable EstimateLobbyPing(ServerInfo serverInfo, GUITextBlock serverPingText) { while (!steamPingInfoReady) { yield return CoroutineStatus.Running; } Steamworks.Data.PingLocation pingLocation = serverInfo.PingLocation.Value; serverInfo.Ping = Steamworks.SteamNetworkingUtils.LocalPingLocation?.EstimatePingTo(pingLocation) ?? -1; serverInfo.PingChecked = true; serverPingText.TextColor = GetPingTextColor(serverInfo.Ping); serverPingText.Text = serverInfo.Ping > -1 ? serverInfo.Ping.ToString() : "?"; yield return CoroutineStatus.Success; } private void ServerQueryFinished() { if (!serverList.Content.Children.Any()) { new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), TextManager.Get("NoServers"), textAlignment: Alignment.Center) { CanBeFocused = false }; } else if (serverList.Content.Children.All(c => !c.Visible)) { new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), TextManager.Get("NoMatchingServers"), textAlignment: Alignment.Center) { CanBeFocused = false, UserData = "noresults" }; } waitingForRefresh = false; } private IEnumerable SendMasterServerRequest() { RestClient client = null; try { client = new RestClient(NetConfig.MasterServerUrl); } catch (Exception e) { DebugConsole.ThrowError("Error while connecting to master server", e); } if (client == null) yield return CoroutineStatus.Success; var request = new RestRequest("masterserver2.php", Method.GET); request.AddParameter("gamename", "barotrauma"); request.AddParameter("action", "listservers"); // execute the request masterServerResponded = false; var restRequestHandle = client.ExecuteAsync(request, response => MasterServerCallBack(response)); DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 8); while (!masterServerResponded) { if (DateTime.Now > timeOut) { serverList.ClearChildren(); restRequestHandle.Abort(); new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.Get("MasterServerTimeOutError")); yield return CoroutineStatus.Success; } yield return CoroutineStatus.Running; } if (masterServerResponse.ErrorException != null) { serverList.ClearChildren(); new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerErrorException", "[error]", masterServerResponse.ErrorException.ToString())); } else if (masterServerResponse.StatusCode != System.Net.HttpStatusCode.OK) { serverList.ClearChildren(); switch (masterServerResponse.StatusCode) { case System.Net.HttpStatusCode.NotFound: new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariable("MasterServerError404", "[masterserverurl]", NetConfig.MasterServerUrl)); break; case System.Net.HttpStatusCode.ServiceUnavailable: new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.Get("MasterServerErrorUnavailable")); break; default: new GUIMessageBox(TextManager.Get("MasterServerErrorLabel"), TextManager.GetWithVariables("MasterServerErrorDefault", new string[2] { "[statuscode]", "[statusdescription]" }, new string[2] { masterServerResponse.StatusCode.ToString(), masterServerResponse.StatusDescription })); break; } } else { UpdateServerList(masterServerResponse.Content); } yield return CoroutineStatus.Success; } private void MasterServerCallBack(IRestResponse response) { masterServerResponse = response; masterServerResponded = true; } private bool JoinServer(string endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { ClientNameBox.Flash(); ClientNameBox.Select(); GUI.PlayUISound(GUISoundType.PickItemFail); return false; } GameMain.Config.PlayerName = ClientNameBox.Text; GameMain.Config.SaveNewPlayerConfig(); CoroutineManager.StartCoroutine(ConnectToServer(endpoint, serverName), "ConnectToServer"); return true; } private IEnumerable ConnectToServer(string endpoint, string serverName) { string serverIP = null; UInt64 serverSteamID = SteamManager.SteamIDStringToUInt64(endpoint); if (serverSteamID == 0) { serverIP = endpoint; } #if !DEBUG try { #endif GameMain.Client = new GameClient(GameMain.Config.PlayerName, serverIP, serverSteamID, serverName); #if !DEBUG } catch (Exception e) { DebugConsole.ThrowError("Failed to start the client", e); } #endif yield return CoroutineStatus.Success; } public void GetServerPing(ServerInfo serverInfo, GUITextBlock serverPingText) { if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; } lock (activePings) { if (activePings.Contains(serverInfo.IP)) { return; } activePings.Add(serverInfo.IP); } serverInfo.PingChecked = false; serverInfo.Ping = -1; TaskPool.Add(PingServerAsync(serverInfo?.IP, 1000), new Tuple(serverInfo, serverPingText), (rtt, obj) => { var info = obj.Item1; var text = obj.Item2; info.Ping = rtt.Result; info.PingChecked = true; text.TextColor = GetPingTextColor(info.Ping); text.Text = info.Ping > -1 ? info.Ping.ToString() : "?"; lock (activePings) { activePings.Remove(serverInfo.IP); } }); } private Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUI.Style.Green, GUI.Style.Orange, GUI.Style.Red); } public async Task PingServerAsync(string ip, int timeOut) { await Task.Yield(); int activePingCount = 100; while (activePingCount > 25) { lock (activePings) { activePingCount = activePings.Count; } await Task.Delay(25); } if (string.IsNullOrWhiteSpace(ip)) { return -1; } long rtt = -1; IPAddress address = null; IPAddress.TryParse(ip, out address); if (address != null) { //don't attempt to ping if the address is IPv6 and it's not supported if (address.AddressFamily != AddressFamily.InterNetworkV6 || Socket.OSSupportsIPv6) { Ping ping = new Ping(); byte[] buffer = new byte[32]; try { PingReply pingReply = ping.Send(address, timeOut, buffer, new PingOptions(128, true)); if (pingReply != null) { switch (pingReply.Status) { case IPStatus.Success: rtt = pingReply.RoundtripTime; break; default: rtt = -1; break; } } } catch (Exception ex) { string errorMsg = "Failed to ping a server (" + ip + ") - " + (ex?.InnerException?.Message ?? ex.Message); GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ip, GameAnalyticsSDK.Net.EGAErrorSeverity.Warning, errorMsg); #if DEBUG DebugConsole.NewMessage(errorMsg, Color.Red); #endif } } } return (int)rtt; } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { graphics.Clear(Color.CornflowerBlue); GameMain.TitleScreen.DrawLoadingText = false; GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch); spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } public override void AddToGUIUpdateList() { menu.AddToGUIUpdateList(); friendPopup?.AddToGUIUpdateList(); friendsDropdown?.AddToGUIUpdateList(); } } }