using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Xml.Linq; namespace Barotrauma { sealed class ServerListScreen : Screen { private enum MsgUserData { RefreshingServerList, NoServers, NoMatchingServers } //how often the client is allowed to refresh servers private static readonly TimeSpan AllowedRefreshInterval = TimeSpan.FromSeconds(3); private DateTime lastRefreshTime = DateTime.Now; private GUIFrame menu; private GUIListBox serverList; private PanelAnimator panelAnimator; private GUIFrame serverPreviewContainer; private GUIListBox serverPreview; private GUIButton joinButton; private Option selectedServer; private GUIButton scanServersButton; private enum TernaryOption { Any, Enabled, Disabled } //friends list public sealed class FriendInfo { public string Name; public readonly AccountId Id; public enum Status { Offline, NotPlaying, PlayingAnotherGame, PlayingBarotrauma } public readonly Status CurrentStatus; public string ServerName; public Option ConnectCommand; public Option Avatar; public bool IsInServer => CurrentStatus == Status.PlayingBarotrauma && ConnectCommand.IsSome(); public bool IsPlayingBarotrauma => CurrentStatus == Status.PlayingBarotrauma; public bool PlayingAnotherGame => CurrentStatus == Status.PlayingAnotherGame; public bool IsOnline => CurrentStatus != Status.Offline; public LocalizedString StatusText => CurrentStatus switch { Status.Offline => "", _ when ConnectCommand.IsSome() => TextManager.GetWithVariable("FriendPlayingOnServer", "[servername]", ServerName), _ => TextManager.Get($"Friend{CurrentStatus}") }; public FriendInfo(string name, AccountId id, Status status) { Name = name; Id = id; CurrentStatus = status; ConnectCommand = Option.None(); Avatar = Option.None(); } } private GUILayoutGroup friendsButtonHolder; private GUIButton friendsDropdownButton; private GUIListBox friendsDropdown; private readonly FriendProvider friendProvider = new SteamFriendProvider(); private List friendsList; private GUIFrame friendPopup; private double friendsListUpdateTime; public enum TabEnum { All, Favorites, Recent } public struct Tab { public readonly string Storage; public readonly GUIButton Button; private readonly List servers; public IReadOnlyList Servers => servers; public Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage) { Storage = storage; servers = new List(); Button = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), tabber.RectTransform), TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton") { OnClicked = (_,__) => { serverListScreen.selectedTab = tabEnum; return false; } }; Reload(); } public void Reload() { if (Storage.IsNullOrEmpty()) { return; } servers.Clear(); XDocument doc = XMLExtensions.TryLoadXml(Storage, out _); if (doc?.Root is null) { return; } servers.AddRange(doc.Root.Elements().Select(ServerInfo.FromXElement).NotNone().Distinct()); } public bool Contains(ServerInfo info) => servers.Contains(info); public bool Remove(ServerInfo info) => servers.Remove(info); public void AddOrUpdate(ServerInfo info) { servers.Remove(info); servers.Add(info); } public void Clear() => servers.Clear(); public void Save() { XDocument doc = new XDocument(); XElement rootElement = new XElement("servers"); doc.Add(rootElement); foreach (ServerInfo info in servers) { rootElement.Add(info.ToXElement()); } doc.SaveSafe(Storage); } } private readonly Dictionary tabs = new Dictionary(); private TabEnum _selectedTabBackingField; private TabEnum selectedTab { get => _selectedTabBackingField; set { _selectedTabBackingField = value; tabs.ForEach(kvp => kvp.Value.Button.Selected = (value == kvp.Key)); if (Screen.Selected == this) { RefreshServers(); } } } private readonly ServerProvider serverProvider = new CompositeServerProvider(new SteamDedicatedServerProvider(), new SteamP2PServerProvider()); public GUITextBox ClientNameBox { get; private set; } enum ColumnLabel { ServerListCompatible, ServerListHasPassword, ServerListName, ServerListRoundStarted, ServerListPlayers, ServerListPing } private struct Column { public float RelativeWidth; public ColumnLabel Label; public static implicit operator Column((float W, ColumnLabel L) pair) => new Column { RelativeWidth = pair.W, Label = pair.L }; public static Column[] Normalize(params Column[] columns) { var totalWidth = columns.Select(c => c.RelativeWidth).Aggregate((a, b) => a + b); for (int i = 0; i < columns.Length; i++) { columns[i].RelativeWidth /= totalWidth; } return columns; } } private static readonly ImmutableDictionary columns = Column.Normalize( (0.1f, ColumnLabel.ServerListCompatible), (0.1f, ColumnLabel.ServerListHasPassword), (0.7f, ColumnLabel.ServerListName), (0.12f, ColumnLabel.ServerListRoundStarted), (0.08f, ColumnLabel.ServerListPlayers), (0.08f, ColumnLabel.ServerListPing) ).Select(c => (c.Label, c)).ToImmutableDictionary(); private GUILayoutGroup labelHolder; private readonly List labelTexts = new List(); //filters private GUITextBox searchBox; private GUITickBox filterSameVersion; private GUITickBox filterPassword; private GUITickBox filterFull; private GUITickBox filterEmpty; private Dictionary ternaryFilters; private Dictionary filterTickBoxes; private Dictionary playStyleTickBoxes; private Dictionary gameModeTickBoxes; private GUITickBox filterOffensive; //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually. private TernaryOption filterFriendlyFireValue = TernaryOption.Any; private TernaryOption filterKarmaValue = TernaryOption.Any; private TernaryOption filterTraitorValue = TernaryOption.Any; private TernaryOption filterVoipValue = TernaryOption.Any; private TernaryOption filterModdedValue = TernaryOption.Any; private ColumnLabel sortedBy; private const float sidebarWidth = 0.2f; public ServerListScreen() { selectedServer = Option.None(); GameMain.Instance.ResolutionChanged += CreateUI; CreateUI(); } private string GetDefaultUserName() { return friendProvider.GetUserName(); } private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action valueSetter) { var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true) { Stretch = true }; var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { IsFixedSize = true, }, null) { HoverColor = Color.Gray, SelectedColor = Color.DarkGray, CanBeFocused = false }; if (box.RectTransform.MinSize.Y > 0) { box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y); box.RectTransform.Resize(box.RectTransform.MinSize); } Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f); var filterLabel = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), TextManager.Get("servertag." + tag + ".label"), textAlignment: Alignment.CenterLeft) { UserData = TextManager.Get($"servertag.{tag}.label") }; GUIStyle.Apply(filterLabel, "GUITextBlock", null); var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3); dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any); dropDown.AddItem(TextManager.Get($"servertag.{tag}.true"), TernaryOption.Enabled, TextManager.Get( $"servertagdescription.{tag}.true")); dropDown.AddItem(TextManager.Get($"servertag.{tag}.false"), TernaryOption.Disabled, TextManager.Get( $"servertagdescription.{tag}.false")); dropDown.SelectItem(TernaryOption.Any); dropDown.OnSelected = (_, data) => { valueSetter((TernaryOption)data); FilterServers(); StoreServerFilters(); return true; }; ternaryFilters.Add(tag, dropDown); } 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: GUIStyle.LargeFont) { Padding = Vector4.Zero, ForceUpperCase = ForceUpperCase.Yes, 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: GUIStyle.SubHeadingFont); ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "") { Text = MultiplayerPreferences.Instance.PlayerName, MaxTextLength = Client.MaxNameLength, OverflowClip = true }; if (string.IsNullOrEmpty(ClientNameBox.Text)) { ClientNameBox.Text = GetDefaultUserName(); } ClientNameBox.OnTextChanged += (textbox, text) => { MultiplayerPreferences.Instance.PlayerName = text; return true; }; var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true); tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, ""); tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml"); tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml"); 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; // 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: GUIStyle.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) }; ternaryFilters = new Dictionary(); filterTickBoxes = new Dictionary(); GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false) { text ??= TextManager.Get(key); var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform), text) { UserData = text, Selected = defaultState, ToolTip = addTooltip ? text : null, OnSelected = (tickBox) => { FilterServers(); StoreServerFilters(); return true; } }; filterTickBoxes.Add(key, tickBox); return tickBox; } filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true); filterPassword = addTickBox("FilterPassword".ToIdentifier()); filterFull = addTickBox("FilterFullServers".ToIdentifier()); filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier()); filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier()); // Filter Tags new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; }); AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; }); AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; }); AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; }); AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; }); // Play Style Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; playStyleTickBoxes = new Dictionary(); foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle))) { var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true); selectionTick.UserData = playStyle; playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick); } // Game mode Selection new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false }; gameModeTickBoxes = new Dictionary(); foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) { continue; } var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true); selectionTick.UserData = mode.Identifier; gameModeTickBoxes.Add(mode.Identifier, selectionTick); } filters.Content.RectTransform.SizeChanged += () => { filters.Content.RectTransform.RecalculateChildren(true, true); filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString()); gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip); GUITextBlock.AutoScaleAndNormalize( filterTickBoxes.Values.Select(tb => tb.TextBlock) .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild())), defaultScale: 1.0f); if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f) { filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f); filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.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(1.0f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) }, isHorizontal: true, childAnchor: Anchor.BottomLeft) { Stretch = false }; foreach (var column in columns.Values) { var label = TextManager.Get(column.Label.ToString()); var btn = new GUIButton(new RectTransform(new Vector2(column.RelativeWidth, 1.0f), labelHolder.RectTransform), text: label, textAlignment: Alignment.Center, style: "GUIButtonSmall") { ToolTip = label, ForceUpperCase = ForceUpperCase.Yes, UserData = column.Label, OnClicked = SortList }; btn.Color *= 0.5f; labelTexts.Add(btn.TextBlock); GUIImage arrowImg(object userData, SpriteEffects sprEffects) => new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true) { CanBeFocused = false, UserData = userData, SpriteEffects = sprEffects, Visible = false }; arrowImg("arrowup", SpriteEffects.None); arrowImg("arrowdown", SpriteEffects.FlipVertically); } serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center)) { PlaySoundOnSelect = true, ScrollBarVisible = true, OnSelected = (btn, obj) => { if (!(obj is ServerInfo serverInfo)) { return false; } joinButton.Enabled = true; selectedServer = Option.Some(serverInfo); if (!serverPreviewContainer.Visible) { serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f); serverPreviewContainer.Visible = true; serverPreviewContainer.IgnoreLayoutGroups = false; } serverInfo.CreatePreviewWindow(serverPreview.Content); serverPreview.ForceLayoutRecalculation(); panelAnimator.RightEnabled = true; panelAnimator.RightVisible = true; btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally); return true; } }; //server preview panel -------------------------------------------------- serverPreviewContainer = 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 }; serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center)) { Padding = Vector4.One * 10 * GUI.Scale, HoverCursor = CursorState.Default, OnSelected = (component, o) => false }; panelAnimator = new PanelAnimator(new RectTransform(Vector2.One, serverListHolder.RectTransform), filtersHolder, serverListContainer, serverPreviewContainer); panelAnimator.RightEnabled = 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 }; scanServersButton = 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(); SoundPlayer.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.TryUnwrap(out var serverInfo)) { JoinServer(serverInfo.Endpoint, serverInfo.ServerName); } 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.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); labelHolder.Recalculate(); serverList.Content.RectTransform.SizeChanged += () => { labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue); labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0); labelHolder.Recalculate(); foreach (GUITextBlock labelText in labelTexts) { labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width); } }; button.SelectedColor = button.Color; selectedTab = TabEnum.All; } public void UpdateOrAddServerInfo(ServerInfo serverInfo) { GUIComponent existingElement = serverList.Content.FindChild(d => d.UserData is ServerInfo existingServerInfo && existingServerInfo.Endpoint == serverInfo.Endpoint); if (existingElement == null) { AddToServerList(serverInfo); } else { existingElement.UserData = serverInfo; } } public void AddToRecentServers(ServerInfo info) { if (info.Endpoint.Address.IsLocalHost) { return; } tabs[TabEnum.Recent].AddOrUpdate(info); tabs[TabEnum.Recent].Save(); } public bool IsFavorite(ServerInfo info) => tabs[TabEnum.Favorites].Contains(info); public void AddToFavoriteServers(ServerInfo info) { tabs[TabEnum.Favorites].AddOrUpdate(info); tabs[TabEnum.Favorites].Save(); } public void RemoveFromFavoriteServers(ServerInfo info) { tabs[TabEnum.Favorites].Remove(info); tabs[TabEnum.Favorites].Save(); } private bool SortList(GUIButton button, object obj) { if (!(obj is ColumnLabel sortBy)) { return false; } SortList(sortBy, toggle: true); return true; } private void SortList(ColumnLabel sortBy, bool toggle) { if (!(labelHolder.GetChildByUserData(sortBy) is GUIButton button)) { 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) => { if (!(c1.GUIComponent.UserData is ServerInfo s1)) { return 0; } if (!(c2.GUIComponent.UserData is ServerInfo s2)) { return 0; } switch (sortBy) { case ColumnLabel.ServerListCompatible: bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion); bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); if (s1Compatible == s2Compatible) { return 0; } return (s1Compatible ? 1 : -1) * (ascending ? 1 : -1); case ColumnLabel.ServerListHasPassword: if (s1.HasPassword == s2.HasPassword) { return 0; } return (s1.HasPassword ? 1 : -1) * (ascending ? 1 : -1); case ColumnLabel.ServerListName: // I think we actually want culture-specific sorting here? return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * (ascending ? 1 : -1); case ColumnLabel.ServerListRoundStarted: if (s1.GameStarted == s2.GameStarted) { return 0; } return (s1.GameStarted ? 1 : -1) * (ascending ? 1 : -1); case ColumnLabel.ServerListPlayers: return s2.PlayerCount.CompareTo(s1.PlayerCount) * (ascending ? 1 : -1); case ColumnLabel.ServerListPing: return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch { (false, false) => 0, (true, true) => s2Ping.CompareTo(s1Ping) * (ascending ? 1 : -1), (false, true) => 1, (true, false) => -1 }; default: return 0; } }); } public override void Select() { base.Select(); Steamworks.SteamMatchmaking.ResetActions(); selectedTab = TabEnum.All; GameMain.ServerListScreen.LoadServerFilters(); if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt) { var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("FilterOffensiveServersPrompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) => { filterOffensive.Selected = true; filterOffensivePrompt.Close(); return true; }; filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close; var config = GameSettings.CurrentConfig; config.ShowOffensiveServerPrompt = false; GameSettings.SetCurrentConfig(config); } if (GameMain.Client != null) { GameMain.Client.Quit(); GameMain.Client = null; } RefreshServers(); } public override void Deselect() { base.Deselect(); GameSettings.SaveCurrentConfig(); } public override void Update(double deltaTime) { base.Update(deltaTime); UpdateFriendsList(); panelAnimator?.Update(); scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval; 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() { RemoveMsgFromServerList(MsgUserData.NoMatchingServers); foreach (GUIComponent child in serverList.Content.Children) { if (!(child.UserData is ServerInfo serverInfo)) { continue; } child.Visible = ShouldShowServer(serverInfo); } if (serverList.Content.Children.All(c => !c.Visible)) { PutMsgInServerList(MsgUserData.NoMatchingServers); } serverList.UpdateScrollBarSize(); } private bool ShouldShowServer(ServerInfo serverInfo) { #if !DEBUG //never show newer versions //(ignore revision number, it doesn't affect compatibility) if (ToolBox.VersionNewerIgnoreRevision(GameMain.Version, serverInfo.GameVersion)) { return false; } #endif if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; } if (filterSameVersion.Selected) { if (!NetworkMember.IsCompatible(serverInfo.GameVersion, GameMain.Version)) { return false; } } if (filterPassword.Selected) { if (serverInfo.HasPassword) { return false; } } if (filterFull.Selected) { if (serverInfo.PlayerCount >= serverInfo.MaxPlayers) { return false; } } if (filterEmpty.Selected) { if (serverInfo.PlayerCount <= 0) { return false; } } if (filterOffensive.Selected) { if (ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) { return false; } } if (filterKarmaValue != TernaryOption.Any) { if (serverInfo.KarmaEnabled != (filterKarmaValue == TernaryOption.Enabled)) { return false; } } if (filterFriendlyFireValue != TernaryOption.Any) { if (serverInfo.FriendlyFireEnabled != (filterFriendlyFireValue == TernaryOption.Enabled)) { return false; } } if (filterTraitorValue != TernaryOption.Any) { if ((serverInfo.TraitorsEnabled == YesNoMaybe.Yes || serverInfo.TraitorsEnabled == YesNoMaybe.Maybe) != (filterTraitorValue == TernaryOption.Enabled)) { return false; } } if (filterVoipValue != TernaryOption.Any) { if (serverInfo.VoipEnabled != (filterVoipValue == TernaryOption.Enabled)) { return false; } } if (filterModdedValue != TernaryOption.Any) { if (serverInfo.IsModded != (filterModdedValue == TernaryOption.Enabled)) { return false; } } foreach (GUITickBox tickBox in playStyleTickBoxes.Values) { var playStyle = (PlayStyle)tickBox.UserData; if (!tickBox.Selected && serverInfo.PlayStyle == playStyle) { return false; } } foreach (GUITickBox tickBox in gameModeTickBoxes.Values) { var gameMode = (Identifier)tickBox.UserData; if (!tickBox.Selected && !serverInfo.GameMode.IsEmpty && serverInfo.GameMode == gameMode) { return false; } } return true; } private void ShowDirectJoinPrompt() { var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "", new LocalizedString[] { 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) => { if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } JoinServer(endpoint, ""); msgBox.Close(); return false; }; var favoriteButton = msgBox.Buttons[1]; favoriteButton.Enabled = false; favoriteButton.OnClicked = (button, userdata) => { if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; } var serverInfo = new ServerInfo(endpoint) { ServerName = "Server", GameVersion = GameMain.Version }; var serverFrame = serverList.Content.FindChild(d => d.UserData is ServerInfo info && info.Equals(serverInfo)); if (serverFrame != null) { serverInfo = (ServerInfo)serverFrame.UserData; } else { AddToServerList(serverInfo); } AddToFavoriteServers(serverInfo); selectedTab = TabEnum.Favorites; FilterServers(); #warning Interface with server providers to get up-to-date info on the given server 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) { if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } GameMain.Instance.ConnectCommand = info.ConnectCommand; return false; } private bool OpenFriendPopup(GUIButton button, object userdata) { if (!(userdata is FriendInfo { IsInServer: true } info)) { return false; } if (info.IsInServer && info.ConnectCommand is Some { Value: { EndpointOrLobby: var endpointOrLobby } } && endpointOrLobby.TryGet(out ConnectCommand.NameAndEndpoint nameAndEndpoint)) { const int framePadding = 5; friendPopup = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas)); var serverNameText = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), friendPopup.RectTransform, Anchor.CenterLeft), nameAndEndpoint.ServerName ?? "[Unnamed]"); serverNameText.RectTransform.AbsoluteOffset = new Point(framePadding, 0); var joinButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), friendPopup.RectTransform, Anchor.CenterRight), TextManager.Get("ServerListJoin")) { UserData = info }; joinButton.OnClicked = JoinFriend; joinButton.RectTransform.AbsoluteOffset = new Point(framePadding, 0); Point joinButtonTextSize = joinButton.Font.MeasureString(joinButton.Text).ToPoint(); int joinButtonHeight = joinButton.RectTransform.NonScaledSize.Y; int totalAdditionalTextPadding = (joinButtonHeight - joinButtonTextSize.Y); // Make the final button sized so that the space between the text and the edges in the X direction is the same as the Y direction. Point finalButtonSize = new Point(joinButtonTextSize.X + totalAdditionalTextPadding, joinButtonHeight); // Add padding to the server name to match the padding on the button text. serverNameText.Padding = new Vector4(totalAdditionalTextPadding / 2); // Get the dimensions of the text we want to show, plus the extra padding we added. Point serverNameSize = serverNameText.Font.MeasureString(serverNameText.Text).ToPoint() + new Point(totalAdditionalTextPadding, totalAdditionalTextPadding); // Now determine how large the parent frame has to be to exactly fit our two controls. Point frameDims = new Point(serverNameSize.X + finalButtonSize.X + framePadding*2, Math.Max(serverNameSize.Y, finalButtonSize.Y) + framePadding * 2); var popupPos = PlayerInput.MousePosition.ToPoint(); if(popupPos.X+frameDims.X > GUI.Canvas.NonScaledSize.X) { // Prevent the Join button from going off the end of the screen if the server name is long or we click a user towards the edge. popupPos.X = GUI.Canvas.NonScaledSize.X - frameDims.X; } // Apply the size and position changes. friendPopup.RectTransform.NonScaledSize = frameDims; friendPopup.RectTransform.RelativeOffset = Vector2.Zero; friendPopup.RectTransform.AbsoluteOffset = popupPos; joinButton.RectTransform.NonScaledSize = finalButtonSize; friendPopup.RectTransform.RecalculateChildren(true); friendPopup.RectTransform.SetPosition(Anchor.TopLeft); } return false; } public enum AvatarSize { Small, Medium, Large } private void UpdateFriendsList() { if (friendsListUpdateTime > Timing.TotalTime) { return; } friendsListUpdateTime = Timing.TotalTime + 5.0; float prevDropdownScroll = friendsDropdown?.ScrollBar.BarScrollValue ?? 0.0f; friendsDropdown ??= new GUIListBox(new RectTransform(Vector2.One, GUI.Canvas)) { OutlineColor = Color.Black, Visible = false }; friendsDropdown.ClearChildren(); var avatarSize = friendsButtonHolder.RectTransform.Rect.Height switch { var h when h <= 24 => AvatarSize.Small, var h when h <= 48 => AvatarSize.Medium, _ => AvatarSize.Large }; FriendInfo[] friends = friendProvider.RetrieveFriends(); foreach (var friend in friends) { int existingIndex = friendsList.FindIndex(f => f.Id == friend.Id); if (existingIndex >= 0) { friend.Avatar = friend.Avatar.Fallback(friendsList[existingIndex].Avatar); } if (friend.Avatar.IsNone()) { friendProvider.RetrieveAvatar(friend, avatarSize); } } friendsList.Clear(); friendsList.AddRange(friends.OrderByDescending(f => f.CurrentStatus)); 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") { 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; } for (int i = 0; i < friendsList.Count; i++) { var friend = friendsList[i]; if (i < 5) { string style = friend.IsPlayingBarotrauma ? "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.Avatar.TryUnwrap(out Sprite sprite)) { new GUICustomComponent(new RectTransform(Vector2.One, guiButton.RectTransform, Anchor.Center), onDraw: (sb, component) => { var destinationRect = component.Rect; destinationRect.Inflate(-GUI.IntScale(4), -GUI.IntScale(4)); sb.Draw(sprite.Texture, destinationRect, Color.White); if (!GUI.IsMouseOn(guiButton)) { return; } sb.End(); sb.Begin( SpriteSortMode.Deferred, blendState: BlendState.Additive, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); sb.Draw(sprite.Texture, destinationRect, Color.White * 0.5f); sb.End(); sb.Begin( SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); }) { CanBeFocused = false }; } } var friendFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.167f), friendsDropdown.Content.RectTransform), style: "GUIFrameFriendsDropdown"); if (friend.Avatar.TryUnwrap(out var avatar)) { GUIImage guiImage = new GUIImage( new RectTransform(Vector2.One * 0.9f, friendFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(0.02f, 0.02f) }, avatar, 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 = GUIStyle.SmallFont }; if (friend.IsPlayingBarotrauma) { textBlock.TextColor = GUIStyle.Green; } if (friend.PlayingAnotherGame) { textBlock.TextColor = GUIStyle.Blue; } if (friend.IsInServer) { 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, 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 RemoveMsgFromServerList() { serverList.Content.Children .Where(c => c.UserData is MsgUserData) .ForEachMod(serverList.Content.RemoveChild); } private void RemoveMsgFromServerList(MsgUserData userData) { serverList.Content.RemoveChild(serverList.Content.FindChild(userData)); } private void PutMsgInServerList(MsgUserData userData) { RemoveMsgFromServerList(); new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform), TextManager.Get(userData.ToString()), textAlignment: Alignment.Center) { CanBeFocused = false, UserData = userData }; } private void RefreshServers() { lastRefreshTime = DateTime.Now; serverProvider.Cancel(); currentServerDataRecvCallbackObj = null; PingUtils.QueryPingData(); tabs[TabEnum.All].Clear(); serverList.ClearChildren(); serverPreview.Content.ClearChildren(); panelAnimator.RightEnabled = false; joinButton.Enabled = false; selectedServer = null; if (selectedTab == TabEnum.All) { PutMsgInServerList(MsgUserData.RefreshingServerList); } else { var servers = tabs[selectedTab].Servers.ToArray(); foreach (var server in servers) { server.Ping = Option.None(); AddToServerList(server, skipPing: true); } if (!servers.Any()) { PutMsgInServerList(MsgUserData.NoServers); return; } } var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks(); serverProvider.RetrieveServers(onServerDataReceived, onQueryCompleted); } private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo) { bool matches(GUIComponent c) => c.UserData is ServerInfo info && info.Equals(serverInfo); #if DEBUG if (serverList.Content.Children.Count(matches) > 1) { DebugConsole.ThrowError($"There are several entries in the server list for endpoint {serverInfo.Endpoint}"); } #endif return serverList.Content.FindChild(matches); } private object currentServerDataRecvCallbackObj = null; private (Action OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks() { var uniqueObject = new object(); currentServerDataRecvCallbackObj = uniqueObject; bool shouldRunCallback() { // If currentServerDataRecvCallbackObj != uniqueObject, then one of the following happened: // - The query this call is associated to was meant to be over // - Another query was started before the one associated to this call was finished // In either case, do not add the received info to the server list. return ReferenceEquals(currentServerDataRecvCallbackObj, uniqueObject); } return ( serverInfo => { if (!shouldRunCallback()) { return; } if (selectedTab == TabEnum.All) { AddToServerList(serverInfo); } else { if (FindFrameMatchingServerInfo(serverInfo) == null) { return; } UpdateServerInfoUI(serverInfo); PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } }, () => { if (shouldRunCallback()) { ServerQueryFinished(); } } ); } private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) { RemoveMsgFromServerList(MsgUserData.RefreshingServerList); RemoveMsgFromServerList(MsgUserData.NoServers); var 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(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = false }; UpdateServerInfoUI(serverInfo); if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } SortList(sortedBy, toggle: false); FilterServers(); } private void UpdateServerInfoUI(ServerInfo serverInfo) { var serverFrame = FindFrameMatchingServerInfo(serverInfo); if (serverFrame == null) { return; } serverFrame.UserData = serverInfo; serverFrame.ToolTip = ""; var serverContent = serverFrame.Children.First() as GUILayoutGroup; serverContent.ClearChildren(); Dictionary sections = new Dictionary(); foreach (ColumnLabel label in Enum.GetValues(typeof(ColumnLabel))) { sections[label] = new GUIFrame( new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform), style: null); } void errorTooltip(RichString toolTip) { sections.Values.ForEach(c => { c.CanBeFocused = false; c.Children.First().CanBeFocused = false; }); serverFrame.ToolTip = toolTip; } RectTransform columnRT(ColumnLabel label, float scale = 0.95f) => new RectTransform(Vector2.One * scale, sections[label].RectTransform, Anchor.Center); void sectionTooltip(ColumnLabel label, RichString toolTip) { var section = sections[label]; section.CanBeFocused = true; section.ToolTip = toolTip; } var compatibleBox = new GUITickBox(columnRT(ColumnLabel.ServerListCompatible), label: "") { CanBeFocused = false, Selected = NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion), UserData = "compatible" }; var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox") { Selected = serverInfo.HasPassword, UserData = "password", CanBeFocused = false }; sectionTooltip(ColumnLabel.ServerListHasPassword, TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword")); var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName), #if DEBUG $"[{serverInfo.Endpoint.GetType().Name}] " + #endif serverInfo.ServerName, style: "GUIServerListTextBox") { CanBeFocused = false }; if (serverInfo.IsModded) { serverName.TextColor = GUIStyle.ModdedServerColor; } new GUITickBox(columnRT(ColumnLabel.ServerListRoundStarted), label: "") { Selected = serverInfo.GameStarted, CanBeFocused = false }; sectionTooltip(ColumnLabel.ServerListRoundStarted, TextManager.Get(serverInfo.GameStarted ? "ServerListRoundStarted" : "ServerListRoundNotStarted")); var serverPlayers = new GUITextBlock(columnRT(ColumnLabel.ServerListPlayers), $"{serverInfo.PlayerCount}/{serverInfo.MaxPlayers}", style: "GUIServerListTextBox", textAlignment: Alignment.Right) { ToolTip = TextManager.Get("ServerListPlayers") }; var serverPingText = new GUITextBlock(columnRT(ColumnLabel.ServerListPing), "?", style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right) { ToolTip = TextManager.Get("ServerListPing") }; if (serverInfo.Ping.TryUnwrap(out var ping)) { serverPingText.Text = ping.ToString(); serverPingText.TextColor = GetPingTextColor(ping); } else { serverPingText.Text = "?"; serverPingText.TextColor = Color.DarkRed; } if (!serverInfo.Checked) { errorTooltip(TextManager.Get("ServerOffline")); serverName.TextColor *= 0.8f; serverPlayers.TextColor *= 0.8f; } else if (!serverInfo.ContentPackages.Any()) { compatibleBox.Selected = false; new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center), " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center) { ToolTip = TextManager.Get("ServerListUnknownContentPackage") }; } else if (!compatibleBox.Selected) { LocalizedString toolTip = ""; if (serverInfo.GameVersion != GameMain.Version) { toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString()); } int maxIncompatibleToList = 10; List incompatibleModNames = new List(); foreach (var contentPackage in serverInfo.ContentPackages) { bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(cp => cp.Hash.StringRepresentation == contentPackage.Hash); if (listAsIncompatible) { incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat", ("[name]", contentPackage.Name), ("[hash]", Md5Hash.GetShortHash(contentPackage.Hash)))); } } if (incompatibleModNames.Any()) { toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList)); if (incompatibleModNames.Count > maxIncompatibleToList) { toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString()); } } errorTooltip(toolTip); serverName.TextColor *= 0.5f; serverPlayers.TextColor *= 0.5f; } else { LocalizedString toolTip = ""; foreach (var contentPackage in serverInfo.ContentPackages) { if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash)) { if (toolTip != "") { toolTip += "\n"; } toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", contentPackage.Name); break; } } errorTooltip(toolTip); } foreach (var section in sections.Values) { var child = section.Children.First(); child.RectTransform.ScaleBasis = child is GUITextBlock ? ScaleBasis.Normal : ScaleBasis.BothHeight; } // The next twenty-something lines are an optimization. // The issue is that the serverlist has a ton of text elements, // and resizing all of them is extremely expensive. However, since // you don't see most of them most of the time, it makes sense to // just resize them lazily based on when you actually can see them. // That would entail a UI refactor of some kind, and I don't want to // do that just yet, so here's a hack instead! bool isDirty = true; void markAsDirty() => isDirty = true; serverContent.GetAllChildren().ForEach(c => { c.RectTransform.ResetSizeChanged(); c.RectTransform.SizeChanged += markAsDirty; }); new GUICustomComponent(new RectTransform(Vector2.Zero, serverContent.RectTransform), onUpdate: (_, __) => { if (serverFrame.MouseRect.Height <= 0 || !isDirty) { return; } serverContent.GetAllChildren().ForEach(c => { switch (c) { case GUITextBlock textBlock: textBlock.SetTextPos(); break; case GUITickBox tickBox: tickBox.ResizeBox(); break; } }); serverName.Text = ToolBox.LimitString(serverInfo.ServerName, serverName.Font, serverName.Rect.Width); isDirty = false; }); // Hacky optimization ends here serverContent.Recalculate(); if (tabs[TabEnum.Favorites].Contains(serverInfo)) { AddToFavoriteServers(serverInfo); } SortList(sortedBy, toggle: false); FilterServers(); } private void ServerQueryFinished() { currentServerDataRecvCallbackObj = null; if (!serverList.Content.Children.Any(c => c.UserData is ServerInfo)) { PutMsgInServerList(MsgUserData.NoServers); } else if (serverList.Content.Children.All(c => !c.Visible)) { PutMsgInServerList(MsgUserData.NoMatchingServers); } } public void JoinServer(Endpoint endpoint, string serverName) { if (string.IsNullOrWhiteSpace(ClientNameBox.Text)) { ClientNameBox.Flash(); ClientNameBox.Select(); SoundPlayer.PlayUISound(GUISoundType.PickItemFail); return; } MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text; GameSettings.SaveCurrentConfig(); #if !DEBUG try { #endif GameMain.Client = new GameClient(MultiplayerPreferences.Instance.PlayerName.FallbackNullOrEmpty(GetDefaultUserName()), endpoint, serverName, Option.None()); #if !DEBUG } catch (Exception e) { DebugConsole.ThrowError("Failed to start the client", e); } #endif } private Color GetPingTextColor(int ping) { if (ping < 0) { return Color.DarkRed; } return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); } 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(); } public void StoreServerFilters() { foreach (KeyValuePair filterBox in filterTickBoxes) { ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString()); } foreach (KeyValuePair ternaryFilter in ternaryFilters) { ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString()); } } public void LoadServerFilters() { XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath); ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters")); foreach (KeyValuePair filterBox in filterTickBoxes) { filterBox.Value.Selected = ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected); } foreach (KeyValuePair ternaryFilter in ternaryFilters) { TernaryOption ternaryOption = ServerListFilters.Instance.GetAttributeEnum( ternaryFilter.Key, (TernaryOption)ternaryFilter.Value.SelectedData); var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption); ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child)); } } } }