Client LuaCsForBarotrauma
ServerListScreen.cs
2 using Barotrauma.IO;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 using System;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
9 using System.Linq;
10 using System.Threading.Tasks;
11 using System.Xml.Linq;
12 using Barotrauma.Steam;
13 
14 namespace Barotrauma
15 {
16  sealed class ServerListScreen : Screen
17  {
18  private enum MsgUserData
19  {
20  RefreshingServerList,
21  NoServers,
22  NoMatchingServers
23  }
24 
25  //how often the client is allowed to refresh servers
26  private static readonly TimeSpan AllowedRefreshInterval = TimeSpan.FromSeconds(3);
27 
28  private DateTime lastRefreshTime = DateTime.Now;
29 
30  private GUIFrame menu;
31 
32  private GUIListBox serverList;
33  private PanelAnimator panelAnimator;
34  private GUIFrame serverPreviewContainer;
35  private GUIListBox serverPreview;
36 
37  private GUIButton joinButton;
38  private Option<ServerInfo> selectedServer;
39 
40  private GUIButton scanServersButton;
41 
42  private enum TernaryOption
43  {
44  Any,
45  Enabled,
46  Disabled
47  }
48 
49  public enum TabEnum
50  {
51  All,
52  Favorites,
53  Recent
54  }
55 
56  public readonly struct Tab
57  {
58  public readonly string Storage;
59  public readonly GUIButton Button;
60 
61  private readonly List<ServerInfo> servers;
62  public IReadOnlyList<ServerInfo> Servers => servers;
63 
64  public Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage)
65  {
66  Storage = storage;
67  servers = new List<ServerInfo>();
68  Button = new GUIButton(new RectTransform(new Vector2(0.33f, 1.0f), tabber.RectTransform),
69  TextManager.Get($"ServerListTab.{tabEnum}"), style: "GUITabButton")
70  {
71  OnClicked = (_,__) =>
72  {
73  serverListScreen.selectedTab = tabEnum;
74  return false;
75  }
76  };
77 
78  Reload();
79  }
80 
81  public void Reload()
82  {
83  if (Storage.IsNullOrEmpty()) { return; }
84  servers.Clear();
85  XDocument doc = XMLExtensions.TryLoadXml(Storage, out _);
86  if (doc?.Root is null) { return; }
87  servers.AddRange(doc.Root.Elements().Select(ServerInfo.FromXElement).NotNone().Distinct());
88  }
89 
90  public bool Contains(ServerInfo info) => servers.Contains(info);
91  public bool Remove(ServerInfo info) => servers.Remove(info);
92  public void AddOrUpdate(ServerInfo info)
93  {
94  servers.Remove(info); servers.Add(info);
95  }
96 
97  public void Clear() => servers.Clear();
98 
99  public void Save()
100  {
101  XDocument doc = new XDocument();
102  XElement rootElement = new XElement("servers");
103  doc.Add(rootElement);
104 
105  foreach (ServerInfo info in servers)
106  {
107  rootElement.Add(info.ToXElement());
108  }
109 
110  doc.SaveSafe(Storage);
111  }
112  }
113 
114  private readonly Dictionary<TabEnum, Tab> tabs = new Dictionary<TabEnum, Tab>();
115 
116  private TabEnum _selectedTabBackingField;
117  private TabEnum selectedTab
118  {
119  get => _selectedTabBackingField;
120  set
121  {
122  _selectedTabBackingField = value;
123  tabs.ForEach(kvp => kvp.Value.Button.Selected = (value == kvp.Key));
124  if (Screen.Selected == this) { RefreshServers(); }
125  }
126  }
127 
128  private ServerProvider serverProvider = null;
129 
130  public GUITextBox ClientNameBox { get; private set; }
131 
132  enum ColumnLabel
133  {
134  ServerListCompatible,
135  ServerListHasPassword,
136  ServerListName,
137  ServerListRoundStarted,
138  ServerListPlayers,
139  ServerListPing
140  }
141  private struct Column
142  {
143  public float RelativeWidth;
144  public ColumnLabel Label;
145 
146  public static implicit operator Column((float W, ColumnLabel L) pair) =>
147  new Column { RelativeWidth = pair.W, Label = pair.L };
148 
149  public static Column[] Normalize(params Column[] columns)
150  {
151  var totalWidth = columns.Select(c => c.RelativeWidth).Aggregate((a, b) => a + b);
152  for (int i = 0; i < columns.Length; i++)
153  {
154  columns[i].RelativeWidth /= totalWidth;
155  }
156  return columns;
157  }
158  }
159 
160  private static readonly ImmutableDictionary<ColumnLabel, Column> columns =
161  Column.Normalize(
162  (0.1f, ColumnLabel.ServerListCompatible),
163  (0.1f, ColumnLabel.ServerListHasPassword),
164  (0.7f, ColumnLabel.ServerListName),
165  (0.12f, ColumnLabel.ServerListRoundStarted),
166  (0.08f, ColumnLabel.ServerListPlayers),
167  (0.08f, ColumnLabel.ServerListPing)
168  ).Select(c => (c.Label, c)).ToImmutableDictionary();
169 
170  private GUILayoutGroup labelHolder;
171  private readonly List<GUITextBlock> labelTexts = new List<GUITextBlock>();
172 
173  //filters
174  private GUITextBox searchBox;
175  private GUITickBox filterSameVersion;
176  private GUITickBox filterPassword;
177  private GUITickBox filterFull;
178  private GUITickBox filterEmpty;
179  private GUIDropDown languageDropdown;
180  private Dictionary<Identifier, GUIDropDown> ternaryFilters;
181  private Dictionary<Identifier, GUITickBox> filterTickBoxes;
182  private Dictionary<Identifier, GUITickBox> playStyleTickBoxes;
183  private Dictionary<Identifier, GUITickBox> gameModeTickBoxes;
184  private GUITickBox filterOffensive;
185 
186  //GUIDropDown sends the OnSelected event before SelectedData is set, so we have to cache it manually.
187  private TernaryOption filterFriendlyFireValue = TernaryOption.Any;
188  private TernaryOption filterKarmaValue = TernaryOption.Any;
189  private TernaryOption filterTraitorValue = TernaryOption.Any;
190  private TernaryOption filterVoipValue = TernaryOption.Any;
191  private TernaryOption filterModdedValue = TernaryOption.Any;
192 
193  private ColumnLabel sortedBy;
194  private bool sortedAscending = true;
195 
196  private const float sidebarWidth = 0.2f;
197  public ServerListScreen() : base()
198  {
199  selectedServer = Option<ServerInfo>.None();
200  GameMain.Instance.ResolutionChanged += CreateUI;
201  CreateUI();
202  }
203 
204  private static Task<string> GetDefaultUserName()
205  {
207  }
208 
209  private void AddTernaryFilter(RectTransform parent, float elementHeight, Identifier tag, Action<TernaryOption> valueSetter)
210  {
211  var filterLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), parent), isHorizontal: true)
212  {
213  Stretch = true
214  };
215 
216  var box = new GUIFrame(new RectTransform(Vector2.One, filterLayoutGroup.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight)
217  {
218  IsFixedSize = true,
219  }, null)
220  {
221  HoverColor = Color.Gray,
222  SelectedColor = Color.DarkGray,
223  CanBeFocused = false
224  };
225  if (box.RectTransform.MinSize.Y > 0)
226  {
227  box.RectTransform.MinSize = new Point(box.RectTransform.MinSize.Y);
228  box.RectTransform.Resize(box.RectTransform.MinSize);
229  }
230  Vector2 textBlockScale = new Vector2((float)(filterLayoutGroup.Rect.Width - filterLayoutGroup.Rect.Height) / (float)Math.Max(filterLayoutGroup.Rect.Width, 1.0), 1.0f);
231 
232  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)
233  {
234  UserData = TextManager.Get($"servertag.{tag}.label")
235  };
236  GUIStyle.Apply(filterLabel, "GUITextBlock", null);
237 
238  var dropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f) * textBlockScale, filterLayoutGroup.RectTransform, Anchor.CenterLeft), elementCount: 3);
239  dropDown.AddItem(TextManager.Get("any"), TernaryOption.Any);
240  dropDown.AddItem(TextManager.Get($"servertag.{tag}.true"), TernaryOption.Enabled, TextManager.Get(
241  $"servertagdescription.{tag}.true"));
242  dropDown.AddItem(TextManager.Get($"servertag.{tag}.false"), TernaryOption.Disabled, TextManager.Get(
243  $"servertagdescription.{tag}.false"));
244  dropDown.SelectItem(TernaryOption.Any);
245  dropDown.OnSelected = (_, data) => {
246  valueSetter((TernaryOption)data);
247  FilterServers();
249  return true;
250  };
251 
252  ternaryFilters.Add(tag, dropDown);
253  }
254 
255  private void CreateUI()
256  {
257  menu = new GUIFrame(new RectTransform(new Vector2(0.95f, 0.85f), GUI.Canvas, Anchor.Center) { MinSize = new Point(GameMain.GraphicsHeight, 0) });
258 
259  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.98f), menu.RectTransform, Anchor.Center))
260  {
261  RelativeSpacing = 0.02f,
262  Stretch = true
263  };
264 
265  //-------------------------------------------------------------------------------------
266  //Top row
267  //-------------------------------------------------------------------------------------
268 
269  var topRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), paddedFrame.RectTransform)) { Stretch = true };
270 
271  var titleContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.995f, 0.33f), topRow.RectTransform), isHorizontal: true) { Stretch = true };
272 
273  var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), titleContainer.RectTransform), TextManager.Get("JoinServer"), font: GUIStyle.LargeFont)
274  {
275  Padding = Vector4.Zero,
277  AutoScaleHorizontal = true
278  };
279 
280  var friendsButton = new GUIButton(
281  new RectTransform(Vector2.One * 0.9f, titleContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
282  style: "FriendsButton")
283  {
284  OnClicked = (_, _) =>
285  {
286  if (SocialOverlay.Instance is { } socialOverlay) { socialOverlay.IsOpen = true; }
287  return false;
288  },
289  ToolTip = TextManager.GetWithVariable("SocialOverlayShortcutHint", "[shortcut]", SocialOverlay.ShortcutBindText)
290  };
291  new GUIFrame(new RectTransform(Vector2.One, friendsButton.RectTransform, Anchor.Center),
292  style: "FriendsButtonIcon")
293  {
294  CanBeFocused = false
295  };
296 
297  var infoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.33f), topRow.RectTransform), isHorizontal: true, Anchor.BottomLeft) { RelativeSpacing = 0.01f, Stretch = false };
298 
299  var clientNameHolder = new GUILayoutGroup(new RectTransform(new Vector2(sidebarWidth, 1.0f), infoHolder.RectTransform)) { RelativeSpacing = 0.05f };
300 
301  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), clientNameHolder.RectTransform), TextManager.Get("YourName"), font: GUIStyle.SubHeadingFont);
302  ClientNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), clientNameHolder.RectTransform), "")
303  {
304  Text = MultiplayerPreferences.Instance.PlayerName,
305  MaxTextLength = Client.MaxNameLength,
306  OverflowClip = true
307  };
308 
309  var tabButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - sidebarWidth - infoHolder.RelativeSpacing, 0.5f), infoHolder.RectTransform), isHorizontal: true);
310 
311  tabs[TabEnum.All] = new Tab(TabEnum.All, this, tabButtonHolder, "");
312  tabs[TabEnum.Favorites] = new Tab(TabEnum.Favorites, this, tabButtonHolder, "Data/favoriteservers.xml");
313  tabs[TabEnum.Recent] = new Tab(TabEnum.Recent, this, tabButtonHolder, "Data/recentservers.xml");
314 
315  //-------------------------------------------------------------------------------------
316  // Bottom row
317  //-------------------------------------------------------------------------------------
318 
319  var bottomRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f - topRow.RectTransform.RelativeSize.Y),
320  paddedFrame.RectTransform, Anchor.CenterRight))
321  {
322  Stretch = true
323  };
324 
325  var serverListHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), bottomRow.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
326  {
327  OutlineColor = Color.Black
328  };
329 
330  GUILayoutGroup serverListContainer = null;
331  GUIFrame filtersHolder = null;
332 
333  // filters -------------------------------------------
334 
335  filtersHolder = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null)
336  {
337  Color = new Color(12, 14, 15, 255) * 0.5f,
338  OutlineColor = Color.Black
339  };
340 
341  float elementHeight = 0.05f;
342  var filterTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform), TextManager.Get("FilterServers"), font: GUIStyle.SubHeadingFont)
343  {
344  Padding = Vector4.Zero,
345  AutoScaleHorizontal = true,
346  CanBeFocused = false
347  };
348 
349  var searchHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, elementHeight), filtersHolder.RectTransform) { RelativeOffset = new Vector2(0.0f, elementHeight) }, isHorizontal: true) { Stretch = true };
350 
351  var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), searchHolder.RectTransform), TextManager.Get("Search") + "...");
352  searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 1.0f), searchHolder.RectTransform), "");
353  searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; };
354  searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; };
355  searchBox.OnTextChanged += (txtBox, txt) => { FilterServers(); return true; };
356 
357  var filters = new GUIListBox(new RectTransform(new Vector2(0.98f, 1.0f - elementHeight * 2), filtersHolder.RectTransform, Anchor.BottomLeft))
358  {
359  ScrollBarVisible = true,
360  Spacing = (int)(5 * GUI.Scale)
361  };
362 
363  ternaryFilters = new Dictionary<Identifier, GUIDropDown>();
364  filterTickBoxes = new Dictionary<Identifier, GUITickBox>();
365 
366  RectTransform createFilterRectT()
367  => new RectTransform(new Vector2(1.0f, elementHeight), filters.Content.RectTransform);
368 
369  GUITickBox addTickBox(Identifier key, LocalizedString text = null, bool defaultState = false, bool addTooltip = false)
370  {
371  text ??= TextManager.Get(key);
372  var tickBox = new GUITickBox(createFilterRectT(), text)
373  {
374  UserData = text,
375  Selected = defaultState,
376  ToolTip = addTooltip ? text : null,
377  OnSelected = (tickBox) =>
378  {
379  FilterServers();
381  return true;
382  }
383  };
384  filterTickBoxes.Add(key, tickBox);
385  return tickBox;
386  }
387 
388  filterSameVersion = addTickBox("FilterSameVersion".ToIdentifier(), defaultState: true);
389  filterPassword = addTickBox("FilterPassword".ToIdentifier());
390  filterFull = addTickBox("FilterFullServers".ToIdentifier());
391  filterEmpty = addTickBox("FilterEmptyServers".ToIdentifier());
392  filterOffensive = addTickBox("FilterOffensiveServers".ToIdentifier());
393 
394  // Language filter
395  if (ServerLanguageOptions.Options.Any())
396  {
397  var languageKey = "Language".ToIdentifier();
398  var allLanguagesKey = "AllLanguages".ToIdentifier();
399 
400  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get(languageKey), font: GUIStyle.SubHeadingFont)
401  {
402  CanBeFocused = false
403  };
404 
405  languageDropdown = new GUIDropDown(createFilterRectT(), selectMultiple: true);
406 
407  languageDropdown.AddItem(TextManager.Get(allLanguagesKey), allLanguagesKey);
408  var allTickbox = languageDropdown.ListBox.Content.FindChild(allLanguagesKey)?.GetChild<GUITickBox>();
409 
410  // Spacer between "All" and the individual languages
411  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), languageDropdown.ListBox.Content.RectTransform)
412  {
413  MinSize = new Point(0, GUI.IntScaleCeiling(2))
414  }, style: null)
415  {
416  Color = Color.DarkGray,
417  CanBeFocused = false
418  };
419 
420  var selectedLanguages
421  = ServerListFilters.Instance.GetAttributeLanguageIdentifierArray(
422  languageKey,
423  Array.Empty<LanguageIdentifier>());
424  foreach (var (label, identifier, _) in ServerLanguageOptions.Options)
425  {
426  languageDropdown.AddItem(label, identifier);
427  }
428 
429  if (!selectedLanguages.Any())
430  {
431  selectedLanguages = ServerLanguageOptions.Options.Select(o => o.Identifier).ToArray();
432  }
433 
434  foreach (var lang in selectedLanguages)
435  {
436  languageDropdown.SelectItem(lang);
437  }
438 
439  if (ServerLanguageOptions.Options.All(o => selectedLanguages.Any(l => o.Identifier == l)))
440  {
441  languageDropdown.SelectItem(allLanguagesKey);
442  languageDropdown.Text = TextManager.Get(allLanguagesKey);
443  }
444 
445  var langTickboxes = languageDropdown.ListBox.Content.Children
446  .Where(c => c.UserData is LanguageIdentifier)
447  .Select(c => c.GetChild<GUITickBox>())
448  .ToArray();
449 
450  bool inSelectedCall = false;
451  languageDropdown.OnSelected = (_, userData) =>
452  {
453  if (inSelectedCall) { return true; }
454  try
455  {
456  inSelectedCall = true;
457 
458  if (Equals(allLanguagesKey, userData))
459  {
460  foreach (var tb in langTickboxes)
461  {
462  tb.Selected = allTickbox.Selected;
463  }
464  }
465 
466  bool noneSelected = langTickboxes.All(tb => !tb.Selected);
467  bool allSelected = langTickboxes.All(tb => tb.Selected);
468 
469  if (allSelected != allTickbox.Selected)
470  {
471  allTickbox.Selected = allSelected;
472  }
473 
474  if (allSelected)
475  {
476  languageDropdown.Text = TextManager.Get(allLanguagesKey);
477  }
478  else if (noneSelected)
479  {
480  languageDropdown.Text = TextManager.Get("None");
481  }
482 
483  var languages = languageDropdown.SelectedDataMultiple.OfType<LanguageIdentifier>();
484 
485  ServerListFilters.Instance.SetAttribute(languageKey, string.Join(", ", languages));
486  GameSettings.SaveCurrentConfig();
487  return true;
488  }
489  finally
490  {
491  inSelectedCall = false;
492  FilterServers();
493  }
494  };
495  }
496 
497  // Filter Tags
498  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("servertags"), font: GUIStyle.SubHeadingFont)
499  {
500  CanBeFocused = false
501  };
502 
503  AddTernaryFilter(filters.Content.RectTransform, elementHeight, "karma".ToIdentifier(), (value) => { filterKarmaValue = value; });
504  AddTernaryFilter(filters.Content.RectTransform, elementHeight, "traitors".ToIdentifier(), (value) => { filterTraitorValue = value; });
505  AddTernaryFilter(filters.Content.RectTransform, elementHeight, "friendlyfire".ToIdentifier(), (value) => { filterFriendlyFireValue = value; });
506  AddTernaryFilter(filters.Content.RectTransform, elementHeight, "voip".ToIdentifier(), (value) => { filterVoipValue = value; });
507  AddTernaryFilter(filters.Content.RectTransform, elementHeight, "modded".ToIdentifier(), (value) => { filterModdedValue = value; });
508 
509  // Play Style Selection
510  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("ServerSettingsPlayStyle"), font: GUIStyle.SubHeadingFont)
511  {
512  CanBeFocused = false
513  };
514 
515  playStyleTickBoxes = new Dictionary<Identifier, GUITickBox>();
516  foreach (PlayStyle playStyle in Enum.GetValues(typeof(PlayStyle)))
517  {
518  var selectionTick = addTickBox($"servertag.{playStyle}".ToIdentifier(), defaultState: true, addTooltip: true);
519  selectionTick.UserData = playStyle;
520  playStyleTickBoxes.Add($"servertag.{playStyle}".ToIdentifier(), selectionTick);
521  }
522 
523  // Game mode Selection
524  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), filters.Content.RectTransform), TextManager.Get("gamemode"), font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
525 
526  gameModeTickBoxes = new Dictionary<Identifier, GUITickBox>();
527  foreach (GameModePreset mode in GameModePreset.List)
528  {
529  if (mode.IsSinglePlayer) { continue; }
530 
531  var selectionTick = addTickBox(mode.Identifier, mode.Name, defaultState: true, addTooltip: true);
532  selectionTick.UserData = mode.Identifier;
533  gameModeTickBoxes.Add(mode.Identifier, selectionTick);
534  }
535 
536  filters.Content.RectTransform.SizeChanged += () =>
537  {
538  filters.Content.RectTransform.RecalculateChildren(true, true);
539  filterTickBoxes.ForEach(t => t.Value.Text = t.Value.UserData is LocalizedString lStr ? lStr : t.Value.UserData.ToString());
540  gameModeTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip);
541  playStyleTickBoxes.ForEach(tb => tb.Value.Text = tb.Value.ToolTip);
542  GUITextBlock.AutoScaleAndNormalize(
543  filterTickBoxes.Values.Select(tb => tb.TextBlock)
544  .Concat(ternaryFilters.Values.Select(dd => dd.Parent.GetChild<GUITextBlock>())),
545  defaultScale: 1.0f);
546  if (filterTickBoxes.Values.First().TextBlock.TextScale < 0.8f)
547  {
548  filterTickBoxes.ForEach(t => t.Value.TextBlock.TextScale = 1.0f);
549  filterTickBoxes.ForEach(t => t.Value.TextBlock.Text = ToolBox.LimitString(t.Value.TextBlock.Text, t.Value.TextBlock.Font, (int)(filters.Content.Rect.Width * 0.8f)));
550  }
551  };
552 
553  // server list ---------------------------------------------------------------------
554 
555  serverListContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverListHolder.RectTransform)) { Stretch = true };
556 
557  labelHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), serverListContainer.RectTransform) { MinSize = new Point(0, 15) },
558  isHorizontal: true, childAnchor: Anchor.BottomLeft)
559  {
560  Stretch = false
561  };
562 
563  foreach (var column in columns.Values)
564  {
565  var label = TextManager.Get(column.Label.ToString());
566  var btn = new GUIButton(new RectTransform(new Vector2(column.RelativeWidth, 1.0f), labelHolder.RectTransform),
567  text: label, textAlignment: Alignment.Center, style: "GUIButtonSmall")
568  {
569  ToolTip = label,
571  UserData = column.Label,
572  OnClicked = SortList
573  };
574  btn.Color *= 0.5f;
575  labelTexts.Add(btn.TextBlock);
576 
577  GUIImage arrowImg(object userData, SpriteEffects sprEffects)
578  => new GUIImage(new RectTransform(new Vector2(0.5f, 0.3f), btn.RectTransform, Anchor.BottomCenter, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrow", scaleToFit: true)
579  {
580  CanBeFocused = false,
581  UserData = userData,
582  SpriteEffects = sprEffects,
583  Visible = false
584  };
585 
586  arrowImg("arrowup", SpriteEffects.None);
587  arrowImg("arrowdown", SpriteEffects.FlipVertically);
588  }
589 
590  serverList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), serverListContainer.RectTransform, Anchor.Center))
591  {
592  PlaySoundOnSelect = true,
593  ScrollBarVisible = true,
594  OnSelected = (btn, obj) =>
595  {
596  if (GUI.MouseOn is GUIButton) { return false; }
597  if (obj is not ServerInfo serverInfo) { return false; }
598 
599  joinButton.Enabled = true;
600  selectedServer = Option<ServerInfo>.Some(serverInfo);
601  if (!serverPreviewContainer.Visible)
602  {
603  serverPreviewContainer.RectTransform.RelativeSize = new Vector2(sidebarWidth, 1.0f);
604  serverPreviewContainer.Visible = true;
605  serverPreviewContainer.IgnoreLayoutGroups = false;
606  }
607  serverInfo.CreatePreviewWindow(serverPreview.Content);
608  serverPreview.ForceLayoutRecalculation();
609  panelAnimator.RightEnabled = true;
610  panelAnimator.RightVisible = true;
611  btn.Children.ForEach(c => c.SpriteEffects = serverPreviewContainer.Visible ? SpriteEffects.None : SpriteEffects.FlipHorizontally);
612  return true;
613  }
614  };
615 
616  //server preview panel --------------------------------------------------
617  serverPreviewContainer = new GUIFrame(new RectTransform(new Vector2(sidebarWidth, 1.0f), serverListHolder.RectTransform, Anchor.Center), style: null)
618  {
619  Color = new Color(12, 14, 15, 255) * 0.5f,
620  OutlineColor = Color.Black,
621  IgnoreLayoutGroups = true
622  };
623  serverPreview = new GUIListBox(new RectTransform(Vector2.One, serverPreviewContainer.RectTransform, Anchor.Center))
624  {
625  Padding = Vector4.One * 10 * GUI.Scale,
626  HoverCursor = CursorState.Default,
627  OnSelected = (component, o) => false
628  };
629 
630  panelAnimator = new PanelAnimator(new RectTransform(Vector2.One, serverListHolder.RectTransform),
631  filtersHolder,
632  serverListContainer,
633  serverPreviewContainer);
634  panelAnimator.RightEnabled = false;
635 
636  // Spacing
637  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), bottomRow.RectTransform), style: null);
638 
639  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.075f), bottomRow.RectTransform, Anchor.Center), isHorizontal: true)
640  {
641  RelativeSpacing = 0.02f,
642  Stretch = true
643  };
644 
645  GUIButton button = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
646  TextManager.Get("Back"))
647  {
648  OnClicked = GameMain.MainMenuScreen.ReturnToMainMenu
649  };
650 
651  scanServersButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
652  TextManager.Get("ServerListRefresh"))
653  {
654  OnClicked = (btn, userdata) => { RefreshServers(); return true; }
655  };
656 
657  var directJoinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
658  TextManager.Get("serverlistdirectjoin"))
659  {
660  OnClicked = (btn, userdata) =>
661  {
662  if (string.IsNullOrWhiteSpace(ClientNameBox.Text))
663  {
666  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
667  return false;
668  }
669  ShowDirectJoinPrompt();
670  return true;
671  }
672  };
673 
674  joinButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.9f), buttonContainer.RectTransform),
675  TextManager.Get("ServerListJoin"))
676  {
677  OnClicked = (btn, userdata) =>
678  {
679  if (selectedServer.TryUnwrap(out var serverInfo))
680  {
681  JoinServer(serverInfo.Endpoints, serverInfo.ServerName);
682  }
683  return true;
684  },
685  Enabled = false
686  };
687 
688  buttonContainer.RectTransform.MinSize = new Point(0, (int)(buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y) * 1.2f));
689 
690  //--------------------------------------------------------
691 
692  bottomRow.Recalculate();
693  serverListHolder.Recalculate();
694  serverListContainer.Recalculate();
695  labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue);
696  labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0);
697  labelHolder.Recalculate();
698 
699  serverList.Content.RectTransform.SizeChanged += () =>
700  {
701  labelHolder.RectTransform.MaxSize = new Point(serverList.Content.Rect.Width, int.MaxValue);
702  labelHolder.RectTransform.AbsoluteOffset = new Point((int)serverList.Padding.X, 0);
703  labelHolder.Recalculate();
704  foreach (GUITextBlock labelText in labelTexts)
705  {
706  labelText.Text = ToolBox.LimitString(labelText.ToolTip, labelText.Font, labelText.Rect.Width);
707  }
708  };
709 
710  button.SelectedColor = button.Color;
711 
712  selectedTab = TabEnum.All;
713  }
714 
715  public void UpdateOrAddServerInfo(ServerInfo serverInfo)
716  {
717  GUIComponent existingElement = serverList.Content.FindChild(d =>
718  d.UserData is ServerInfo existingServerInfo &&
719  existingServerInfo.Endpoints.Any(serverInfo.Endpoints.Contains));
720  if (existingElement == null)
721  {
722  AddToServerList(serverInfo);
723  }
724  else
725  {
726  existingElement.UserData = serverInfo;
727  }
728  }
729 
730  public void AddToRecentServers(ServerInfo info)
731  {
732  if (info.Endpoints.First().Address.IsLocalHost) { return; }
733  tabs[TabEnum.Recent].AddOrUpdate(info);
734  tabs[TabEnum.Recent].Save();
735  }
736 
737  public bool IsFavorite(ServerInfo info)
738  => tabs[TabEnum.Favorites].Contains(info);
739 
741  {
742  tabs[TabEnum.Favorites].AddOrUpdate(info);
743  tabs[TabEnum.Favorites].Save();
744  }
745 
747  {
748  tabs[TabEnum.Favorites].Remove(info);
749  tabs[TabEnum.Favorites].Save();
750  }
751 
752  private bool SortList(GUIButton button, object obj)
753  {
754  if (obj is not ColumnLabel sortBy) { return false; }
755  SortList(sortBy, toggle: true);
756  return true;
757  }
758 
759  private void SortList(ColumnLabel sortBy, bool toggle)
760  {
761  if (labelHolder.GetChildByUserData(sortBy) is not GUIButton button) { return; }
762 
763  sortedBy = sortBy;
764 
765  var arrowUp = button.GetChildByUserData("arrowup");
766  var arrowDown = button.GetChildByUserData("arrowdown");
767 
768  //disable arrow buttons in other labels
769  foreach (var child in button.Parent.Children)
770  {
771  if (child != button)
772  {
773  child.GetChildByUserData("arrowup").Visible = false;
774  child.GetChildByUserData("arrowdown").Visible = false;
775  }
776  }
777 
778  sortedAscending = arrowUp.Visible;
779  if (toggle)
780  {
781  sortedAscending = !sortedAscending;
782  }
783 
784  arrowUp.Visible = sortedAscending;
785  arrowDown.Visible = !sortedAscending;
786  serverList.Content.RectTransform.SortChildren((c1, c2) =>
787  {
788  if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; }
789  if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; }
790  return CompareServer(sortBy, s1, s2, sortedAscending);
791  });
792  }
793 
794  public void HideServerPreview()
795  {
796  serverPreviewContainer.Visible = false;
797  panelAnimator.RightEnabled = false;
798  panelAnimator.RightVisible = false;
799  }
800 
801  private void InsertServer(ServerInfo serverInfo, GUIComponent component)
802  {
803  var children = serverList.Content.RectTransform.Children.Reverse().ToList();
804 
805  foreach (var child in children)
806  {
807  if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; }
808  if (CompareServer(sortedBy, serverInfo, serverInfo2, sortedAscending) >= 0)
809  {
810  var index = serverList.Content.RectTransform.GetChildIndex(child);
811  component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1));
812  return;
813  }
814  }
815  component.RectTransform.SetAsFirstChild();
816  }
817 
818  private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2, bool ascending)
819  {
820  //always put servers with unknown ping at the bottom (unless we're specifically sorting by ping)
821  //servers without a ping are often unreachable/spam
822  bool s1HasPing = s1.Ping.IsSome();
823  bool s2HasPing = s2.Ping.IsSome();
824  if (s1HasPing != s2HasPing)
825  {
826  return s1HasPing ? -1 : 1;
827  }
828 
829  int comparison = ascending ? 1 : -1;
830  switch (sortBy)
831  {
832  case ColumnLabel.ServerListCompatible:
833  bool s1Compatible = NetworkMember.IsCompatible(GameMain.Version, s1.GameVersion);
834  bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion);
835 
836  if (s1Compatible == s2Compatible) { return 0; }
837  return (s1Compatible ? -1 : 1) * comparison;
838  case ColumnLabel.ServerListHasPassword:
839  if (s1.HasPassword == s2.HasPassword) { return 0; }
840  return (s1.HasPassword ? 1 : -1) * comparison;
841  case ColumnLabel.ServerListName:
842  // I think we actually want culture-specific sorting here?
843  return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * comparison;
844  case ColumnLabel.ServerListRoundStarted:
845  if (s1.GameStarted == s2.GameStarted) { return 0; }
846  return (s1.GameStarted ? 1 : -1) * comparison;
847  case ColumnLabel.ServerListPlayers:
848  return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison;
849  case ColumnLabel.ServerListPing:
850  return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch
851  {
852  (false, false) => 0,
853  (true, true) => s2Ping.CompareTo(s1Ping),
854  (false, true) => 1,
855  (true, false) => -1
856  } * comparison;
857  default:
858  return 0;
859  }
860  }
861 
862  public override void Select()
863  {
864  base.Select();
865 
866  if (string.IsNullOrEmpty(ClientNameBox.Text))
867  {
868  TaskPool.Add("GetDefaultUserName",
869  GetDefaultUserName(),
870  t =>
871  {
872  if (!t.TryGetResult(out string name)) { return; }
873  if (ClientNameBox.Text.IsNullOrEmpty())
874  {
875  ClientNameBox.Text = name;
876  string nameWithoutInvisibleSymbols = string.Empty;
877  foreach (char c in ClientNameBox.Text)
878  {
879  Vector2 size = ClientNameBox.Font.MeasureChar(c);
880  if (size.X > 0 && size.Y > 0)
881  {
882  nameWithoutInvisibleSymbols += c;
883  }
884  }
885  if (nameWithoutInvisibleSymbols != ClientNameBox.Text)
886  {
887  MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text = nameWithoutInvisibleSymbols;
888  new GUIMessageBox(TextManager.Get("Warning"), TextManager.GetWithVariable("NameContainsInvisibleSymbols", "[name]", nameWithoutInvisibleSymbols));
889  }
890  }
891  });
892  }
893 
894  ClientNameBox.OnTextChanged += (textbox, text) =>
895  {
897  return true;
898  };
899  if (EosInterface.IdQueries.IsLoggedIntoEosConnect)
900  {
901  if (SteamManager.IsInitialized)
902  {
903  serverProvider = new CompositeServerProvider(
904  new EosServerProvider(),
905  new SteamDedicatedServerProvider(),
906  new SteamP2PServerProvider());
907  }
908  else
909  {
910  serverProvider = new EosServerProvider();
911  }
912  }
913  else if (SteamManager.IsInitialized)
914  {
915  serverProvider = new CompositeServerProvider(
916  new SteamDedicatedServerProvider(),
917  new SteamP2PServerProvider());
918  }
919  else
920  {
921  serverProvider = null;
922  }
923 
924  Steamworks.SteamMatchmaking.ResetActions();
925 
926  selectedTab = TabEnum.All;
927  GameMain.ServerListScreen.LoadServerFilters();
928  if (GameSettings.CurrentConfig.ShowOffensiveServerPrompt)
929  {
930  var filterOffensivePrompt = new GUIMessageBox(string.Empty, TextManager.Get("FilterOffensiveServersPrompt"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
931  filterOffensivePrompt.Buttons[0].OnClicked = (btn, userData) =>
932  {
933  filterOffensive.Selected = true;
934  filterOffensivePrompt.Close();
935  return true;
936  };
937  filterOffensivePrompt.Buttons[1].OnClicked = filterOffensivePrompt.Close;
938 
939  var config = GameSettings.CurrentConfig;
940  config.ShowOffensiveServerPrompt = false;
941  GameSettings.SetCurrentConfig(config);
942  }
943 
944  if (GameMain.Client != null)
945  {
946  GameMain.Client.Quit();
947  GameMain.Client = null;
948  }
949 
950  RefreshServers();
951  }
952 
953  public override void Deselect()
954  {
955  base.Deselect();
956  serverProvider?.Cancel();
957  GameSettings.SaveCurrentConfig();
958  }
959 
960  public override void Update(double deltaTime)
961  {
962  base.Update(deltaTime);
963 
964  panelAnimator?.Update();
965 
966  scanServersButton.Enabled = (DateTime.Now - lastRefreshTime) >= AllowedRefreshInterval;
967  }
968 
969  public void FilterServers()
970  {
971  RemoveMsgFromServerList(MsgUserData.NoMatchingServers);
972  foreach (GUIComponent child in serverList.Content.Children)
973  {
974  if (child.UserData is not ServerInfo serverInfo) { continue; }
975  child.Visible = ShouldShowServer(serverInfo);
976  }
977 
978  if (serverList.Content.Children.All(c => !c.Visible))
979  {
980  PutMsgInServerList(MsgUserData.NoMatchingServers);
981  }
982  serverList.UpdateScrollBarSize();
983  }
984 
985  private bool AllLanguagesVisible
986  {
987  get
988  {
989  if (languageDropdown is null) { return true; }
990 
991  // CountChildren-1 because there's a separator element in there that can't be selected
992  int tickBoxCount = languageDropdown.ListBox.Content.CountChildren - 1;
993  int selectedCount = languageDropdown.SelectedIndexMultiple.Count();
994 
995  return selectedCount >= tickBoxCount;
996  }
997  }
998 
999  private bool ShouldShowServer(ServerInfo serverInfo)
1000  {
1001 #if !DEBUG
1002  //never show newer versions
1003  //(ignore revision number, it doesn't affect compatibility)
1004  if (ToolBox.VersionNewerIgnoreRevision(GameMain.Version, serverInfo.GameVersion))
1005  {
1006  return false;
1007  }
1008 #endif
1009  if (SpamServerFilters.IsFiltered(serverInfo)) { return false; }
1010 
1011  if (!string.IsNullOrEmpty(searchBox.Text) && !serverInfo.ServerName.Contains(searchBox.Text, StringComparison.OrdinalIgnoreCase)) { return false; }
1012 
1013  if (filterSameVersion.Selected)
1014  {
1015  if (!NetworkMember.IsCompatible(serverInfo.GameVersion, GameMain.Version)) { return false; }
1016  }
1017  if (filterPassword.Selected)
1018  {
1019  if (serverInfo.HasPassword) { return false; }
1020  }
1021  if (filterFull.Selected)
1022  {
1023  if (serverInfo.PlayerCount >= serverInfo.MaxPlayers) { return false; }
1024  }
1025  if (filterEmpty.Selected)
1026  {
1027  if (serverInfo.PlayerCount <= 0) { return false; }
1028  }
1029  if (filterOffensive.Selected)
1030  {
1031  if (ForbiddenWordFilter.IsForbidden(serverInfo.ServerName)) { return false; }
1032  }
1033 
1034  if (filterKarmaValue != TernaryOption.Any)
1035  {
1036  if (serverInfo.KarmaEnabled != (filterKarmaValue == TernaryOption.Enabled)) { return false; }
1037  }
1038  if (filterFriendlyFireValue != TernaryOption.Any)
1039  {
1040  if (serverInfo.FriendlyFireEnabled != (filterFriendlyFireValue == TernaryOption.Enabled)) { return false; }
1041  }
1042  if (filterTraitorValue != TernaryOption.Any)
1043  {
1044  if ((serverInfo.TraitorProbability > 0.0f) != (filterTraitorValue == TernaryOption.Enabled))
1045  {
1046  return false;
1047  }
1048  }
1049  if (filterVoipValue != TernaryOption.Any)
1050  {
1051  if (serverInfo.VoipEnabled != (filterVoipValue == TernaryOption.Enabled)) { return false; }
1052  }
1053  if (filterModdedValue != TernaryOption.Any)
1054  {
1055  if (serverInfo.IsModded != (filterModdedValue == TernaryOption.Enabled)) { return false; }
1056  }
1057 
1058  foreach (GUITickBox tickBox in playStyleTickBoxes.Values)
1059  {
1060  var playStyle = (PlayStyle)tickBox.UserData;
1061  if (!tickBox.Selected && serverInfo.PlayStyle == playStyle)
1062  {
1063  return false;
1064  }
1065  }
1066 
1067  if (!AllLanguagesVisible)
1068  {
1069  if (!languageDropdown.SelectedDataMultiple.OfType<LanguageIdentifier>().Contains(serverInfo.Language))
1070  {
1071  return false;
1072  }
1073  }
1074 
1075  foreach (GUITickBox tickBox in gameModeTickBoxes.Values)
1076  {
1077  var gameMode = (Identifier)tickBox.UserData;
1078  if (!tickBox.Selected && !serverInfo.GameMode.IsEmpty && serverInfo.GameMode == gameMode)
1079  {
1080  return false;
1081  }
1082  }
1083 
1084  return true;
1085  }
1086 
1087  private void ShowDirectJoinPrompt()
1088  {
1089  var msgBox = new GUIMessageBox(TextManager.Get("ServerListDirectJoin"), "",
1090  new LocalizedString[] { TextManager.Get("ServerListJoin"), TextManager.Get("AddToFavorites"), TextManager.Get("Cancel") },
1091  relativeSize: new Vector2(0.25f, 0.2f), minSize: new Point(400, 150));
1092  msgBox.Content.ChildAnchor = Anchor.TopCenter;
1093 
1094  var content = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.5f), msgBox.Content.RectTransform), childAnchor: Anchor.TopCenter)
1095  {
1096  IgnoreLayoutGroups = false,
1097  Stretch = true,
1098  RelativeSpacing = 0.05f
1099  };
1100 
1101  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform),
1102  SteamManager.IsInitialized ? TextManager.Get("ServerEndpoint") : TextManager.Get("ServerIP"), textAlignment: Alignment.Center);
1103  var endpointBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.5f), content.RectTransform));
1104 
1105  content.RectTransform.NonScaledSize = new Point(content.Rect.Width, (int)(content.RectTransform.Children.Sum(c => c.Rect.Height)));
1106  content.RectTransform.IsFixedSize = true;
1107  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));
1108 
1109  var okButton = msgBox.Buttons[0];
1110  okButton.Enabled = false;
1111  okButton.OnClicked = (btn, userdata) =>
1112  {
1113  if (Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint))
1114  {
1115  if (endpoint is SteamP2PEndpoint && !SteamManager.IsInitialized)
1116  {
1117  new GUIMessageBox(TextManager.Get("error"), TextManager.Get("CannotJoinSteamServer.SteamNotInitialized"));
1118  }
1119  else
1120  {
1121  JoinServer(endpoint.ToEnumerable().ToImmutableArray(), "");
1122  }
1123  }
1124  else if (LidgrenEndpoint.ParseFromWithHostNameCheck(endpointBox.Text, tryParseHostName: true).TryUnwrap(out var lidgrenEndpoint))
1125  {
1126  JoinServer(((Endpoint)lidgrenEndpoint).ToEnumerable().ToImmutableArray(), "");
1127  }
1128  else
1129  {
1130  new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("invalidipaddress", "[serverip]:[port]", endpointBox.Text));
1131  endpointBox.Flash();
1132  }
1133  msgBox.Close();
1134  return false;
1135  };
1136 
1137  var favoriteButton = msgBox.Buttons[1];
1138  favoriteButton.Enabled = false;
1139  favoriteButton.OnClicked = (button, userdata) =>
1140  {
1141  if (!Endpoint.Parse(endpointBox.Text).TryUnwrap(out var endpoint)) { return false; }
1142 
1143  var serverInfo = new ServerInfo(endpoint)
1144  {
1145  ServerName = "Server",
1146  GameVersion = GameMain.Version
1147  };
1148 
1149  var serverFrame = serverList.Content.FindChild(d =>
1150  d.UserData is ServerInfo info
1151  && info.Equals(serverInfo));
1152 
1153  if (serverFrame != null)
1154  {
1155  serverInfo = (ServerInfo)serverFrame.UserData;
1156  }
1157  else
1158  {
1159  AddToServerList(serverInfo);
1160  }
1161 
1162  AddToFavoriteServers(serverInfo);
1163 
1164  selectedTab = TabEnum.Favorites;
1165  FilterServers();
1166 
1167  msgBox.Close();
1168  return false;
1169  };
1170 
1171  var cancelButton = msgBox.Buttons[2];
1172  cancelButton.OnClicked = msgBox.Close;
1173 
1174  endpointBox.OnTextChanged += (textBox, text) =>
1175  {
1176  okButton.Enabled = favoriteButton.Enabled = !string.IsNullOrEmpty(text);
1177  return true;
1178  };
1179  }
1180 
1181  private void RemoveMsgFromServerList()
1182  {
1183  serverList.Content.Children
1184  .Where(c => c.UserData is MsgUserData)
1185  .ForEachMod(serverList.Content.RemoveChild);
1186  }
1187 
1188  private void RemoveMsgFromServerList(MsgUserData userData)
1189  {
1190  serverList.Content.RemoveChild(serverList.Content.FindChild(userData));
1191  }
1192 
1193  private void PutMsgInServerList(MsgUserData userData)
1194  {
1195  RemoveMsgFromServerList();
1196  new GUITextBlock(new RectTransform(Vector2.One, serverList.Content.RectTransform),
1197  TextManager.Get(userData.ToString()), textAlignment: Alignment.Center)
1198  {
1199  CanBeFocused = false,
1200  UserData = userData
1201  };
1202  }
1203 
1204  private void RefreshServers()
1205  {
1206  lastRefreshTime = DateTime.Now;
1207  serverProvider?.Cancel();
1208  currentServerDataRecvCallbackObj = null;
1209 
1210  PingUtils.QueryPingData();
1211 
1212  tabs[TabEnum.All].Clear();
1213  serverList.ClearChildren();
1214  serverPreview.Content.ClearChildren();
1215  panelAnimator.RightEnabled = false;
1216  joinButton.Enabled = false;
1217  selectedServer = Option.None;
1218 
1219  if (selectedTab == TabEnum.All)
1220  {
1221  PutMsgInServerList(MsgUserData.RefreshingServerList);
1222  }
1223  else
1224  {
1225  var servers = tabs[selectedTab].Servers.ToArray();
1226  foreach (var server in servers)
1227  {
1228  server.Ping = Option<int>.None();
1229  AddToServerList(server, skipPing: true);
1230  }
1231 
1232  if (!servers.Any())
1233  {
1234  PutMsgInServerList(MsgUserData.NoServers);
1235  return;
1236  }
1237  }
1238 
1239  var (onServerDataReceived, onQueryCompleted) = MakeServerQueryCallbacks();
1240  serverProvider?.RetrieveServers(onServerDataReceived, onQueryCompleted);
1241  }
1242 
1243  private GUIComponent FindFrameMatchingServerInfo(ServerInfo serverInfo)
1244  {
1245  bool matches(GUIComponent c)
1246  => c.UserData is ServerInfo info
1247  && info.Equals(serverInfo);
1248 
1249 #if DEBUG
1250  if (serverList.Content.Children.Count(matches) > 1)
1251  {
1252  DebugConsole.ThrowError($"There are several entries in the server list for endpoints {string.Join(", ", serverInfo.Endpoints)}");
1253  }
1254 #endif
1255 
1256  return serverList.Content.FindChild(matches);
1257  }
1258 
1259  private object currentServerDataRecvCallbackObj = null;
1260  private (Action<ServerInfo, ServerProvider> OnServerDataReceived, Action OnQueryCompleted) MakeServerQueryCallbacks()
1261  {
1262  var uniqueObject = new object();
1263  currentServerDataRecvCallbackObj = uniqueObject;
1264 
1265  bool shouldRunCallback()
1266  {
1267  // If currentServerDataRecvCallbackObj != uniqueObject, then one of the following happened:
1268  // - The query this call is associated to was meant to be over
1269  // - Another query was started before the one associated to this call was finished
1270  // In either case, do not add the received info to the server list.
1271  return ReferenceEquals(currentServerDataRecvCallbackObj, uniqueObject);
1272  }
1273 
1274  return (
1275  (serverInfo, serverProvider) =>
1276  {
1277  if (!shouldRunCallback()) { return; }
1278 
1279  if (serverProvider is not EosServerProvider
1280  && EosInterface.IdQueries.IsLoggedIntoEosConnect)
1281  {
1282  if (serverInfo.EosCrossplay)
1283  {
1284  // EosServerProvider should get us this server,
1285  // don't add it again
1286  return;
1287  }
1288  }
1289 
1290  if (selectedTab == TabEnum.All)
1291  {
1292  AddToServerList(serverInfo);
1293  }
1294  else
1295  {
1296  if (FindFrameMatchingServerInfo(serverInfo) == null) { return; }
1297  UpdateServerInfoUI(serverInfo);
1298  PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI);
1299  }
1300  },
1301  () =>
1302  {
1303  if (shouldRunCallback()) { ServerQueryFinished(); }
1304  }
1305  );
1306  }
1307 
1308  private void AddToServerList(ServerInfo serverInfo, bool skipPing = false)
1309  {
1310  const int MaxAllowedPlayers = 1000;
1311  const int MaxAllowedSimilarServers = 10;
1312  const float MinSimilarityPercentage = 0.8f;
1313 
1314  if (string.IsNullOrWhiteSpace(serverInfo.ServerName)) { return; }
1315  if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; }
1316  if (serverInfo.PlayerCount < 0) { return; }
1317  if (serverInfo.MaxPlayers <= 0) { return; }
1318  //no way a legit server can have this many players
1319  if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; }
1320 
1321  int similarServerCount = 0;
1322  string serverInfoStr = getServerInfoStr(serverInfo);
1323  foreach (var serverElement in serverList.Content.Children)
1324  {
1325  if (!serverElement.Visible) { continue; }
1326  if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; }
1327  if (ToolBox.LevenshteinDistance(serverInfoStr, getServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage))
1328  {
1329  similarServerCount++;
1330  if (similarServerCount > MaxAllowedSimilarServers)
1331  {
1332  DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server.");
1333  break;
1334  }
1335  }
1336  }
1337  if (similarServerCount > MaxAllowedSimilarServers) { return; }
1338 
1339  static string getServerInfoStr(ServerInfo serverInfo)
1340  {
1341  string str = serverInfo.ServerName + serverInfo.ServerMessage + serverInfo.MaxPlayers;
1342  if (str.Length > 200) { return str.Substring(0, 200); }
1343  return str;
1344  }
1345 
1346  RemoveMsgFromServerList(MsgUserData.RefreshingServerList);
1347  RemoveMsgFromServerList(MsgUserData.NoServers);
1348  var serverFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.06f), serverList.Content.RectTransform) { MinSize = new Point(0, 35) },
1349  style: "ListBoxElement")
1350  {
1351  UserData = serverInfo,
1352  };
1353 
1354  serverFrame.OnSecondaryClicked += (_, data) =>
1355  {
1356  if (data is not ServerInfo info) { return false; }
1357  CreateContextMenu(info);
1358  return true;
1359  };
1360 
1361  new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 1.0f), serverFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
1362  {
1363  Stretch = false
1364  };
1365  UpdateServerInfoUI(serverInfo);
1366  if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
1367  }
1368 
1369  private static readonly Vector2 confirmPopupSize = new Vector2(0.2f, 0.2625f);
1370  private static readonly Point confirmPopupMinSize = new Point(300, 300);
1371 
1372  private void CreateContextMenu(ServerInfo info)
1373  {
1374  var favoriteOption = new ContextMenuOption(IsFavorite(info) ? "removefromfavorites" : "addtofavorites", isEnabled: true, () =>
1375  {
1376  if (IsFavorite(info))
1377  {
1378  RemoveFromFavoriteServers(info);
1379  }
1380  else
1381  {
1382  AddToFavoriteServers(info);
1383  }
1384  FilterServers();
1385  });
1386  var reportOption = new ContextMenuOption("reportserver", isEnabled: true, () => { CreateReportPrompt(info); });
1387  var filterOption = new ContextMenuOption("filterserver", isEnabled: true, () =>
1388  {
1389  CreateFilterServerPrompt(info);
1390  })
1391  {
1392  Tooltip = TextManager.Get("filterservertooltip")
1393  };
1394 
1395  GUIContextMenu.CreateContextMenu(favoriteOption, filterOption, reportOption);
1396  }
1397 
1398  public static void CreateFilterServerPrompt(ServerInfo info)
1399  {
1400  GUI.AskForConfirmation(
1401  header: TextManager.Get("filterserver"),
1402  body: TextManager.GetWithVariables("filterserverconfirm", ("[server]", info.ServerName), ("[filepath]", SpamServerFilter.SavePath)),
1403  onConfirm: () =>
1404  {
1405  SpamServerFilters.AddServerToLocalSpamList(info);
1406 
1407  if (GameMain.ServerListScreen is not { } serverListScreen) { return; }
1408 
1409  if (serverListScreen.selectedServer.TryUnwrap(out var selectedServer) && selectedServer.Equals(info))
1410  {
1411  serverListScreen.HideServerPreview();
1412  }
1413  serverListScreen.FilterServers();
1414  }, relativeSize: confirmPopupSize, minSize: confirmPopupMinSize);
1415  }
1416 
1417  private enum ReportReason
1418  {
1419  Spam,
1420  Advertising,
1421  Inappropriate
1422  }
1423 
1424  public static void CreateReportPrompt(ServerInfo info)
1425  {
1426  if (!GameAnalyticsManager.SendUserStatistics)
1427  {
1428  GUI.NotifyPrompt(TextManager.Get("reportserver"), TextManager.Get("reportserverdisabled"));
1429  return;
1430  }
1431 
1432  var msgBox = new GUIMessageBox(
1433  headerText: TextManager.Get("reportserver"),
1434  text: string.Empty,
1435  relativeSize: new Vector2(0.2f, 0.4f),
1436  minSize: new Point(380, 430),
1437  buttons: Array.Empty<LocalizedString>());
1438 
1439  var layout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform, Anchor.Center));
1440 
1441  new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform), TextManager.GetWithVariable("reportserverexplanation", "[server]", info.ServerName), wrap: true)
1442  {
1443  ToolTip = TextManager.Get("reportserverprompttooltip")
1444  };
1445 
1446  var listBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform));
1447 
1448  var enums = Enum.GetValues<ReportReason>();
1449  foreach (ReportReason reason in enums)
1450  {
1451  new GUITickBox(new RectTransform(new Vector2(1f, 1f / enums.Length), listBox.Content.RectTransform), TextManager.Get($"reportreason.{reason}"))
1452  {
1453  UserData = reason
1454  };
1455  }
1456 
1457  // padding
1458  new GUIFrame(new RectTransform(new Vector2(1f, 0.05f), layout.RectTransform), style: null);
1459 
1460  var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.3f), layout.RectTransform))
1461  {
1462  Stretch = true
1463  };
1464 
1465  var reportAndHideButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.reportandhide"))
1466  {
1467  Enabled = false,
1468  OnClicked = (_, _) =>
1469  {
1470  CreateFilterServerPrompt(info);
1471  msgBox.Close();
1472  return true;
1473  }
1474  };
1475  var reportButton = new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("reportoption.report"))
1476  {
1477  Enabled = false,
1478  OnClicked = (_, _) =>
1479  {
1480  ReportServer(info, GetUserSelectedReasons());
1481  msgBox.Close();
1482  return true;
1483  }
1484  };
1485 
1486  new GUIButton(new RectTransform(new Vector2(1f, 0.333f), buttonLayout.RectTransform), TextManager.Get("cancel"))
1487  {
1488  OnClicked = (_, _) =>
1489  {
1490  msgBox.Close();
1491  return true;
1492  }
1493  };
1494 
1495  foreach (var child in listBox.Content.GetAllChildren<GUITickBox>())
1496  {
1497  child.OnSelected += _ =>
1498  {
1499  reportAndHideButton.Enabled = reportButton.Enabled = GetUserSelectedReasons().Any();
1500  return true;
1501  };
1502  }
1503 
1504  IEnumerable<ReportReason> GetUserSelectedReasons()
1505  => listBox.Content.Children
1506  .Where(static c => c.UserData is ReportReason && c.Selected)
1507  .Select(static c => (ReportReason)c.UserData).ToArray();
1508  }
1509 
1510  private static void ReportServer(ServerInfo info, IEnumerable<ReportReason> reasons)
1511  {
1512  if (!reasons.Any()) { return; }
1513  GameAnalyticsManager.AddErrorEvent(GameAnalyticsManager.ErrorSeverity.Info, $"[Spam] Reported server: Name: \"{info.ServerName}\", Message: \"{info.ServerMessage}\", Endpoint: \"{info.Endpoints.First().StringRepresentation}\". Reason: \"{string.Join(", ", reasons)}\".");
1514  }
1515 
1516  private void UpdateServerInfoUI(ServerInfo serverInfo)
1517  {
1518  var serverFrame = FindFrameMatchingServerInfo(serverInfo);
1519  if (serverFrame == null) { return; }
1520 
1521  serverFrame.UserData = serverInfo;
1522 
1523  var serverContent = serverFrame.Children.First() as GUILayoutGroup;
1524  serverContent.ClearChildren();
1525 
1526  Dictionary<ColumnLabel, GUIFrame> sections = new Dictionary<ColumnLabel, GUIFrame>();
1527  foreach (ColumnLabel label in Enum.GetValues(typeof(ColumnLabel)))
1528  {
1529  sections[label] =
1530  new GUIFrame(
1531  new RectTransform(new Vector2(columns[label].RelativeWidth, 1.0f), serverContent.RectTransform),
1532  style: null);
1533  }
1534 
1535  void disableElementFocus()
1536  {
1537  sections.Values.ForEach(c =>
1538  {
1539  c.CanBeFocused = false;
1540  c.Children.First().CanBeFocused = false;
1541  });
1542  }
1543 
1544  RectTransform columnRT(ColumnLabel label, float scale = 0.95f)
1545  => new RectTransform(Vector2.One * scale, sections[label].RectTransform, Anchor.Center);
1546 
1547  void sectionTooltip(ColumnLabel label, RichString toolTip)
1548  {
1549  var section = sections[label];
1550  section.CanBeFocused = true;
1551  section.ToolTip = toolTip;
1552  }
1553 
1554  var compatibleBox = new GUITickBox(columnRT(ColumnLabel.ServerListCompatible), label: "")
1555  {
1556  CanBeFocused = false,
1557  Selected =
1558  NetworkMember.IsCompatible(GameMain.Version, serverInfo.GameVersion),
1559  UserData = "compatible"
1560  };
1561 
1562  var passwordBox = new GUITickBox(columnRT(ColumnLabel.ServerListHasPassword, scale: 0.6f), label: "", style: "GUIServerListPasswordTickBox")
1563  {
1564  Selected = serverInfo.HasPassword,
1565  UserData = "password",
1566  CanBeFocused = false
1567  };
1568  sectionTooltip(ColumnLabel.ServerListHasPassword,
1569  TextManager.Get((serverInfo.HasPassword) ? "ServerListHasPassword" : "FilterPassword"));
1570 
1571  var serverName = new GUITextBlock(columnRT(ColumnLabel.ServerListName),
1572 #if DEBUG
1573  $"[{serverInfo.Endpoints.First().GetType().Name}] " +
1574 #endif
1575  serverInfo.ServerName,
1576  style: "GUIServerListTextBox") { CanBeFocused = false };
1577 
1578  if (serverInfo.IsModded)
1579  {
1580  serverName.TextColor = GUIStyle.ModdedServerColor;
1581  }
1582 
1583  new GUITickBox(columnRT(ColumnLabel.ServerListRoundStarted), label: "")
1584  {
1585  Selected = serverInfo.GameStarted,
1586  CanBeFocused = false
1587  };
1588  sectionTooltip(ColumnLabel.ServerListRoundStarted,
1589  TextManager.Get(serverInfo.GameStarted ? "ServerListRoundStarted" : "ServerListRoundNotStarted"));
1590 
1591  var serverPlayers = new GUITextBlock(columnRT(ColumnLabel.ServerListPlayers),
1592  $"{serverInfo.PlayerCount}/{serverInfo.MaxPlayers}", style: "GUIServerListTextBox", textAlignment: Alignment.Right)
1593  {
1594  ToolTip = TextManager.Get("ServerListPlayers")
1595  };
1596 
1597  var serverPingText = new GUITextBlock(columnRT(ColumnLabel.ServerListPing), "?",
1598  style: "GUIServerListTextBox", textColor: Color.White * 0.5f, textAlignment: Alignment.Right)
1599  {
1600  ToolTip = TextManager.Get("ServerListPing")
1601  };
1602 
1603  if (serverInfo.Ping.TryUnwrap(out var ping))
1604  {
1605  serverPingText.Text = ping.ToString();
1606  serverPingText.TextColor = GetPingTextColor(ping);
1607  }
1608  else if ((serverInfo.Endpoints.Length == 1 && serverInfo.Endpoints.First() is EosP2PEndpoint)
1609  || (!SteamManager.IsInitialized && serverInfo.Endpoints.Any(e => e is P2PEndpoint)))
1610  {
1611  serverPingText.Text = "-";
1612  serverPingText.ToolTip = TextManager.Get("EosPingUnavailable");
1613  serverPingText.TextAlignment = Alignment.Center;
1614  }
1615  else
1616  {
1617  serverPingText.Text = "?";
1618  serverPingText.TextColor = Color.DarkRed;
1619  }
1620 
1621  LocalizedString toolTip = "";
1622  if (!serverInfo.Checked)
1623  {
1624  toolTip = TextManager.Get("ServerOffline");
1625  serverName.TextColor *= 0.8f;
1626  serverPlayers.TextColor *= 0.8f;
1627  }
1628  else if (!serverInfo.ContentPackages.Any())
1629  {
1630  compatibleBox.Selected = false;
1631  new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), compatibleBox.Box.RectTransform, Anchor.Center),
1632  " ? ", GUIStyle.Orange * 0.85f, textAlignment: Alignment.Center)
1633  {
1634  ToolTip = TextManager.Get("ServerListUnknownContentPackage")
1635  };
1636  }
1637  else if (!compatibleBox.Selected)
1638  {
1639  if (serverInfo.GameVersion != GameMain.Version)
1640  {
1641  toolTip = TextManager.GetWithVariable("ServerListIncompatibleVersion", "[version]", serverInfo.GameVersion.ToString());
1642  }
1643 
1644  int maxIncompatibleToList = 10;
1645  List<LocalizedString> incompatibleModNames = new List<LocalizedString>();
1646  foreach (var contentPackage in serverInfo.ContentPackages)
1647  {
1648  bool listAsIncompatible = !ContentPackageManager.EnabledPackages.All.Any(cp => cp.Hash.StringRepresentation == contentPackage.Hash);
1649  if (listAsIncompatible)
1650  {
1651  incompatibleModNames.Add(TextManager.GetWithVariables("ModNameAndHashFormat",
1652  ("[name]", contentPackage.Name),
1653  ("[hash]", Md5Hash.GetShortHash(contentPackage.Hash))));
1654  }
1655  }
1656  if (incompatibleModNames.Any())
1657  {
1658  toolTip += '\n' + TextManager.Get("ModDownloadHeader") + "\n" + string.Join(", ", incompatibleModNames.Take(maxIncompatibleToList));
1659  if (incompatibleModNames.Count > maxIncompatibleToList)
1660  {
1661  toolTip += '\n' + TextManager.GetWithVariable("workshopitemdownloadprompttruncated", "[number]", (incompatibleModNames.Count - maxIncompatibleToList).ToString());
1662  }
1663  }
1664 
1665  serverName.TextColor *= 0.5f;
1666  serverPlayers.TextColor *= 0.5f;
1667  }
1668  else
1669  {
1670  foreach (var contentPackage in serverInfo.ContentPackages)
1671  {
1672  if (ContentPackageManager.EnabledPackages.All.None(cp => cp.Hash.StringRepresentation == contentPackage.Hash))
1673  {
1674  if (toolTip != "") { toolTip += "\n"; }
1675  toolTip += TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", contentPackage.Name);
1676  break;
1677  }
1678  }
1679  }
1680  disableElementFocus();
1681 
1682  string separator = toolTip.IsNullOrWhiteSpace() ? "" : "\n\n";
1683  serverFrame.ToolTip = RichString.Rich(toolTip + separator + $"‖color:gui.blue‖{TextManager.GetWithVariable("serverlisttooltip", "[button]", PlayerInput.SecondaryMouseLabel)}‖end‖");
1684 
1685  foreach (var section in sections.Values)
1686  {
1687  var child = section.Children.First();
1688  child.RectTransform.ScaleBasis
1689  = child is GUITextBlock ? ScaleBasis.Normal : ScaleBasis.BothHeight;
1690  }
1691 
1692  // The next twenty-something lines are an optimization.
1693  // The issue is that the serverlist has a ton of text elements,
1694  // and resizing all of them is extremely expensive. However, since
1695  // you don't see most of them most of the time, it makes sense to
1696  // just resize them lazily based on when you actually can see them.
1697  // That would entail a UI refactor of some kind, and I don't want to
1698  // do that just yet, so here's a hack instead!
1699  bool isDirty = true;
1700  void markAsDirty() => isDirty = true;
1701  serverContent.GetAllChildren().ForEach(c =>
1702  {
1703  c.RectTransform.ResetSizeChanged();
1704  c.RectTransform.SizeChanged += markAsDirty;
1705  });
1706  new GUICustomComponent(new RectTransform(Vector2.Zero, serverContent.RectTransform), onUpdate: (_, __) =>
1707  {
1708  if (serverFrame.MouseRect.Height <= 0 || !isDirty) { return; }
1709  serverContent.GetAllChildren().ForEach(c =>
1710  {
1711  switch (c)
1712  {
1713  case GUITextBlock textBlock:
1714  textBlock.SetTextPos();
1715  break;
1716  case GUITickBox tickBox:
1717  tickBox.ResizeBox();
1718  break;
1719  }
1720  });
1721  serverName.Text = ToolBox.LimitString(serverInfo.ServerName, serverName.Font, serverName.Rect.Width);
1722  isDirty = false;
1723  });
1724  // Hacky optimization ends here
1725 
1726  serverContent.Recalculate();
1727 
1728  if (tabs[TabEnum.Favorites].Contains(serverInfo))
1729  {
1730  AddToFavoriteServers(serverInfo);
1731  }
1732 
1733  InsertServer(serverInfo, serverFrame);
1734  FilterServers();
1735  }
1736 
1737  private void ServerQueryFinished()
1738  {
1739  currentServerDataRecvCallbackObj = null;
1740  if (!serverList.Content.Children.Any(c => c.UserData is ServerInfo))
1741  {
1742  PutMsgInServerList(MsgUserData.NoServers);
1743  }
1744  else if (serverList.Content.Children.All(c => !c.Visible))
1745  {
1746  PutMsgInServerList(MsgUserData.NoMatchingServers);
1747  }
1748  }
1749 
1750  public void JoinServer(ImmutableArray<Endpoint> endpoints, string serverName)
1751  {
1752  if (string.IsNullOrWhiteSpace(ClientNameBox.Text))
1753  {
1754  ClientNameBox.Flash();
1755  ClientNameBox.Select();
1756  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
1757  return;
1758  }
1759 
1760  MultiplayerPreferences.Instance.PlayerName = ClientNameBox.Text;
1761  GameSettings.SaveCurrentConfig();
1762 
1763  if (MultiplayerPreferences.Instance.PlayerName.IsNullOrEmpty())
1764  {
1765  TaskPool.Add("GetDefaultUserName",
1766  GetDefaultUserName(),
1767  t =>
1768  {
1769  if (!t.TryGetResult(out string name)) { return; }
1770  startClient(name);
1771  });
1772  }
1773  else
1774  {
1776  }
1777 
1778  void startClient(string name)
1779  {
1780 #if !DEBUG
1781  try
1782  {
1783 #endif
1784  GameMain.Client = new GameClient(name, endpoints, serverName, Option.None);
1785 #if !DEBUG
1786  }
1787  catch (Exception e)
1788  {
1789  DebugConsole.ThrowError("Failed to start the client", e);
1790  }
1791 #endif
1792  }
1793  }
1794 
1795  private static Color GetPingTextColor(int ping)
1796  {
1797  if (ping < 0) { return Color.DarkRed; }
1798  return ToolBox.GradientLerp(ping / 200.0f, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red);
1799  }
1800 
1801  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
1802  {
1803  graphics.Clear(Color.CornflowerBlue);
1804 
1805  spriteBatch.Begin(SpriteSortMode.Deferred, null, GUI.SamplerState, null, GameMain.ScissorTestEnable);
1806  GameMain.MainMenuScreen.DrawBackground(graphics, spriteBatch);
1807  GUI.Draw(Cam, spriteBatch);
1808  spriteBatch.End();
1809  }
1810 
1811  public override void AddToGUIUpdateList()
1812  {
1813  menu.AddToGUIUpdateList();
1814  }
1815 
1816  public void StoreServerFilters()
1817  {
1818  if (loadingServerFilters) { return; }
1819  foreach (KeyValuePair<Identifier, GUITickBox> filterBox in filterTickBoxes)
1820  {
1821  ServerListFilters.Instance.SetAttribute(filterBox.Key, filterBox.Value.Selected.ToString());
1822  }
1823  foreach (KeyValuePair<Identifier, GUIDropDown> ternaryFilter in ternaryFilters)
1824  {
1825  ServerListFilters.Instance.SetAttribute(ternaryFilter.Key, ternaryFilter.Value.SelectedData.ToString());
1826  }
1827  GameSettings.SaveCurrentConfig();
1828  }
1829 
1830  private bool loadingServerFilters;
1831  public void LoadServerFilters()
1832  {
1833  loadingServerFilters = true;
1834  XDocument currentConfigDoc = XMLExtensions.TryLoadXml(GameSettings.PlayerConfigPath);
1835  ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters"));
1836  foreach (KeyValuePair<Identifier, GUITickBox> filterBox in filterTickBoxes)
1837  {
1838  filterBox.Value.Selected =
1839  ServerListFilters.Instance.GetAttributeBool(filterBox.Key, filterBox.Value.Selected);
1840  }
1841  foreach (KeyValuePair<Identifier, GUIDropDown> ternaryFilter in ternaryFilters)
1842  {
1843  TernaryOption ternaryOption =
1844  ServerListFilters.Instance.GetAttributeEnum(
1845  ternaryFilter.Key,
1846  (TernaryOption)ternaryFilter.Value.SelectedData);
1847 
1848  var child = ternaryFilter.Value.ListBox.Content.GetChildByUserData(ternaryOption);
1849  ternaryFilter.Value.Select(ternaryFilter.Value.ListBox.Content.GetChildIndex(child));
1850  }
1851  loadingServerFilters = false;
1852  }
1853 
1854  }
1855 }
override bool Enabled
Definition: GUIButton.cs:27
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual Rectangle Rect
IEnumerable< GUIComponent > GetAllChildren()
Returns all child elements in the hierarchy.
Definition: GUIComponent.cs:49
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
override void ForceLayoutRecalculation()
Definition: GUIListBox.cs:874
override void ClearChildren()
Definition: GUIListBox.cs:1243
void Select(int forcedCaretIndex=-1, bool ignoreSelectSound=false)
Definition: GUITextBox.cs:377
override void Flash(Color? color=null, float flashDuration=1.5f, bool useRectangleFlash=false, bool useCircularFlash=false, Vector2? flashRectOffset=null)
Definition: GUITextBox.cs:411
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
Action ResolutionChanged
NOTE: Use very carefully. You need to ensure that you ALWAYS unsubscribe from this when you no longer...
Definition: GameMain.cs:133
static MainMenuScreen MainMenuScreen
Definition: GameMain.cs:53
static GameClient Client
Definition: GameMain.cs:188
static GameMain Instance
Definition: GameMain.cs:144
void DrawBackground(GraphicsDevice graphics, SpriteBatch spriteBatch)
static MultiplayerPreferences Instance
static Option< Endpoint > Parse(string str)
static Option< LidgrenEndpoint > ParseFromWithHostNameCheck(string endpointStr, bool tryParseHostName)
ImmutableArray< Endpoint > Endpoints
Definition: ServerInfo.cs:25
ImmutableArray< ServerListContentPackageInfo > ContentPackages
Definition: ServerInfo.cs:89
static Option< ServerInfo > FromXElement(XElement element)
Definition: ServerInfo.cs:528
override bool Equals(object? obj)
Definition: ServerInfo.cs:583
LanguageIdentifier Language
Definition: ServerInfo.cs:76
Vector2 RelativeSize
Relative to the parent rect.
IEnumerable< RectTransform > Children
bool RepositionChildInHierarchy(int index)
void SortChildren(Comparison< RectTransform > comparison)
int GetChildIndex(RectTransform rectT)
static ServerListFilters Instance
static void Init(XElement? elem)
bool GetAttributeBool(Identifier key, bool def)
void SetAttribute(Identifier key, string val)
void AddToRecentServers(ServerInfo info)
bool IsFavorite(ServerInfo info)
void AddToFavoriteServers(ServerInfo info)
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
void UpdateOrAddServerInfo(ServerInfo serverInfo)
override void AddToGUIUpdateList()
By default, submits the screen's main GUIFrame and, if requested upon construction,...
static void CreateFilterServerPrompt(ServerInfo info)
void JoinServer(ImmutableArray< Endpoint > endpoints, string serverName)
void RemoveFromFavoriteServers(ServerInfo info)
static void CreateReportPrompt(ServerInfo info)
override void Update(double deltaTime)
override async Task< string > GetSelfUserName()
static ? SocialOverlay Instance
static readonly LocalizedString ShortcutBindText
GUISoundType
Definition: GUI.cs:21
CursorState
Definition: GUI.cs:40
IReadOnlyList< ServerInfo > Servers
void AddOrUpdate(ServerInfo info)
Tab(TabEnum tabEnum, ServerListScreen serverListScreen, GUILayoutGroup tabber, string storage)
bool Remove(ServerInfo info)
bool Contains(ServerInfo info)