Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Screens/NetLobbyScreen.cs
3 using Barotrauma.Steam;
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.Globalization;
10 using System.Linq;
11 
12 namespace Barotrauma
13 {
14  partial class NetLobbyScreen : Screen
15  {
16  private GUIListBox chatBox;
17  private GUIButton serverLogReverseButton;
18  private GUIListBox serverLogBox, serverLogFilterTicks;
19 
20  private GUIComponent jobVariantTooltip;
21 
22  private GUIComponent playStyleIconContainer;
23 
24  private GUITextBox chatInput;
25  private GUITextBox serverLogFilter;
27  {
28  get
29  {
30  return chatInput;
31  }
32  }
33 
34  private GUIImage micIcon;
35 
36  private GUIScrollBar levelDifficultySlider;
37 
38  private readonly List<GUIComponent> traitorElements = new List<GUIComponent>();
39  private GUIScrollBar traitorProbabilitySlider;
40  private GUILayoutGroup traitorDangerGroup;
41 
42  public GUIFrame MissionTypeFrame { get; private set; }
43  public GUIFrame CampaignSetupFrame { get; private set; }
44  public GUIFrame CampaignFrame { get; private set; }
45 
46  public GUIButton QuitCampaignButton { get; private set; }
47 
48  private GUITickBox[] missionTypeTickBoxes;
49  private GUIListBox missionTypeList;
50 
51  public GUITextBox LevelSeedBox { get; private set; }
52 
53  private GUIButton joinOnGoingRoundButton;
57  private GUILayoutGroup roundControlsHolder;
58 
59  public GUIButton SettingsButton { get; private set; }
60  public GUIButton ServerMessageButton { get; private set; }
61  public static GUIButton JobInfoFrame { get; set; }
62 
63  private GUITickBox spectateBox;
64  public bool Spectating => spectateBox is { Selected: true, Visible: true };
65 
66  public bool PermadeathMode => GameMain.Client?.ServerSettings?.RespawnMode == RespawnMode.Permadeath;
67  public bool PermanentlyDead => campaignCharacterInfo?.PermanentlyDead ?? false;
68 
69  private GUILayoutGroup playerInfoContent;
70  private GUIComponent changesPendingText;
71  private bool createPendingChangesText = true;
72  public GUIButton PlayerFrame { get; private set; }
73 
74  public GUIButton SubVisibilityButton { get; private set; }
75 
76  private GUITextBox subSearchBox;
77 
78  private GUIComponent subPreviewContainer;
79 
80  private GUITickBox autoRestartBox;
81  private GUITextBlock autoRestartText;
82 
83  private GUITickBox shuttleTickBox;
84 
85  private Sprite backgroundSprite;
86 
87  private GUIButton jobPreferencesButton;
88  private GUIButton appearanceButton;
89 
90  private GUIFrame characterInfoFrame;
91  private GUIFrame appearanceFrame;
92 
93  private GUISelectionCarousel<RespawnMode> respawnModeSelection;
94  private GUITextBlock respawnModeLabel;
95  private GUIComponent respawnIntervalElement;
96 
97  private readonly List<GUIComponent> midRoundRespawnSettings = new List<GUIComponent>();
98  private readonly List<GUIComponent> permadeathEnabledRespawnSettings = new List<GUIComponent>();
99  private readonly List<GUIComponent> permadeathDisabledRespawnSettings = new List<GUIComponent>();
100  private readonly List<GUIComponent> ironmanDisabledRespawnSettings = new List<GUIComponent>();
101  private readonly List<GUIComponent> campaignDisabledElements = new List<GUIComponent>();
102 
104  public GUIFrame JobSelectionFrame { get; private set; }
105 
106  public GUIFrame JobPreferenceContainer { get; private set; }
107  public GUIListBox JobList { get; private set; }
108 
109  private Identifier micIconStyle;
110  private float micCheckTimer;
111  const float MicCheckInterval = 1.0f;
112 
113  private float autoRestartTimer;
114 
115  //persistent characterinfo provided by the server
116  //(character settings cannot be edited when this is set)
117  private CharacterInfo campaignCharacterInfo;
119  {
120  get;
121  set;
122  }
123 
127  private readonly List<GUIComponent> clientDisabledElements = new List<GUIComponent>();
128 
132  private readonly List<GUIComponent> clientHiddenElements = new List<GUIComponent>();
133 
134  private readonly List<GUIComponent> botSettingsElements = new List<GUIComponent>();
135 
136  private readonly Dictionary<GUIComponent, string> settingAssignedComponents = new Dictionary<GUIComponent, string>();
137 
138  public GUIComponent FileTransferFrame { get; private set; }
139  public GUITextBlock FileTransferTitle { get; private set; }
140  public GUIProgressBar FileTransferProgressBar { get; private set; }
141  public GUITextBlock FileTransferProgressText { get; private set; }
142 
143  public GUITickBox Favorite { get; private set; }
144 
145  public GUILayoutGroup LogButtons { get; private set; }
146 
150  private readonly List<GUIButton> chatPanelTabButtons = new List<GUIButton>();
151 
152  private GUITextBlock publicOrPrivateText, playstyleText;
153 
154  public GUIListBox SubList { get; private set; }
155  public GUIDropDown ShuttleList { get; private set; }
156  public GUIListBox ModeList { get; private set; }
157 
158  private int selectedModeIndex;
159  public int SelectedModeIndex
160  {
161  get { return selectedModeIndex; }
162  set
163  {
164  if (HighlightedModeIndex == selectedModeIndex)
165  {
166  ModeList.Select(value);
167  }
168  selectedModeIndex = value;
169  }
170  }
171 
173  {
174  get { return ModeList.SelectedIndex; }
175  set
176  {
177  ModeList.Select(value, GUIListBox.Force.Yes);
178  }
179  }
180 
181  //No, this should not be static even though your IDE might say so! There's a server-side version of this which needs to be an instance method.
182  public IReadOnlyList<SubmarineInfo> GetSubList()
183  => (IReadOnlyList<SubmarineInfo>)GameMain.Client?.ServerSubmarines
184  ?? Array.Empty<SubmarineInfo>();
185 
187 
188  public GUITextBox CharacterNameBox { get; private set; }
189 
190  public GUIListBox TeamPreferenceListBox { get; private set; }
191 
192  public GUIButton StartButton { get; private set; }
193 
194  public GUITickBox ReadyToStartBox { get; private set; }
195 
197 
199 
201 
202  public bool UsingShuttle
203  {
204  get { return shuttleTickBox.Selected && !PermadeathMode; }
205  set { shuttleTickBox.Selected = value; }
206  }
207 
209  {
210  get { return ModeList.SelectedData as GameModePreset; }
211  }
212 
214  {
215  get
216  {
217  MissionType retVal = MissionType.None;
218  int index = 0;
219  foreach (MissionType type in Enum.GetValues(typeof(MissionType)))
220  {
221  if (type == MissionType.None || type == MissionType.All) { continue; }
222 
223  if (missionTypeTickBoxes[index].Selected)
224  {
225  retVal = (MissionType)((int)retVal | (int)type);
226  }
227 
228  index++;
229  }
230 
231  return retVal;
232  }
233  set
234  {
235  int index = 0;
236  foreach (MissionType type in Enum.GetValues(typeof(MissionType)))
237  {
238  if (type == MissionType.None || type == MissionType.All) { continue; }
239  missionTypeTickBoxes[index].Selected = ((int)type & (int)value) != 0;
240  index++;
241  }
242  }
243  }
244 
245  public List<JobVariant> JobPreferences
246  {
247  get
248  {
249  // JobList if the server has already assigned the player a job
250  // (e.g. the player has a pre-existing campaign character)
251  if (JobList?.Content == null)
252  {
253  return new List<JobVariant>();
254  }
255 
256  List<JobVariant> jobPreferences = new List<JobVariant>();
257  foreach (GUIComponent child in JobList.Content.Children)
258  {
259  if (child.UserData is not JobVariant jobPrefab) { continue; }
260  jobPreferences.Add(jobPrefab);
261  }
262  return jobPreferences;
263  }
264  }
265 
266  public string LevelSeed
267  {
268  get
269  {
270  return levelSeed;
271  }
272  set
273  {
274  if (levelSeed == value) { return; }
275 
276  levelSeed = value;
277 
278  int intSeed = ToolBox.StringToInt(levelSeed);
279  backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed);
280  LevelSeedBox.Text = levelSeed;
281  }
282  }
283 
284  private const float MainPanelWidth = 0.7f;
285  private const float SidePanelWidth = 0.3f;
289  private const float PanelSpacing = 0.005f;
290 
294  private static int PanelBorderSize => GUI.IntScale(20);
295 
296  private static Point GetSizeWithoutBorder(GUIComponent parent) => new Point(parent.Rect.Width - PanelBorderSize * 2, parent.Rect.Height - PanelBorderSize * 2);
297 
298  public NetLobbyScreen()
299  {
300  var contentArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center), isHorizontal: false)
301  {
302  Stretch = true,
303  RelativeSpacing = PanelSpacing
304  };
305 
306  var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, contentArea.RectTransform, Anchor.Center), isHorizontal: true)
307  {
308  Stretch = true,
309  RelativeSpacing = PanelSpacing
310  };
311 
312  var mainPanel = new GUIFrame(new RectTransform(new Vector2(MainPanelWidth, 1.0f), horizontalLayout.RectTransform));
313 
314  var mainPanelLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width, mainPanel.Rect.Height - PanelBorderSize), mainPanel.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter)
315  {
316  Stretch = true,
317  //more spacing to more clearly separate the top and bottom
318  RelativeSpacing = PanelSpacing * 4
319  };
320 
321  GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), mainPanelLayout.RectTransform), isHorizontal: true)
322  {
323  Stretch = true,
324  RelativeSpacing = 0.025f
325  };
326  CreateServerInfoContents(serverInfoHolder);
327 
328  var mainPanelTopLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true)
329  {
330  Stretch = true,
331  RelativeSpacing = PanelSpacing
332  };
333 
334  var mainPanelBottomLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true)
335  {
336  Stretch = true,
337  RelativeSpacing = PanelSpacing
338  };
339 
340  //--------------------------------------------------------------------------------------------------------------------------------
341  //top panel (game mode, submarine)
342  //--------------------------------------------------------------------------------------------------------------------------------
343 
344  CreateGameModeDropdown(mainPanelTopLayout);
345  CreateSubmarineListPanel(mainPanelTopLayout);
346  CreateSubmarineInfoPanel(mainPanelTopLayout);
347 
348  //--------------------------------------------------------------------------------------------------------------------------------
349  //bottom panel (settings)
350  //--------------------------------------------------------------------------------------------------------------------------------
351 
352  CreateGameModePanel(mainPanelBottomLayout);
353  CreateGameModeSettingsPanel(mainPanelBottomLayout);
354  CreateGeneralSettingsPanel(mainPanelBottomLayout);
355  mainPanelBottomLayout.Recalculate();
356 
357  foreach (var child in mainPanelBottomLayout.GetAllChildren<GUIComponent>())
358  {
359  if (traitorDangerGroup.Children.Contains(child))
360  {
361  //don't touch the colors of the traitor danger indicators, they're intentionally very dim when disabled
362  continue;
363  }
364  //make the disabled colors slightly less dim (these should be readable, despite being non-interactable)
365  child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f);
366  if (child is GUITextBlock textBlock)
367  {
368  textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f);
369  }
370  }
371 
372  //--------------------------------------------------------------------------------------------------------------------------------
373  //right panel (Character customization/Chat)
374  //--------------------------------------------------------------------------------------------------------------------------------
375 
376  var sidePanel = new GUIFrame(new RectTransform(new Vector2(SidePanelWidth, 1.0f), horizontalLayout.RectTransform));
377  GUILayoutGroup sidePanelLayout = new GUILayoutGroup(new RectTransform(GetSizeWithoutBorder(sidePanel),
378  sidePanel.RectTransform, Anchor.Center))
379  {
380  RelativeSpacing = PanelSpacing * 4,
381  Stretch = true
382  };
383 
384  CreateSidePanelContents(sidePanelLayout);
385 
386  //--------------------------------------------------------------------------------------------------------------------------------
387  // bottom panel (start round, quit, transfers, ready to start...) ------------------------------------------------------------
388  //--------------------------------------------------------------------------------------------------------------------------------
389  GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), contentArea.RectTransform), childAnchor: Anchor.CenterLeft)
390  {
391  Stretch = true,
392  IsHorizontal = true,
393  RelativeSpacing = PanelSpacing
394  };
395  CreateBottomPanelContents(bottomBar);
396  }
397 
398  private void AssignComponentToServerSetting(GUIComponent component, string settingName)
399  {
400  settingAssignedComponents[component] = settingName;
401  }
402 
404  {
405  settingAssignedComponents.ForEach(kvp => GameMain.Client.ServerSettings.AssignGUIComponent(kvp.Value, kvp.Key));
406  }
407 
408  private void CreateServerInfoContents(GUIComponent parent)
409  {
410  GUIFrame serverInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null);
411  var serverBanner = new GUICustomComponent(new RectTransform(Vector2.One, serverInfoFrame.RectTransform), DrawServerBanner)
412  {
413  HideElementsOutsideFrame = true,
414  IgnoreLayoutGroups = true
415  };
416 
417  GUIFrame serverInfoContent = new GUIFrame(new RectTransform(new Vector2(0.98f, 0.9f), serverInfoFrame.RectTransform, Anchor.Center), style: null);
418 
419  var serverLabelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.05f), serverInfoContent.RectTransform), isHorizontal: true)
420  {
421  AbsoluteSpacing = GUI.IntScale(5)
422  };
423 
424  playstyleText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform),
425  "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader");
426  publicOrPrivateText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform),
427  "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader");
428 
429  var serverNameShadow = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(GUI.IntScale(3)) },
430  string.Empty, font: GUIStyle.LargeFont, textColor: Color.Black)
431  {
432  IgnoreLayoutGroups = true
433  };
434  var serverName = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft),
435  string.Empty, font: GUIStyle.LargeFont, textColor: GUIStyle.TextColorBright)
436  {
437  IgnoreLayoutGroups = true,
438  TextGetter = serverNameShadow.TextGetter = () => GameMain.Client?.ServerName
439  };
440 
441  ServerMessageButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.15f), serverInfoContent.RectTransform, Anchor.BottomLeft),
442  TextManager.Get("workshopitemdescription"), style: "GUIButtonSmall")
443  {
444  IgnoreLayoutGroups = true,
445  OnClicked = (bt, userdata) =>
446  {
447  if (GameMain.Client?.ServerSettings is { } serverSettings)
448  {
449  CreateServerMessagePopup(serverSettings.ServerName, serverSettings.ServerMessageText);
450  }
451  return true;
452  }
453  };
454 
455  playStyleIconContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), serverInfoContent.RectTransform, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.BottomRight)
456  {
457  AbsoluteSpacing = GUI.IntScale(5)
458  };
459 
460  Favorite = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight),
461  "", null, "GUIServerListFavoriteTickBox")
462  {
463  IgnoreLayoutGroups = true,
464  Selected = false,
465  ToolTip = TextManager.Get("addtofavorites"),
466  OnSelected = (tickbox) =>
467  {
468  if (GameMain.Client == null) { return true; }
469  ServerInfo info = GameMain.Client.CreateServerInfoFromSettings();
470  if (tickbox.Selected)
471  {
472  GameMain.ServerListScreen.AddToFavoriteServers(info);
473  }
474  else
475  {
476  GameMain.ServerListScreen.RemoveFromFavoriteServers(info);
477  }
478  tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites");
479  return true;
480  }
481  };
482 
483  SettingsButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.4f), serverInfoContent.RectTransform, Anchor.TopRight),
484  TextManager.Get("ServerSettingsButton"), style: "GUIButtonFreeScale");
485  }
486 
487  private void CreateServerMessagePopup(string serverName, string message)
488  {
489  if (string.IsNullOrEmpty(message)) { return; }
490  var popup = new GUIMessageBox(serverName, string.Empty, minSize: new Point(GUI.IntScale(650), GUI.IntScale(650)));
491  //popup.Content.Stretch = true;
492  popup.Header.Font = GUIStyle.LargeFont;
493  popup.Header.RectTransform.MinSize = new Point(0, (int)popup.Header.TextSize.Y);
494  var textListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), popup.Content.RectTransform));
495  var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textListBox.Content.RectTransform), message, wrap: true)
496  {
497  CanBeFocused = false
498  };
499  text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y);
500  }
501 
502  public void RefreshPlaystyleIcons()
503  {
504  playStyleIconContainer?.ClearChildren();
505  if (GameMain.Client?.ClientPeer?.ServerConnection is not { } serverConnection || serverConnection.Endpoint == null) { return; }
506  var serverInfo = ServerInfo.FromServerEndpoints(serverConnection.Endpoint.ToEnumerable().ToImmutableArray(), GameMain.Client.ServerSettings);
507 
508  var playStyleTags = serverInfo.GetPlayStyleTags();
509  foreach (var tag in playStyleTags)
510  {
511  var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}")
512  ?.GetSprite(GUIComponent.ComponentState.None);
513  if (playStyleIcon is null) { continue; }
514 
515  new GUIImage(new RectTransform(Vector2.One, playStyleIconContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight),
516  playStyleIcon, scaleToFit: true)
517  {
518  ToolTip = TextManager.Get($"servertagdescription.{tag}"),
519  Color = Color.White
520  };
521  }
522  }
523 
524  private void CreateGameModeDropdown(GUIComponent parent)
525  {
526  //------------------------------------------------------------------------------------------------------------------
527  // Gamemode panel
528  //------------------------------------------------------------------------------------------------------------------
529 
530  GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform))
531  {
532  Stretch = true,
533  RelativeSpacing = 0.005f
534  };
535 
536  var modeLabel = CreateSubHeader("GameMode", gameModeHolder);
537  var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight),
538  TextManager.Get("Votes"), textAlignment: Alignment.CenterRight)
539  {
540  UserData = "modevotes",
541  Visible = false
542  };
543  ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform))
544  {
545  PlaySoundOnSelect = true,
546  OnSelected = VotableClicked
547  };
548 
549  foreach (GameModePreset mode in GameModePreset.List)
550  {
551  if (mode.IsSinglePlayer) { continue; }
552 
553  var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), ModeList.Content.RectTransform), style: null)
554  {
555  UserData = mode
556  };
557 
558  var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.76f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight))
559  {
560  AbsoluteSpacing = GUI.IntScale(5),
561  Stretch = true
562  };
563 
564  var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont);
565  modeTitle.RectTransform.NonScaledSize = new Point(int.MaxValue, (int)modeTitle.TextSize.Y);
566  modeTitle.RectTransform.IsFixedSize = true;
567  var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true);
568  //leave some padding for the vote count text
569  modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W);
570  modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent;
571  modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor;
572  modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f;
573  modeFrame.OnAddedToGUIUpdateList = (c) =>
574  {
575  modeTitle.State = modeDescription.State = c.State;
576  };
577  modeDescription.RectTransform.SizeChanged += () =>
578  {
579  modeDescription.RectTransform.NonScaledSize = new Point(modeDescription.Rect.Width, (int)modeDescription.TextSize.Y);
580  modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y));
581  };
582 
583  new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) },
584  style: "GameModeIcon." + mode.Identifier, scaleToFit: true);
585  }
586  }
587 
588  private void CreateSubmarineListPanel(GUIComponent parent)
589  {
590  var submarineListHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform))
591  {
592  Stretch = true,
593  RelativeSpacing = 0.005f
594  };
595 
596  var subLabel = CreateSubHeader("Submarine", submarineListHolder);
597  SubVisibilityButton
598  = new GUIButton(
599  new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight,
600  scaleBasis: ScaleBasis.BothHeight)
601  { AbsoluteOffset = new Point(0, GUI.IntScale(5)) },
602  style: "EyeButton")
603  {
604  OnClicked = (button, o) =>
605  {
606  CreateSubmarineVisibilityMenu();
607  return false;
608  }
609  };
610  clientHiddenElements.Add(SubVisibilityButton);
611 
612  var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), submarineListHolder.RectTransform), isHorizontal: true)
613  {
614  Stretch = true
615  };
616  var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font);
617  subSearchBox = new GUITextBox(new RectTransform(Vector2.One, filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true);
618  filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize;
619  subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; };
620  subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; };
621  subSearchBox.OnTextChanged += (textBox, text) =>
622  {
623  UpdateSubVisibility();
624  return true;
625  };
626 
627  SubList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.93f), submarineListHolder.RectTransform))
628  {
629  PlaySoundOnSelect = true,
630  OnSelected = VotableClicked
631  };
632 
633  var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight),
634  TextManager.Get("Votes"), textAlignment: Alignment.CenterRight)
635  {
636  UserData = "subvotes",
637  Visible = false,
638  CanBeFocused = false
639  };
640  }
641 
642  private void CreateSubmarineInfoPanel(GUIComponent parent)
643  {
644  var submarineInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center))
645  {
646  Stretch = true,
647  RelativeSpacing = 0.005f
648  };
649  //submarine preview ------------------------------------------------------------------
650 
651  subPreviewContainer = new GUIFrame(new RectTransform(Vector2.One, submarineInfoHolder.RectTransform), style: null);
652  subPreviewContainer.RectTransform.SizeChanged += () =>
653  {
654  if (SelectedSub != null) { CreateSubPreview(SelectedSub); }
655  };
656  }
657 
658  private GUIComponent CreateGameModePanel(GUIComponent parent)
659  {
660  var gameModeSpecificFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null);
661  CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null)
662  {
663  Visible = false
664  };
665  CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null)
666  {
667  Visible = false
668  };
669  GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center))
670  {
671  RelativeSpacing = 0.05f,
672  Stretch = true
673  };
674  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform),
675  TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center);
676 
677  QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform),
678  TextManager.Get("quitbutton"), textAlignment: Alignment.Center)
679  {
680  OnClicked = (_, __) =>
681  {
682  if (GameMain.Client == null) { return false; }
683  if (GameMain.Client.GameStarted)
684  {
685  GameMain.Client.RequestRoundEnd(save: false);
686  }
687  else
688  {
689  GameMain.Client.RequestRoundEnd(save: false, quitCampaign: true);
690  }
691  return true;
692  }
693  };
694 
695  //mission type ------------------------------------------------------------------
696  MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null);
697 
698  GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform))
699  {
700  Stretch = true
701  };
702 
703  CreateSubHeader("MissionType", missionHolder);
704  missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform))
705  {
706  OnSelected = (component, obj) =>
707  {
708  return false;
709  }
710  };
711  clientDisabledElements.Add(missionTypeList);
712 
713  var missionTypes = (MissionType[])Enum.GetValues(typeof(MissionType));
714  missionTypeTickBoxes = new GUITickBox[missionTypes.Length - 2];
715  int index = 0;
716  for (int i = 0; i < missionTypes.Length; i++)
717  {
718  var missionType = missionTypes[i];
719  if (missionType == MissionType.None || missionType == MissionType.All) { continue; }
720 
721  GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null)
722  {
723  UserData = missionType,
724  };
725 
726  if (MissionPrefab.HiddenMissionClasses.Contains(missionType))
727  {
728  missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), string.Empty)
729  {
730  UserData = (int)missionType,
731  Visible = false,
732  CanBeFocused = false
733  };
734  }
735  else
736  {
737  missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform),
738  TextManager.Get("MissionType." + missionType.ToString()))
739  {
740  UserData = (int)missionType,
741  ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()),
742  OnSelected = (tickbox) =>
743  {
744  int missionTypeOr = tickbox.Selected ? (int)tickbox.UserData : (int)MissionType.None;
745  int missionTypeAnd = (int)MissionType.All & (!tickbox.Selected ? (~(int)tickbox.UserData) : (int)MissionType.All);
746  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, (int)missionTypeOr, (int)missionTypeAnd);
747  return true;
748  }
749  };
750  frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize;
751  }
752  index++;
753  }
754  clientDisabledElements.AddRange(missionTypeTickBoxes);
755 
756  return gameModeSpecificFrame;
757  }
758 
759  private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent)
760  {
761  //------------------------------------------------------------------
762  // settings panel
763  //------------------------------------------------------------------
764 
765  GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform))
766  {
767  Stretch = true
768  };
769  CreateSubHeader("GameModeSettings", settingsLayout);
770 
771  var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content;
772 
773  //seed ------------------------------------------------------------------
774 
775  var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), TextManager.Get("LevelSeed"))
776  {
777  CanBeFocused = false
778  };
779  LevelSeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight));
780  LevelSeedBox.OnDeselected += (textBox, key) =>
781  {
782  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed);
783  };
784  campaignDisabledElements.Add(LevelSeedBox);
785  campaignDisabledElements.Add(seedLabel);
786  clientDisabledElements.Add(LevelSeedBox);
787  clientDisabledElements.Add(seedLabel);
788  LevelSeed = ToolBox.RandomSeed(8);
789 
790  //level difficulty ------------------------------------------------------------------
791 
792  var levelDifficultyHolder = CreateLabeledSlider(settingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel,
793  step: 0.01f, range: new Vector2(0.0f, 100.0f));
794  levelDifficultySlider.OnReleased = (scrollbar, value) =>
795  {
796  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
797  return true;
798  };
799  levelDifficultySlider.OnMoved = (scrollbar, value) =>
800  {
801  if (!EventManagerSettings.Prefabs.Any()) { return true; }
802  difficultySliderLabel.Text =
803  EventManagerSettings.GetByDifficultyPercentile(value).Name
804  + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})";
805  difficultySliderLabel.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red);
806  return true;
807  };
808  AssignComponentToServerSetting(levelDifficultySlider, nameof(ServerSettings.SelectedLevelDifficulty));
809  campaignDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren());
810  clientDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren());
811 
812  //bot count ------------------------------------------------------------------
813  CreateSubHeader("BotSettings", settingsContent);
814 
815  var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
816  new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true);
817  var botCountSelection = new GUISelectionCarousel<int>(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform));
818  for (int i = 0; i <= NetConfig.MaxPlayers; i++)
819  {
820  botCountSelection.AddElement(i, i.ToString());
821  }
822  AssignComponentToServerSetting(botCountSelection, nameof(ServerSettings.BotCount));
823  clientDisabledElements.AddRange(botCountSettingHolder.GetAllChildren());
824  botSettingsElements.Add(botCountSelection);
825 
826  var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
827  new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true);
828  var botSpawnModeSelection = new GUISelectionCarousel<BotSpawnMode>(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform));
829  foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast<BotSpawnMode>())
830  {
831  botSpawnModeSelection.AddElement(botSpawnMode, botSpawnMode.ToString(), TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip"));
832  }
833  botSpawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
834  AssignComponentToServerSetting(botSpawnModeSelection, nameof(ServerSettings.BotSpawnMode));
835  clientDisabledElements.AddRange(botSpawnModeSettingHolder.GetAllChildren());
836  botSettingsElements.Add(botSpawnModeSelection);
837 
838  botCountSelection.OnValueChanged += (_) =>
839  {
840  botSpawnModeSelection.Enabled = GameMain.Client.ServerSettings.BotCount > 0;
841  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
842  };
843 
844  //traitor probability ------------------------------------------------------------------
845 
846  CreateSubHeader("TraitorSettings", settingsContent);
847 
848  //spacing
849  new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), settingsContent.RectTransform), style: null);
850 
851  //the probability slider is a traitor element, but we don't add it to traitorElements
852  //because we don't want to disable it when sliding it to 0 (need to be able to slide it back!)
853  var traitorProbabilityHolder = CreateLabeledSlider(settingsContent, "traitor.probability", "", "traitor.probability.tooltip",
854  out traitorProbabilitySlider, out var traitorProbabilityText,
855  step: 0.01f, range: new Vector2(0.0f, 1.0f));
856  traitorProbabilitySlider.OnMoved = (scrollbar, value) =>
857  {
858  traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString());
859  traitorProbabilityText.TextColor =
860  value <= 0.0f ?
861  GUIStyle.Green :
862  ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red);
863  RefreshEnabledElements();
864  return true;
865  };
866  traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll);
867  traitorProbabilitySlider.OnReleased += (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; };
868  AssignComponentToServerSetting(traitorProbabilitySlider, nameof(ServerSettings.TraitorProbability));
869  traitorElements.Clear();
870  clientDisabledElements.AddRange(traitorProbabilityHolder.GetAllChildren());
871 
872  var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
873  {
874  Stretch = true
875  };
876 
877  var dangerLevelLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true)
878  {
879  ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip")
880  };
881 
882  var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true };
883  var traitorDangerButtons = new GUIButton[2];
884  traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft")
885  {
886  OnClicked = (button, obj) =>
887  {
888  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1);
889  return true;
890  }
891  };
892 
893  traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
894  {
895  Stretch = true,
896  AbsoluteSpacing = 1
897  };
898  for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++)
899  {
900  var difficultyColor = Mission.GetDifficultyColor(i);
901  new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true)
902  {
903  ToolTip =
904  RichString.Rich(
905  $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' +
906  TextManager.Get($"traitor.dangerlevel.{i}.description")),
907  Color = difficultyColor,
908  DisabledColor = Color.Gray * 0.5f,
909  };
910  }
911 
912  traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight")
913  {
914  OnClicked = (button, obj) =>
915  {
916  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1);
917  return true;
918  }
919  };
920 
921  traitorDangerContainer.InheritTotalChildrenMinHeight();
922  SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel);
923  traitorElements.Add(dangerLevelLabel);
924  traitorElements.AddRange(traitorDangerGroup.Children);
925  traitorElements.AddRange(traitorDangerButtons);
926 
927  var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
928  new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsMinPlayerCountHolder.RectTransform), TextManager.Get("ServerSettingsTraitorsMinPlayerCount"), wrap: true)
929  {
930  ToolTip = TextManager.Get("ServerSettingsTraitorsMinPlayerCountToolTip")
931  };
932  var traitorsMinPlayerCount = new GUISelectionCarousel<int>(new RectTransform(new Vector2(0.5f, 1.0f), traitorsMinPlayerCountHolder.RectTransform));
933  for (int i = 1; i <= NetConfig.MaxPlayers; i++)
934  {
935  traitorsMinPlayerCount.AddElement(i, i.ToString());
936  }
937  traitorsMinPlayerCount.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
938  AssignComponentToServerSetting(traitorsMinPlayerCount, nameof(ServerSettings.TraitorsMinPlayerCount));
939  traitorElements.AddRange(traitorsMinPlayerCountHolder.Children);
940 
941  foreach (var traitorElement in traitorElements)
942  {
943  if (!clientDisabledElements.Contains(traitorElement))
944  {
945  clientDisabledElements.Add(traitorElement);
946  }
947  }
948 
949  return settingsContent;
950  }
951 
952  private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent)
953  {
954  //------------------------------------------------------------------
955  // settings panel
956  //------------------------------------------------------------------
957 
958  GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform))
959  {
960  Stretch = true
961  };
962  var respawnSettingsHeader = CreateSubHeader("RespawnSettings", settingsLayout);
963 
964  var settingsContent = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)).Content;
965 
966  // ------------------------------------------------------------------
967 
968  var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
969  respawnModeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), respawnModeHolder.RectTransform), TextManager.Get("RespawnMode"), wrap: true);
970  respawnModeSelection = new GUISelectionCarousel<RespawnMode>(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform));
971  foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast<RespawnMode>())
972  {
973  respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip"));
974  }
975 
976  respawnModeSelection.ElementSelectionCondition += (value) => value != RespawnMode.Permadeath || SelectedMode == GameModePreset.MultiPlayerCampaign;
977  respawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
978  AssignComponentToServerSetting(respawnModeSelection, nameof(ServerSettings.RespawnMode));
979 
980  GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true)
981  {
982  Stretch = true
983  };
984 
985  shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle"))
986  {
987  ToolTip = TextManager.Get("RespawnShuttleExplanation"),
988  Selected = !PermadeathMode,
989  OnSelected = (GUITickBox box) =>
990  {
991  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
992  return true;
993  }
994  };
995  AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle));
996  midRoundRespawnSettings.Add(shuttleTickBox);
997 
998  shuttleTickBox.TextBlock.RectTransform.SizeChanged += () =>
999  {
1000  shuttleTickBox.TextBlock.AutoScaleHorizontal = true;
1001  shuttleTickBox.TextBlock.TextScale = 1.0f;
1002  if (shuttleTickBox.TextBlock.TextScale < 0.75f)
1003  {
1004  shuttleTickBox.TextBlock.Wrap = true;
1005  shuttleTickBox.TextBlock.AutoScaleHorizontal = true;
1006  shuttleTickBox.TextBlock.TextScale = 1.0f;
1007  }
1008  };
1009  ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10)
1010  {
1011  OnSelected = (component, obj) =>
1012  {
1013  GameMain.Client?.RequestSelectSub(obj as SubmarineInfo, isShuttle: true);
1014  return true;
1015  }
1016  };
1017  ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0);
1018  shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y));
1019  midRoundRespawnSettings.Add(ShuttleList);
1020 
1021  respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel,
1022  range: new Vector2(10.0f, 600.0f));
1023  LocalizedString intervalLabel = respawnIntervalSliderLabel.Text;
1024  respawnIntervalSlider.StepValue = 10.0f;
1025  respawnIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
1026  {
1027  GUITextBlock text = scrollBar.UserData as GUITextBlock;
1028  text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue);
1029  return true;
1030  };
1031  respawnIntervalSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1032  {
1033  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1034  return true;
1035  };
1036  respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll);
1037  AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval));
1038 
1039  var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel,
1040  step: 0.1f, range: new Vector2(0.0f, 1.0f));
1041  minRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
1042  {
1043  GUITextBlock text = scrollBar.UserData as GUITextBlock;
1044  text.Text = ToolBox.GetFormattedPercentage(scrollBar.BarScrollValue);
1045  return true;
1046  };
1047  minRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1048  {
1049  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1050  return true;
1051  };
1052  minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll);
1053  midRoundRespawnSettings.AddRange(minRespawnElement.GetAllChildren());
1054  AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio));
1055 
1056  var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel,
1057  step: 0.1f, range: new Vector2(60.0f, 660.0f));
1058  respawnDurationSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
1059  {
1060  GUITextBlock text = scrollBar.UserData as GUITextBlock;
1061  text.Text = scrollBar.BarScrollValue <= 0 ? TextManager.Get("Unlimited") : ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue);
1062  return true;
1063  };
1064  respawnDurationSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1065  {
1066  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1067  return true;
1068  };
1069  respawnDurationSlider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) =>
1070  {
1071  return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X;
1072  };
1073  respawnDurationSlider.ValueToScroll = (GUIScrollBar scrollBar, float value) =>
1074  {
1075  return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X);
1076  };
1077  respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll);
1078  midRoundRespawnSettings.AddRange(respawnDurationElement.GetAllChildren());
1079  AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime));
1080 
1081  var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip",
1082  out var skillLossSlider, out var skillLossSliderLabel, range: new Vector2(0, 100));
1083  skillLossSlider.StepValue = 1;
1084  skillLossSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
1085  {
1086  GUITextBlock text = scrollBar.UserData as GUITextBlock;
1087  text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString());
1088  return true;
1089  };
1090  skillLossSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1091  {
1092  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1093  return true;
1094  };
1095  permadeathDisabledRespawnSettings.AddRange(skillLossElement.GetAllChildren());
1096  clientDisabledElements.AddRange(skillLossElement.GetAllChildren());
1097  AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath));
1098  skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll);
1099 
1100  var skillLossImmediateRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnImmediateRespawn", "", "ServerSettingsSkillLossPercentageOnImmediateRespawnToolTip",
1101  out var skillLossImmediateRespawnSlider, out var skillLossImmediateRespawnSliderLabel, range: new Vector2(0, 100));
1102  skillLossImmediateRespawnSlider.StepValue = 1;
1103  skillLossImmediateRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
1104  {
1105  GUITextBlock text = scrollBar.UserData as GUITextBlock;
1106  text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString());
1107  return true;
1108  };
1109  skillLossImmediateRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1110  {
1111  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1112  return true;
1113  };
1114  midRoundRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
1115  permadeathDisabledRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren());
1116  AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn));
1117  skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll);
1118 
1119  var newCharacterCostSliderElement = CreateLabeledSlider(settingsContent,
1120  "ServerSettings.ReplaceCostPercentage", "", "ServerSettings.ReplaceCostPercentage.tooltip",
1121  out var newCharacterCostSlider, out var newCharacterCostSliderLabel,
1122  range: new Vector2(0, 200), step: 10f);
1123  newCharacterCostSlider.StepValue = 10f;
1124  newCharacterCostSlider.OnMoved = (GUIScrollBar scrollBar, float _) =>
1125  {
1126  GUITextBlock textBlock = scrollBar.UserData as GUITextBlock;
1127  int currentMultiplier = (int)Math.Round(scrollBar.BarScrollValue);
1128  if (currentMultiplier < 1)
1129  {
1130  textBlock.Text = TextManager.Get("ServerSettings.ReplaceCostPercentage.Free");
1131  }
1132  else
1133  {
1134  textBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", currentMultiplier.ToString());
1135  }
1136  return true;
1137  };
1138  newCharacterCostSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) =>
1139  {
1140  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1141  return true;
1142  };
1143  clientDisabledElements.AddRange(newCharacterCostSliderElement.GetAllChildren());
1144  permadeathEnabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren());
1145  ironmanDisabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren());
1146  AssignComponentToServerSetting(newCharacterCostSlider, nameof(ServerSettings.ReplaceCostPercentage));
1147  newCharacterCostSlider.OnMoved(newCharacterCostSlider, newCharacterCostSlider.BarScroll); // initialize
1148 
1149  var allowBotTakeoverTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("AllowBotTakeover"))
1150  {
1151  ToolTip = TextManager.Get("AllowBotTakeover.Tooltip"),
1152  Selected = GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath,
1153  OnSelected = (GUITickBox box) =>
1154  {
1155  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1156  return true;
1157  }
1158  };
1159  AssignComponentToServerSetting(allowBotTakeoverTickbox, nameof(ServerSettings.AllowBotTakeoverOnPermadeath));
1160  permadeathEnabledRespawnSettings.Add(allowBotTakeoverTickbox);
1161  ironmanDisabledRespawnSettings.Add(allowBotTakeoverTickbox);
1162  clientDisabledElements.Add(allowBotTakeoverTickbox);
1163 
1164  var ironmanTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("IronmanMode").ToUpper())
1165  {
1166  ToolTip = TextManager.Get("IronmanMode.Tooltip"),
1167  Selected = GameMain.Client != null && GameMain.Client.ServerSettings.IronmanMode,
1168  OnSelected = (GUITickBox box) =>
1169  {
1170  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1171  return true;
1172  }
1173  };
1174  AssignComponentToServerSetting(ironmanTickbox, nameof(ServerSettings.IronmanMode));
1175  permadeathEnabledRespawnSettings.Add(ironmanTickbox);
1176  clientDisabledElements.Add(ironmanTickbox);
1177 
1178  foreach (var respawnElement in midRoundRespawnSettings)
1179  {
1180  if (!clientDisabledElements.Contains(respawnElement))
1181  {
1182  clientDisabledElements.Add(respawnElement);
1183  }
1184  }
1185 
1186  return settingsContent;
1187  }
1188 
1189  public static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag = null)
1190  {
1191  var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), parent.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) },
1192  TextManager.Get(textTag), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft, textColor: GUIStyle.TextColorBright)
1193  {
1194  CanBeFocused = false
1195  };
1196  if (!toolTipTag.IsNullOrEmpty())
1197  {
1198  header.ToolTip = TextManager.Get(toolTipTag);
1199  }
1200  return header;
1201  }
1202 
1203  public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null)
1204  {
1205  GUILayoutGroup verticalLayout = null;
1206  if (!headerTag.IsNullOrEmpty())
1207  {
1208  verticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), isHorizontal: false)
1209  {
1210  Stretch = true
1211  };
1212  var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform),
1213  TextManager.Get(headerTag), textAlignment: Alignment.CenterLeft)
1214  {
1215  CanBeFocused = false
1216  };
1217  header.RectTransform.MinSize = new Point(0, (int)header.TextSize.Y);
1218  }
1219 
1220  var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, headerTag == null ? 0.0f : 0.5f), (verticalLayout ?? parent).RectTransform), isHorizontal: true)
1221  {
1222  Stretch = true,
1223  RelativeSpacing = 0.05f
1224  };
1225 
1226  //spacing
1227  new GUIFrame(new RectTransform(new Point(GUI.IntScale(5), 0), container.RectTransform), style: null);
1228 
1229  slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider");
1230  if (step.HasValue) { slider.Step = step.Value; }
1231  if (range.HasValue) { slider.Range = range.Value; }
1232 
1233  container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y);
1234  container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y);
1235  verticalLayout?.InheritTotalChildrenMinHeight();
1236 
1237  label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform, Anchor.CenterRight),
1238  string.IsNullOrEmpty(valueLabelTag) ? "" : TextManager.Get(valueLabelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont)
1239  {
1240  CanBeFocused = false
1241  };
1242 
1243  //slider has a reference to the label to change the text when it's used
1244  slider.UserData = label;
1245 
1246  slider.ToolTip = label.ToolTip = TextManager.Get(tooltipTag);
1247  return verticalLayout ?? container;
1248  }
1249 
1250  public static GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null, GUIFont font = null)
1251  {
1252  var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true)
1253  {
1254  Stretch = true,
1255  RelativeSpacing = 0.05f,
1256  ToolTip = TextManager.Get(labelTag)
1257  };
1258 
1259  var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform),
1260  TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: font)
1261  {
1262  AutoScaleHorizontal = true
1263  };
1264  if (!string.IsNullOrEmpty(toolTipTag))
1265  {
1266  label.ToolTip = TextManager.Get(toolTipTag);
1267  }
1268  var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int)
1269  {
1270  MinValueInt = min,
1271  MaxValueInt = max
1272  };
1273 
1274  container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y);
1275  container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y);
1276 
1277  return input;
1278  }
1279 
1280  public static GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null)
1281  {
1282  var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true)
1283  {
1284  Stretch = true,
1285  RelativeSpacing = 0.05f,
1286  ToolTip = TextManager.Get(labelTag)
1287  };
1288 
1289  var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform),
1290  TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft)
1291  {
1292  AutoScaleHorizontal = true
1293  };
1294  if (!string.IsNullOrEmpty(toolTipTag))
1295  {
1296  label.ToolTip = TextManager.Get(toolTipTag);
1297  }
1298  var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements);
1299 
1300  container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y);
1301  container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y);
1302 
1303  return input;
1304  }
1305 
1306  private void CreateSidePanelContents(GUIComponent rightPanel)
1307  {
1308  //player info panel ------------------------------------------------------------
1309 
1310  var myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), rightPanel.RectTransform), style: null);
1311  var myCharacterContent = new GUILayoutGroup(new RectTransform(new Vector2(1), myCharacterFrame.RectTransform, Anchor.Center))
1312  {
1313  Stretch = true
1314  };
1315 
1316  spectateBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), myCharacterContent.RectTransform),
1317  TextManager.Get("spectatebutton"))
1318  {
1319  Selected = false,
1320  OnSelected = ToggleSpectate,
1321  UserData = "spectate"
1322  };
1323 
1324  playerInfoContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), myCharacterContent.RectTransform))
1325  {
1326  Stretch = true
1327  };
1328 
1329  // Social area
1330 
1331  GUIFrame logFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightPanel.RectTransform), style: null);
1332  GUILayoutGroup logContents = new GUILayoutGroup(new RectTransform(Vector2.One, logFrame.RectTransform, Anchor.Center))
1333  {
1334  Stretch = true
1335  };
1336 
1337  GUILayoutGroup socialHolder = null; GUILayoutGroup serverLogHolder = null;
1338 
1339  LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logContents.RectTransform), true)
1340  {
1341  Stretch = true,
1342  RelativeSpacing = 0.02f
1343  };
1344 
1345  clientHiddenElements.Add(LogButtons);
1346 
1347  // Show chat button
1348  chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform),
1349  TextManager.Get("Chat"), style: "GUITabButton")
1350  {
1351  Selected = true,
1352  OnClicked = (GUIButton button, object userData) =>
1353  {
1354  if (socialHolder != null) { socialHolder.Visible = true; }
1355  if (serverLogHolder != null) { serverLogHolder.Visible = false; }
1356  chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button);
1357  return true;
1358  }
1359  });
1360 
1361  // Server log button
1362  chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform),
1363  TextManager.Get("ServerLog"), style: "GUITabButton")
1364  {
1365  OnClicked = (GUIButton button, object userData) =>
1366  {
1367  if (socialHolder != null) { socialHolder.Visible = false; }
1368  if (serverLogHolder is { Visible: false })
1369  {
1370  if (GameMain.Client?.ServerSettings?.ServerLog == null) { return false; }
1371  serverLogHolder.Visible = true;
1372  GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter);
1373  }
1374  chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button);
1375  return true;
1376  }
1377  });
1378 
1379  GUITextBlock.AutoScaleAndNormalize(chatPanelTabButtons.Select(btn => btn.TextBlock));
1380 
1381  GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logContents.RectTransform), style: null)
1382  {
1383  CanBeFocused = false
1384  };
1385 
1386  socialHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center))
1387  {
1388  Stretch = true
1389  };
1390 
1391  // Spacing
1392  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null)
1393  {
1394  CanBeFocused = false
1395  };
1396 
1397  GUILayoutGroup socialHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), socialHolder.RectTransform), isHorizontal: true)
1398  {
1399  Stretch = true
1400  };
1401 
1402  //chatbox ----------------------------------------------------------------------
1403 
1404  chatBox = new GUIListBox(new RectTransform(new Vector2(0.6f, 1.0f), socialHolderHorizontal.RectTransform));
1405 
1406  //player list ------------------------------------------------------------------
1407 
1408  PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform))
1409  {
1410  PlaySoundOnSelect = true,
1411  OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; }
1412  };
1413 
1414  // Spacing
1415  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null)
1416  {
1417  CanBeFocused = false
1418  };
1419 
1420  // Chat input
1421 
1422  var chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform),
1423  isHorizontal: true, childAnchor: Anchor.CenterLeft)
1424  {
1425  Stretch = true
1426  };
1427 
1428  chatInput = new GUITextBox(new RectTransform(new Vector2(0.95f, 1.0f), chatRow.RectTransform))
1429  {
1430  MaxTextLength = ChatMessage.MaxLength,
1431  Font = GUIStyle.SmallFont,
1432  DeselectAfterMessage = false
1433  };
1434 
1435  micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable");
1436 
1437  serverLogHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center))
1438  {
1439  Stretch = true,
1440  Visible = false
1441  };
1442 
1443  // Spacing
1444  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null)
1445  {
1446  CanBeFocused = false
1447  };
1448 
1449  GUILayoutGroup serverLogHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), serverLogHolder.RectTransform), isHorizontal: true)
1450  {
1451  Stretch = true
1452  };
1453 
1454  //server log ----------------------------------------------------------------------
1455 
1456  GUILayoutGroup serverLogListboxLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform))
1457  {
1458  Stretch = true
1459  };
1460 
1461  serverLogReverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), serverLogListboxLayout.RectTransform), style: "UIToggleButtonVertical");
1462  serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform));
1463 
1464  //filter tickbox list ------------------------------------------------------------------
1465 
1466  serverLogFilterTicks = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform) { MinSize = new Point(150, 0) })
1467  {
1468  OnSelected = (component, userdata) => { return false; }
1469  };
1470 
1471  // Spacing
1472  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null)
1473  {
1474  CanBeFocused = false
1475  };
1476 
1477  // Filter text input
1478 
1479  serverLogFilter = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.07f), serverLogHolder.RectTransform))
1480  {
1481  MaxTextLength = ChatMessage.MaxLength,
1482  Font = GUIStyle.SmallFont
1483  };
1484 
1485  }
1486 
1487  private void CreateBottomPanelContents(GUIComponent bottomBar)
1488  {
1489  //bottom panel ------------------------------------------------------------
1490 
1491  GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft)
1492  {
1493  Stretch = true,
1494  IsHorizontal = true,
1495  RelativeSpacing = PanelSpacing
1496  };
1497  GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft)
1498  {
1499  Stretch = true,
1500  IsHorizontal = true,
1501  RelativeSpacing = PanelSpacing
1502  };
1503  GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft)
1504  {
1505  Stretch = true,
1506  IsHorizontal = true,
1507  RelativeSpacing = PanelSpacing
1508  };
1509 
1510  var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect"))
1511  {
1512  OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; }
1513  };
1514  disconnectButton.TextBlock.AutoScaleHorizontal = true;
1515 
1516 
1517  // file transfers ------------------------------------------------------------
1518  FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame");
1519  var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center))
1520  {
1521  Stretch = true,
1522  RelativeSpacing = 0.05f
1523  };
1524  FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont);
1525  var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
1526  {
1527  Stretch = true
1528  };
1529  FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen);
1530  FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "",
1531  font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
1532  new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall")
1533  {
1534  OnClicked = (btn, userdata) =>
1535  {
1536  if (FileTransferFrame.UserData is not FileReceiver.FileTransferIn transfer) { return false; }
1537  GameMain.Client?.CancelFileTransfer(transfer);
1538  GameMain.Client?.FileReceiver.StopTransfer(transfer);
1539  return true;
1540  }
1541  };
1542 
1543 
1544  roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform),
1545  isHorizontal: true, childAnchor: Anchor.CenterLeft)
1546  {
1547  Stretch = true
1548  };
1549 
1550  GUIFrame readyToStartContainer = new GUIFrame(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), style: "TextFrame")
1551  {
1552  Visible = false
1553  };
1554 
1555  // Ready to start tickbox
1556  ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), readyToStartContainer.RectTransform, anchor: Anchor.Center),
1557  TextManager.Get("ReadyToStartTickBox"));
1558 
1559  joinOnGoingRoundButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform),
1560  TextManager.Get("ServerListJoin"));
1561 
1562  // Start button
1563  StartButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform),
1564  TextManager.Get("StartGameButton"))
1565  {
1566  OnClicked = (btn, obj) =>
1567  {
1568  if (GameMain.Client == null) { return true; }
1569  if (CampaignSetupFrame.Visible && CampaignSetupUI != null)
1570  {
1571  CampaignSetupUI.StartGameClicked(btn, obj);
1572  }
1573  else
1574  {
1575  //if a campaign is active, and we're not setting one up atm, start button continues the existing campaign
1576  GameMain.Client.RequestStartRound(continueCampaign: GameMain.GameSession?.GameMode is CampaignMode && CampaignSetupFrame is not { Visible: true });
1577  CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound");
1578  }
1579  return true;
1580  }
1581  };
1582  clientHiddenElements.Add(StartButton);
1583  bottomBar.RectTransform.MinSize =
1584  new Point(0, (int)Math.Max(ReadyToStartBox.RectTransform.MinSize.Y / 0.75f, StartButton.RectTransform.MinSize.Y));
1585 
1586  //autorestart ------------------------------------------------------------------
1587 
1588  autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUIStyle.SmallFont, style: "TextFrame", textAlignment: Alignment.Center);
1589  GUIFrame autoRestartBoxContainer = new GUIFrame(new RectTransform(Vector2.One, bottomBarMid.RectTransform), style: "TextFrame");
1590  autoRestartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), autoRestartBoxContainer.RectTransform, Anchor.Center), TextManager.Get("AutoRestart"))
1591  {
1592  OnSelected = (tickBox) =>
1593  {
1594  GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties);
1595  return true;
1596  }
1597  };
1598  clientDisabledElements.Add(autoRestartBox);
1599  AssignComponentToServerSetting(autoRestartBox, nameof(ServerSettings.AutoRestart));
1600 
1601  }
1602 
1604  {
1605  CoroutineManager.StopCoroutines("WaitForStartRound");
1606  if (StartButton != null)
1607  {
1608  StartButton.Enabled = true;
1609  }
1610  GUI.ClearCursorWait();
1611  }
1612 
1613  public static IEnumerable<CoroutineStatus> WaitForStartRound(GUIButton startButton)
1614  {
1615  GUI.SetCursorWaiting();
1616  LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait");
1617  var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty<LocalizedString>());
1618 
1619  if (startButton != null)
1620  {
1621  startButton.Enabled = false;
1622  }
1623 
1624  DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10);
1625  while (Selected == GameMain.NetLobbyScreen && DateTime.Now < timeOut)
1626  {
1627  msgBox.Header.Text = headerText + new string('.', ((int)Timing.TotalTime % 3 + 1));
1628  yield return CoroutineStatus.Running;
1629  }
1630 
1631  msgBox.Close();
1632  if (startButton != null)
1633  {
1634  startButton.Enabled = true;
1635  }
1636  GUI.ClearCursorWait();
1637  yield return CoroutineStatus.Success;
1638  }
1639 
1640  public override void Deselect()
1641  {
1642  SaveAppearance();
1643  chatInput.Deselect();
1644  CampaignCharacterDiscarded = false;
1645 
1646  CharacterAppearanceCustomizationMenu?.Dispose();
1647  JobSelectionFrame = null;
1648  }
1649 
1650  public override void Select()
1651  {
1652  if (GameMain.NetworkMember == null) { return; }
1653 
1654  visibilityMenuOrder.Clear();
1655 
1656  CharacterAppearanceCustomizationMenu?.Dispose();
1657  JobSelectionFrame = null;
1658 
1659  Character.Controlled = null;
1660  GameMain.LightManager.LosEnabled = false;
1661  GUI.PreventPauseMenuToggle = false;
1662  CampaignCharacterDiscarded = false;
1663 
1664  changesPendingText?.Parent?.RemoveChild(changesPendingText);
1665  changesPendingText = null;
1666 
1667  chatInput.Select();
1668  chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage;
1669  chatInput.OnTextChanged += GameMain.Client.TypingChatMessage;
1670  chatInput.OnDeselected += (sender, key) =>
1671  {
1673  };
1674 
1675  //disable/hide elements the clients are not supposed to use/see
1676  clientDisabledElements.ForEach(c => c.Enabled = false);
1677  clientHiddenElements.ForEach(c => c.Visible = false);
1678 
1679  RefreshEnabledElements();
1680 
1681  if (GameMain.Client != null)
1682  {
1684  joinOnGoingRoundButton.Visible = GameMain.Client.GameStarted;
1685  ReadyToStartBox.Selected = false;
1686  GameMain.Client.SetReadyToStart(ReadyToStartBox);
1687  }
1688  else
1689  {
1690  joinOnGoingRoundButton.Visible = false;
1691  }
1692  SetSpectate(spectateBox.Selected);
1693 
1694  if (GameMain.Client != null)
1695  {
1697  joinOnGoingRoundButton.OnClicked = GameMain.Client.JoinOnGoingClicked;
1698  ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart;
1699  }
1700 
1701  roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible);
1702  roundControlsHolder.Recalculate();
1703 
1704  AssignComponentsToServerSettings();
1705 
1706  RefreshPlaystyleIcons();
1707 
1708  base.Select();
1709  }
1710 
1712  {
1713  if (GameMain.Client == null) { return; }
1714  var client = GameMain.Client;
1715  var settings = client.ServerSettings;
1716  bool manageSettings = HasPermission(ClientPermissions.ManageSettings);
1717  bool campaignSelected = CampaignFrame.Visible || CampaignSetupFrame.Visible;
1718  bool campaignStarted = CampaignFrame.Visible;
1719  bool gameStarted = client != null && client.GameStarted;
1720 
1721  // First, enable or disable elements based on client permissions
1722  foreach (var element in clientDisabledElements)
1723  {
1724  element.Enabled = manageSettings;
1725  }
1726 
1727  // Then disable elements depending on other conditions
1728  traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0);
1729  SetTraitorDangerIndicators(settings.TraitorDangerLevel);
1730  respawnModeSelection.Enabled = respawnModeLabel.Enabled = manageSettings && !gameStarted;
1731  midRoundRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.MidRound);
1732  permadeathDisabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode != RespawnMode.Permadeath);
1733  permadeathEnabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.Permadeath && !gameStarted);
1734  ironmanDisabledRespawnSettings.ForEach(e => e.Enabled &= !settings.IronmanMode);
1735 
1736  // The respawn interval is used even if the shuttle is not
1737  respawnIntervalElement.GetAllChildren().ForEach(e => e.Enabled = settings.RespawnMode != RespawnMode.BetweenRounds && manageSettings);
1738 
1739  //go through the individual elements that are only enabled in a specific context
1740  shuttleTickBox.Enabled &= !gameStarted;
1741  if (ShuttleList != null)
1742  {
1743  // Shuttle list depends on shuttle tickbox
1744  ShuttleList.Enabled &= shuttleTickBox.Enabled && HasPermission(ClientPermissions.SelectSub);
1745  ShuttleList.ButtonEnabled = ShuttleList.Enabled;
1746  }
1747  if (SubList != null)
1748  {
1749  SubList.Enabled = !campaignStarted && (settings.AllowSubVoting || HasPermission(ClientPermissions.SelectSub));
1750  }
1751  if (ModeList != null)
1752  {
1753  ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode));
1754  }
1755 
1756  RefreshStartButtonVisibility();
1757 
1758  botSettingsElements.ForEach(b => b.Enabled = !campaignStarted && manageSettings);
1759 
1760  campaignDisabledElements.ForEach(e => e.Enabled = !campaignSelected && manageSettings);
1761  levelDifficultySlider.ToolTip = levelDifficultySlider.Enabled ? string.Empty : TextManager.Get("campaigndifficultydisabled");
1762 
1763  //hide elements the client shouldn't
1764  foreach (var element in clientHiddenElements)
1765  {
1766  element.Visible = manageSettings;
1767  }
1768  //go through the individual elements that are only visible in a specific context
1769  ReadyToStartBox.Parent.Visible = !gameStarted;
1770  LogButtons.Visible = HasPermission(ClientPermissions.ServerLog);
1771 
1772  client?.UpdateLogButtonPermissions();
1773  roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible);
1774  roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One);
1775  roundControlsHolder.Recalculate();
1776 
1777  SettingsButton.OnClicked = settings.ToggleSettingsFrame;
1778 
1779  RefreshGameModeContent();
1780 
1781  static bool HasPermission(ClientPermissions permissions)
1782  {
1783  if (GameMain.Client == null) { return false; }
1784  return GameMain.Client.HasPermission(permissions);
1785  }
1786  }
1787 
1788  public void ShowSpectateButton()
1789  {
1790  if (GameMain.Client == null) { return; }
1791  joinOnGoingRoundButton.Visible = true;
1792  joinOnGoingRoundButton.Enabled = true;
1793  StartButton.Visible = false;
1794  }
1795 
1796  public void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo)
1797  {
1798  if (newCampaignCharacterInfo != null)
1799  {
1800  if (CampaignCharacterDiscarded) { return; }
1801  if (campaignCharacterInfo != newCampaignCharacterInfo)
1802  {
1803  campaignCharacterInfo = newCampaignCharacterInfo;
1804  UpdatePlayerFrame(campaignCharacterInfo, false);
1805  }
1806  }
1807  else if (campaignCharacterInfo != null)
1808  {
1809  campaignCharacterInfo = null;
1810  UpdatePlayerFrame(null, false);
1811  }
1812  }
1813 
1814  private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing = true)
1815  {
1816  UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContent);
1817  }
1818 
1819  public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false)
1820  {
1821  if (GameMain.Client == null) { return; }
1822  UpdatePlayerFrame(
1824  allowEditing: alwaysAllowEditing || campaignCharacterInfo == null,
1825  parent: parent,
1826  createPendingText: createPendingText);
1827  }
1828 
1829  private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = true)
1830  {
1831  if (GameMain.Client == null) { return; }
1832 
1833  // When permanently dead and still characterless, spectating is the only option
1834  spectateBox.Enabled = !PermanentlyDead;
1835 
1836  createPendingChangesText = createPendingText;
1837  if (characterInfo == null || CampaignCharacterDiscarded)
1838  {
1839  characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null);
1840  characterInfo.RecreateHead(MultiplayerPreferences.Instance);
1841  GameMain.Client.CharacterInfo = characterInfo;
1842  characterInfo.OmitJobInMenus = true;
1843  }
1844 
1845  parent.ClearChildren();
1846 
1847  bool isGameRunning = GameMain.GameSession?.IsRunning ?? false;
1848 
1849  parent.ClearChildren();
1850  parent.UserData = characterInfo;
1851 
1852  bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName;
1853  changesPendingText?.Parent?.RemoveChild(changesPendingText);
1854  changesPendingText = null;
1855 
1856  if (TabMenu.PendingChanges)
1857  {
1858  CreateChangesPendingText();
1859  }
1860 
1861  CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), parent.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center)
1862  {
1863  MaxTextLength = Client.MaxNameLength,
1864  OverflowClip = true
1865  };
1866 
1867  if (!allowEditing ||
1868  (PermanentlyDead && !characterInfo.RenamingEnabled))
1869  {
1870  CharacterNameBox.Readonly = true;
1871  CharacterNameBox.Enabled = false;
1872  }
1873  else
1874  {
1875  CharacterNameBox.OnEnterPressed += (tb, text) =>
1876  {
1877  CharacterNameBox.Deselect();
1878  return true;
1879  };
1880  CharacterNameBox.OnDeselected += (tb, key) =>
1881  {
1882  if (GameMain.Client == null)
1883  {
1884  return;
1885  }
1886 
1887  string newName = Client.SanitizeName(tb.Text);
1888  if (newName == GameMain.Client.Name) { return; }
1889  if (string.IsNullOrWhiteSpace(newName))
1890  {
1891  tb.Text = GameMain.Client.Name;
1892  }
1893  else
1894  {
1895  if (isGameRunning)
1896  {
1897  GameMain.Client.PendingName = tb.Text;
1898  TabMenu.PendingChanges = true;
1899  if (createPendingText)
1900  {
1901  CreateChangesPendingText();
1902  }
1903  }
1904  else
1905  {
1906  ReadyToStartBox.Selected = false;
1907  }
1908 
1909  GameMain.Client.SetName(tb.Text);
1910  }
1911  };
1912  }
1913 
1914  //spacing
1915  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null);
1916 
1917  if (allowEditing && (!PermadeathMode || !isGameRunning))
1918  {
1919  GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true)
1920  {
1921  Stretch = true,
1922  RelativeSpacing = 0.02f
1923  };
1924 
1925  jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform),
1926  TextManager.Get("JobPreferences"), style: "GUITabButton")
1927  {
1928  Selected = true,
1929  OnClicked = SelectJobPreferencesTab
1930  };
1931  appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform),
1932  TextManager.Get("CharacterAppearance"), style: "GUITabButton")
1933  {
1934  OnClicked = SelectAppearanceTab
1935  };
1936 
1937  GUITextBlock.AutoScaleAndNormalize(jobPreferencesButton.TextBlock, appearanceButton.TextBlock);
1938 
1939  // Unsubscribe from previous events, not even sure if this matters here but it doesn't hurt so why not
1940  if (characterInfoFrame != null) { characterInfoFrame.RectTransform.SizeChanged -= RecalculateSubDescription; }
1941  characterInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null);
1942  characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription;
1943 
1944  JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform),
1945  style: "GUIFrameListBox");
1946  characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0f, 0.025f) });
1947  JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true)
1948  {
1949  Enabled = true,
1950  PlaySoundOnSelect = true,
1951  OnSelected = (child, obj) =>
1952  {
1953  if (child.IsParentOf(GUI.MouseOn)) { return false; }
1954  return OpenJobSelection(child, obj);
1955  }
1956  };
1957 
1958  for (int i = 0; i < 3; i++)
1959  {
1960  JobVariant jobPrefab = null;
1961  while (i < MultiplayerPreferences.Instance.JobPreferences.Count)
1962  {
1963  var jobPreference = MultiplayerPreferences.Instance.JobPreferences[i];
1964  if (!JobPrefab.Prefabs.TryGet(jobPreference.JobIdentifier, out JobPrefab prefab) || prefab.HiddenJob)
1965  {
1966  MultiplayerPreferences.Instance.JobPreferences.RemoveAt(i);
1967  continue;
1968  }
1969  // The old job variant system used one-based indexing
1970  // so let's make sure no one get to pick a variant which doesn't exist
1971  int variant = Math.Min(jobPreference.Variant, prefab.Variants - 1);
1972  jobPrefab = new JobVariant(prefab, variant);
1973  break;
1974  }
1975 
1976  var slot = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), JobList.Content.RectTransform), style: "ListBoxElementSquare")
1977  {
1978  CanBeFocused = true,
1979  UserData = jobPrefab
1980  };
1981  }
1982 
1983  UpdateJobPreferences(characterInfo);
1984 
1985  appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox")
1986  {
1987  Visible = false,
1988  Color = Color.White
1989  };
1990  }
1991  else
1992  {
1993  characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter));
1994 
1995  if (PermanentlyDead)
1996  {
1997  new GUITextBlock(
1998  new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
1999  TextManager.Get("deceased"),
2000  textAlignment: Alignment.Center, font: GUIStyle.LargeFont);
2001 
2002  if (GameMain.Client?.ServerSettings is { IronmanMode: true })
2003  {
2004  new GUITextBlock(
2005  new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
2006  TextManager.Get("lobby.ironmaninfo"),
2007  textAlignment: Alignment.Center, wrap: true);
2008  }
2009  else
2010  {
2011  new GUITextBlock(
2012  new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
2013  TextManager.Get("lobby.permadeathinfo"),
2014  textAlignment: Alignment.Center, wrap: true);
2015  new GUITextBlock(
2016  new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
2017  TextManager.Get("lobby.permadeathoptionsexplanation"),
2018  textAlignment: Alignment.Center, wrap: true);
2019  }
2020  }
2021  else
2022  {
2023  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true)
2024  {
2025  HoverColor = Color.Transparent,
2026  SelectedColor = Color.Transparent
2027  };
2028 
2029  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont);
2030  foreach (Skill skill in characterInfo.Job.GetSkills())
2031  {
2032  Color textColor = Color.White * (0.5f + skill.Level / 200.0f);
2033  var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform),
2034  " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()),
2035  textColor,
2036  font: GUIStyle.SmallFont);
2037  }
2038  }
2039 
2040  // Spacing
2041  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null);
2042 
2043  if (GameMain.Client?.ServerSettings?.RespawnMode != RespawnMode.Permadeath)
2044  {
2045  // Button to create new character
2046  new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew"))
2047  {
2048  IgnoreLayoutGroups = true,
2049  OnClicked = (btn, userdata) =>
2050  {
2051  TryDiscardCampaignCharacter(() =>
2052  {
2053  UpdatePlayerFrame(null, true, parent);
2054  });
2055  return true;
2056  }
2057  };
2058  }
2059  }
2060 
2061  TeamPreferenceListBox = null;
2062  if (SelectedMode == GameModePreset.PvP)
2063  {
2064  TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), parent.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null)
2065  {
2066  Enabled = true,
2067  KeepSpaceForScrollBar = false,
2068  PlaySoundOnSelect = true,
2069  ScrollBarEnabled = false,
2070  ScrollBarVisible = false
2071  };
2072  TeamPreferenceListBox.RectTransform.MinSize = new Point(0, GUI.IntScale(30));
2073  TeamPreferenceListBox.UpdateDimensions();
2074 
2075  Color team1Color = new Color(0, 110, 150, 255);
2076  var team1Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team1"), textAlignment: Alignment.Center, style: null)
2077  {
2078  UserData = CharacterTeamType.Team1,
2079  CanBeFocused = true,
2080  Padding = Vector4.One * 10.0f * GUI.Scale,
2081  Color = Color.Lerp(team1Color, Color.Black, 0.7f) * 0.7f,
2082  HoverColor = team1Color * 0.95f,
2083  SelectedColor = team1Color * 0.8f,
2084  OutlineColor = team1Color,
2085  TextColor = Color.White,
2086  HoverTextColor = Color.White,
2087  SelectedTextColor = Color.White
2088  };
2089 
2090  Color noPreferenceColor = new Color(100, 100, 100, 255);
2091  var noPreferenceOption = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.nopreference"), textAlignment: Alignment.Center, style: null)
2092  {
2093  UserData = CharacterTeamType.None,
2094  CanBeFocused = true,
2095  Padding = Vector4.One * 10.0f * GUI.Scale,
2096  Color = Color.Lerp(noPreferenceColor, Color.Black, 0.7f) * 0.7f,
2097  HoverColor = noPreferenceColor * 0.95f,
2098  SelectedColor = noPreferenceColor * 0.8f,
2099  OutlineColor = noPreferenceColor,
2100  TextColor = Color.White,
2101  HoverTextColor = Color.White,
2102  SelectedTextColor = Color.White
2103  };
2104 
2105  Color team2Color = new Color(150, 110, 0, 255);
2106  var team2Option = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team2"), textAlignment: Alignment.Center, style: null)
2107  {
2108  UserData = CharacterTeamType.Team2,
2109  CanBeFocused = true,
2110  Padding = Vector4.One * 10.0f * GUI.Scale,
2111  Color = Color.Lerp(team2Color, Color.Black, 0.7f) * 0.7f,
2112  HoverColor = team2Color * 0.95f,
2113  SelectedColor = team2Color * 0.8f,
2114  OutlineColor = team2Color,
2115  TextColor = Color.White,
2116  HoverTextColor = Color.White,
2117  SelectedTextColor = Color.White
2118  };
2119 
2120  TeamPreferenceListBox.Select(MultiplayerPreferences.Instance.TeamPreference);
2121 
2122  TeamPreferenceListBox.OnSelected += (component, obj) =>
2123  {
2124  if ((CharacterTeamType)obj == MultiplayerPreferences.Instance.TeamPreference) { return true; }
2125 
2126  MultiplayerPreferences.Instance.TeamPreference = (CharacterTeamType)obj;
2127  GameMain.Client.ForceNameAndJobUpdate();
2128  GameSettings.SaveCurrentConfig();
2129 
2130  return true;
2131  };
2132  }
2133  }
2134 
2135  public void TryDiscardCampaignCharacter(Action onYes)
2136  {
2137  var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"),
2138  new[] { TextManager.Get("Yes"), TextManager.Get("No") });
2139  confirmation.Buttons[0].OnClicked += confirmation.Close;
2140  confirmation.Buttons[0].OnClicked += (btn2, userdata2) =>
2141  {
2142  CampaignCharacterDiscarded = true;
2143  campaignCharacterInfo = null;
2144  onYes();
2145  return true;
2146  };
2147  confirmation.Buttons[1].OnClicked += confirmation.Close;
2148  }
2149 
2150  private void CreateChangesPendingText()
2151  {
2152  if (!createPendingChangesText || changesPendingText != null || playerInfoContent == null) { return; }
2153 
2154  changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), playerInfoContent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) },
2155  style: "OuterGlow")
2156  {
2157  Color = Color.Black,
2158  IgnoreLayoutGroups = true
2159  };
2160  var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center),
2161  TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null);
2162  changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f));
2163  }
2164 
2165  public static void CreateChangesPendingFrame(GUIComponent parent)
2166  {
2167  parent.ClearChildren();
2168  GUIFrame changesPendingFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center),
2169  style: "OuterGlow")
2170  {
2171  Color = Color.Black
2172  };
2173  new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center),
2174  TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null)
2175  {
2176  AutoScaleHorizontal = true
2177  };
2178  }
2179 
2180  private void CreateJobVariantTooltip(JobPrefab jobPrefab, int variant, GUIComponent parentSlot)
2181  {
2182  jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight),
2183  style: "GUIToolTip")
2184  {
2185  UserData = new JobVariant(jobPrefab, variant)
2186  };
2187  jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y);
2188 
2189  var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), jobVariantTooltip.RectTransform, Anchor.Center))
2190  {
2191  Stretch = true,
2192  AbsoluteSpacing = (int)(15 * GUI.Scale)
2193  };
2194 
2195  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center);
2196 
2197  var itemIdentifiers = jobPrefab.PreviewItems[variant]
2198  .Where(it => it.ShowPreview)
2199  .Select(it => it.ItemIdentifier)
2200  .Distinct();
2201 
2202  int itemsPerRow = 5;
2203  int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1);
2204 
2205  new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter),
2206  onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), itemsPerRow); });
2207 
2208  jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing));
2209  }
2210 
2211  private void SetTraitorDangerIndicators(int dangerLevel)
2212  {
2213  int i = 0;
2214  foreach (var child in traitorDangerGroup.Children)
2215  {
2216  child.Enabled = i < dangerLevel && GameMain.Client?.ServerSettings is { TraitorProbability: > 0 };
2217  i++;
2218  }
2219  }
2220 
2221  public bool ToggleSpectate(GUITickBox tickBox)
2222  {
2223  SetSpectate(tickBox.Selected);
2224  return false;
2225  }
2226 
2227  public void SetSpectate(bool spectate)
2228  {
2229  if (GameMain.Client == null) { return; }
2230  spectateBox.Selected = spectate;
2231 
2232  if (spectate)
2233  {
2235  GameMain.Client.CharacterInfo = null;
2236  // TODO: The following lines are ancient, unexplained, and they cause a client spectating because of permadeath
2237  // to get kicked from the server at round transition because the server expects to be in control of
2238  // removing Characters and the client to still have one. Commenting these lines out for now, but
2239  // if no side-effects occur, they can just be deleted.
2240  //GameMain.Client.Character?.Remove();
2241  //GameMain.Client.Character = null;
2242 
2243  playerInfoContent.ClearChildren();
2244 
2245  new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center),
2246  TextManager.Get("PlayingAsSpectator"),
2247  textAlignment: Alignment.Center);
2248  }
2249  else
2250  {
2251  UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null);
2252  }
2253  }
2254 
2255  public void SetAllowSpectating(bool allowSpectating)
2256  {
2257  // Server owner is allowed to spectate regardless of the server settings
2258  if (GameMain.Client != null && GameMain.Client.IsServerOwner) { return; }
2259 
2260  // A client whose character has faced permadeath and hasn't chosen a new
2261  // character yet has no choice but to spectate
2262  if (campaignCharacterInfo != null && campaignCharacterInfo.PermanentlyDead) { return; }
2263 
2264  // Show the player config menu if spectating is not allowed
2265  if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; }
2266 
2267  // Hide spectate tickbox if spectating is not allowed
2268  spectateBox.Visible = allowSpectating;
2269  }
2270 
2271  public void SetAutoRestart(bool enabled, float timer = 0.0f)
2272  {
2273  autoRestartBox.Selected = enabled;
2274  autoRestartTimer = timer;
2275  }
2276 
2277  public void SetMissionType(MissionType missionType)
2278  {
2279  MissionType = missionType;
2280  }
2281 
2282  public void UpdateSubList(GUIComponent subList, IEnumerable<SubmarineInfo> submarines)
2283  {
2284  if (subList == null) { return; }
2285 
2286  subList.ClearChildren();
2287 
2288  foreach (SubmarineInfo sub in submarines)
2289  {
2290  AddSubmarine(subList, sub);
2291  }
2292  }
2293 
2294  private void AddSubmarine(GUIComponent subList, SubmarineInfo sub)
2295  {
2296  if (subList is GUIListBox listBox)
2297  {
2298  subList = listBox.Content;
2299  }
2300  else if (subList is GUIDropDown dropDown)
2301  {
2302  subList = dropDown.ListBox.Content;
2303  }
2304 
2305  var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform)
2306  {
2307  //enough space for 2 lines (price and class) + some padding
2308  MinSize = new Point(0, (int)(GUIStyle.SmallFont.LineHeight * 2.3f))
2309  },
2310  style: "ListBoxElement")
2311  {
2312  ToolTip = sub.Description,
2313  UserData = sub
2314  };
2315 
2316  int buttonSize = (int)(frame.Rect.Height * 0.8f);
2317  var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), frame.RectTransform, Anchor.CenterLeft),
2318  ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft)
2319  {
2320  CanBeFocused = false
2321  };
2322 
2323  var matchingSub =
2324  SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation) ??
2325  SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name);
2326 
2327  if (matchingSub == null)
2328  {
2329  subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f);
2330  frame.ToolTip = TextManager.Get("SubNotFound");
2331  }
2332  else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.StringRepresentation != sub.MD5Hash?.StringRepresentation)
2333  {
2334  subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f);
2335  frame.ToolTip = TextManager.Get("SubDoesntMatch");
2336  }
2337  else
2338  {
2339  if (subList == ShuttleList || subList == ShuttleList.ListBox || subList == ShuttleList.ListBox.Content)
2340  {
2341  subTextBlock.TextColor = new Color(subTextBlock.TextColor, sub.HasTag(SubmarineTag.Shuttle) ? 1.0f : 0.6f);
2342  }
2343  }
2344 
2346  {
2347  subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f);
2348  frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.ToolTip.SanitizedString;
2349  }
2350 
2351  CreateSubmarineClassText(
2352  frame,
2353  sub,
2354  subTextBlock,
2355  subList);
2356  }
2357 
2358  private void CreateSubmarineClassText(
2359  GUIComponent parent,
2360  SubmarineInfo sub,
2361  GUITextBlock subTextBlock,
2362  GUIComponent subList)
2363  {
2364  if (sub.HasTag(SubmarineTag.Shuttle))
2365  {
2366  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) },
2367  TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont)
2368  {
2369  TextColor = subTextBlock.TextColor * 0.8f,
2370  ToolTip = subTextBlock.ToolTip?.SanitizedString,
2371  CanBeFocused = false
2372  };
2373  //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended)
2374  if (subList == SubList.Content)
2375  {
2376  subTextBlock.TextColor *= 0.8f;
2377  foreach (GUIComponent child in parent.Children)
2378  {
2379  child.Color *= 0.8f;
2380  }
2381  }
2382  }
2383  else
2384  {
2385  var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, isHorizontal: false);
2386  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform),
2387  TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont)
2388  {
2389  UserData = "pricetext",
2390  TextColor = subTextBlock.TextColor * 0.8f,
2391  CanBeFocused = false
2392  };
2393  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform),
2394  TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.TopRight, font: GUIStyle.SmallFont)
2395  {
2396  UserData = "classtext",
2397  TextColor = subTextBlock.TextColor * 0.8f,
2398  ToolTip = subTextBlock.ToolTip,
2399  CanBeFocused = false
2400  };
2401  }
2402  }
2403 
2404  public bool VotableClicked(GUIComponent component, object userData)
2405  {
2406  if (GameMain.Client == null) { return false; }
2407 
2408  VoteType voteType;
2409  if (component.Parent == GameMain.NetLobbyScreen.SubList.Content)
2410  {
2411  if (!GameMain.Client.ServerSettings.AllowSubVoting)
2412  {
2413  var selectedSub = component.UserData as SubmarineInfo;
2414  if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null)
2415  {
2416  if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney)
2417  {
2418  new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive"));
2419  }
2420  if (!selectedSub.IsCampaignCompatible)
2421  {
2422  new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubincompatible"));
2423  }
2424  }
2425  if (!selectedSub.RequiredContentPackagesInstalled)
2426  {
2427  var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"),
2428  selectedSub.RequiredContentPackages.Any() ?
2429  TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)) :
2430  TextManager.Get("ContentPackageMismatchWarningGeneric"),
2431  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
2432 
2433  msgBox.Buttons[0].OnClicked = msgBox.Close;
2434  msgBox.Buttons[0].OnClicked += (button, obj) =>
2435  {
2436  GameMain.Client.RequestSelectSub(obj as SubmarineInfo, isShuttle: false);
2437  return true;
2438  };
2439  msgBox.Buttons[1].OnClicked = msgBox.Close;
2440  return false;
2441  }
2442  else if (GameMain.Client.HasPermission(ClientPermissions.SelectSub))
2443  {
2444  GameMain.Client.RequestSelectSub(selectedSub, isShuttle: false);
2445  return true;
2446  }
2447  return false;
2448  }
2449  if (component.UserData is SubmarineInfo sub)
2450  {
2451  CreateSubPreview(sub);
2452  }
2453  voteType = VoteType.Sub;
2454  }
2455  else if (component.Parent == GameMain.NetLobbyScreen.ModeList.Content)
2456  {
2457  if (!GameMain.Client.ServerSettings.AllowModeVoting)
2458  {
2460  {
2461  Identifier presetName = ((GameModePreset)component.UserData).Identifier;
2462 
2463  //display a verification prompt when switching away from the campaign
2464  if (HighlightedModeIndex == SelectedModeIndex &&
2467  {
2468  var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
2469  verificationBox.Buttons[0].OnClicked += (btn, userdata) =>
2470  {
2471  GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component));
2472  HighlightMode(SelectedModeIndex);
2473  verificationBox.Close(btn, userdata);
2474  return true;
2475  };
2476  verificationBox.Buttons[1].OnClicked = verificationBox.Close;
2477  return false;
2478  }
2479  GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component));
2480  HighlightMode(SelectedModeIndex);
2481 
2482  if (presetName == "multiplayercampaign")
2483  {
2484  GUI.SetCursorWaiting(endCondition: () =>
2485  {
2486  return CampaignFrame.Visible || CampaignSetupFrame.Visible;
2487  });
2488  }
2489 
2490  return presetName != "multiplayercampaign";
2491  }
2492  return false;
2493  }
2494  else if (!((GameModePreset)userData).Votable)
2495  {
2496  return false;
2497  }
2498 
2499  voteType = VoteType.Mode;
2500  }
2501  else
2502  {
2503  return false;
2504  }
2505 
2506  GameMain.Client.Vote(voteType, userData);
2507 
2508  return true;
2509  }
2510 
2511  public void AddPlayer(Client client)
2512  {
2513  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), PlayerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) },
2514  client.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, style: null)
2515  {
2516  Padding = Vector4.One * 10.0f * GUI.Scale,
2517  Color = Color.White * 0.25f,
2518  HoverColor = Color.White * 0.5f,
2519  SelectedColor = Color.White * 0.85f,
2520  OutlineColor = Color.White * 0.5f,
2521  TextColor = Color.White,
2522  SelectedTextColor = Color.Black,
2523  UserData = client
2524  };
2525  var soundIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) },
2526  sprite: GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true)
2527  {
2528  UserData = new Pair<string, float>("soundicon", 0.0f),
2529  CanBeFocused = false,
2530  Visible = true,
2531  OverrideState = GUIComponent.ComponentState.None,
2532  HoverColor = Color.White
2533  };
2534 
2535  var soundIconDisabled = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) },
2536  "GUISoundIconDisabled")
2537  {
2538  UserData = "soundicondisabled",
2539  CanBeFocused = true,
2540  Visible = false,
2541  OverrideState = GUIComponent.ComponentState.None,
2542  HoverColor = Color.White
2543  };
2544 
2545  var readyTick = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.6f), textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, style: "GUIReadyToStart")
2546  {
2547  Visible = false,
2548  CanBeFocused = false,
2549  ToolTip = TextManager.Get("ReadyToStartTickBox"),
2550  UserData = "clientready"
2551  };
2552 
2553  var downloadingThrobber = new GUICustomComponent(
2554  new RectTransform(Vector2.One, textBlock.RectTransform, scaleBasis: ScaleBasis.BothHeight),
2555  onUpdate: null,
2556  onDraw: DrawDownloadThrobber(client, soundIcon, soundIconDisabled, readyTick));
2557  }
2558 
2559  private Action<SpriteBatch, GUICustomComponent> DrawDownloadThrobber(Client client, params GUIComponent[] otherComponents)
2560  => (sb, c) => DrawDownloadThrobber(client, otherComponents, sb, c); //poor man's currying
2561 
2562  private static void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component)
2563  {
2564  if (!client.IsDownloading)
2565  {
2566  component.ToolTip = "";
2567  return;
2568  }
2569 
2570  component.HideElementsOutsideFrame = false;
2571  int drawRectX = otherComponents.Where(c => c.Visible)
2572  .Select(c => c.Rect)
2573  .Concat(new Rectangle(component.Parent.Rect.Right, component.Parent.Rect.Y, 0, component.Parent.Rect.Height).ToEnumerable())
2574  .Min(r => r.X) - component.Parent.Rect.Height - 10;
2575  Rectangle drawRect
2576  = new Rectangle(drawRectX, component.Rect.Y, component.Parent.Rect.Height, component.Parent.Rect.Height);
2577  component.RectTransform.AbsoluteOffset = drawRect.Location - component.Parent.Rect.Location;
2578  component.RectTransform.NonScaledSize = drawRect.Size;
2579  var sheet = GUIStyle.GenericThrobber;
2580  sheet.Draw(
2581  spriteBatch,
2582  pos: drawRect.Location.ToVector2(),
2583  spriteIndex: (int)Math.Floor(Timing.TotalTime * 24.0f) % sheet.FrameCount,
2584  color: Color.White,
2585  origin: Vector2.Zero, rotate: 0.0f,
2586  scale: Vector2.One * component.Parent.Rect.Height / sheet.FrameSize.ToVector2());
2587  if (component.ToolTip.IsNullOrEmpty())
2588  {
2589  component.ToolTip = TextManager.Get("PlayerIsDownloadingFiles");
2590  }
2591  }
2592 
2594  {
2595  var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client);
2596  if (playerFrame == null) { return; }
2597  playerFrame.Text = client.Name;
2598 
2599  playerFrame.ToolTip = "";
2600  Color color = Color.White;
2601  if (SelectedMode == GameModePreset.PvP)
2602  {
2603  switch (client.PreferredTeam)
2604  {
2605  case CharacterTeamType.Team1:
2606  color = new Color(0, 110, 150, 255);
2607  playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team1"));
2608  break;
2609  case CharacterTeamType.Team2:
2610  color = new Color(150, 110, 0, 255);
2611  playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team2"));
2612  break;
2613  default:
2614  playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("none"));
2615  break;
2616  }
2617  }
2618  else
2619  {
2620  if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob))
2621  {
2622  color = JobPrefab.Prefabs[client.PreferredJob].UIColor;
2623  playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", JobPrefab.Prefabs[client.PreferredJob].Name);
2624  }
2625  else
2626  {
2627  playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", TextManager.Get("none"));
2628  }
2629  }
2630  playerFrame.Color = color * 0.4f;
2631  playerFrame.HoverColor = color * 0.6f;
2632  playerFrame.SelectedColor = color * 0.8f;
2633  playerFrame.OutlineColor = color * 0.5f;
2634  playerFrame.TextColor = color;
2635  }
2636 
2637  public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally)
2638  {
2639  var PlayerFrame = PlayerList.Content.FindChild(client);
2640  if (PlayerFrame == null) { return; }
2641  var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon");
2642  var soundIconDisabled = PlayerFrame.FindChild("soundicondisabled");
2643 
2644  Pair<string, float> userdata = soundIcon.UserData as Pair<string, float>;
2645 
2646  if (!soundIcon.Visible)
2647  {
2648  userdata.Second = 0.0f;
2649  }
2650  soundIcon.Visible = !muted && !mutedLocally;
2651  soundIconDisabled.Visible = muted || mutedLocally;
2652  soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally");
2653  }
2654 
2655  public void SetPlayerSpeaking(Client client)
2656  {
2657  var PlayerFrame = PlayerList.Content.FindChild(client);
2658  if (PlayerFrame == null) { return; }
2659  var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon");
2660  Pair<string, float> userdata = soundIcon.UserData as Pair<string, float>;
2661  userdata.Second = Math.Max(userdata.Second, 0.18f);
2662  soundIcon.Visible = true;
2663  }
2664 
2665  public void RemovePlayer(Client client)
2666  {
2667  GUIComponent child = PlayerList.Content.GetChildByUserData(client);
2668  if (child != null) { PlayerList.RemoveChild(child); }
2669  }
2670 
2672  => area.Data.ExtractClient();
2673 
2675  {
2676  var client = ExtractClientFromClickableArea(area);
2677  if (client is null) { return; }
2679  }
2680 
2682  {
2683  var client = ExtractClientFromClickableArea(area);
2684  if (client is null) { return; }
2685  CreateModerationContextMenu(client);
2686  }
2687 
2688  #region Context Menu
2689  public static void CreateModerationContextMenu(Client client)
2690  {
2691  if (GUIContextMenu.CurrentContextMenu != null) { return; }
2692  if (GameMain.IsSingleplayer || client == null) { return; }
2693  if (!(GameMain.Client is { PreviouslyConnectedClients: var previouslyConnectedClients })
2694  || !previouslyConnectedClients.Contains(client)) { return; }
2695 
2696  bool hasAccountId = client.AccountId.IsSome();
2697  bool canKick = GameMain.Client.HasPermission(ClientPermissions.Kick);
2698  bool canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking;
2699  bool canManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions);
2700 
2701  // Disable options if we are targeting ourselves
2702  if (client.SessionId == GameMain.Client.SessionId)
2703  {
2704  canKick = canBan = canManagePermissions = false;
2705  }
2706 
2707  List<ContextMenuOption> options = new List<ContextMenuOption>();
2708 
2709  if (client.AccountId.TryUnwrap(out var accountId))
2710  {
2711  options.Add(new ContextMenuOption(accountId.ViewProfileLabel(), isEnabled: hasAccountId, onSelected: () =>
2712  {
2713  accountId.OpenProfile();
2714  }));
2715  }
2716 
2717  options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: () =>
2718  {
2720  }));
2721 
2722  // Creates sub context menu options for all the ranks
2723  List<ContextMenuOption> rankOptions = new List<ContextMenuOption>();
2724  foreach (PermissionPreset rank in PermissionPreset.List)
2725  {
2726  rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () =>
2727  {
2728  LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName));
2729  GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
2730 
2731  msgBox.Buttons[0].OnClicked = delegate
2732  {
2733  client.SetPermissions(rank.Permissions, rank.PermittedCommands);
2734  GameMain.Client.UpdateClientPermissions(client);
2735  msgBox.Close();
2736  return true;
2737  };
2738  msgBox.Buttons[1].OnClicked = delegate
2739  {
2740  msgBox.Close();
2741  return true;
2742  };
2743  }) { Tooltip = rank.Description });
2744  }
2745 
2746  options.Add(new ContextMenuOption("Rank", isEnabled: canManagePermissions, options: rankOptions.ToArray()));
2747 
2748  Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White;
2749 
2750  if (GameMain.Client.ConnectedClients.Contains(client))
2751  {
2752  options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.SessionId != GameMain.Client.SessionId, onSelected: delegate
2753  {
2754  client.MutedLocally = !client.MutedLocally;
2755  }));
2756 
2757  bool kickEnabled = client.SessionId != GameMain.Client.SessionId && client.AllowKicking;
2758 
2759  // if the user can kick create a kick option else create the votekick option
2760  ContextMenuOption kickOption;
2761  if (canKick)
2762  {
2763  kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate
2764  {
2765  GameMain.Client?.CreateKickReasonPrompt(client.Name, false);
2766  });
2767  }
2768  else
2769  {
2770  kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate
2771  {
2772  GameMain.Client?.VoteForKick(client);
2773  });
2774  }
2775 
2776  options.Add(kickOption);
2777  }
2778 
2779  if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(client)) ?? false)
2780  {
2781  options.Add(new ContextMenuOption("clientpermission.unban", isEnabled: canBan, onSelected: delegate
2782  {
2783  GameMain.Client?.UnbanPlayer(client.Name);
2784  }));
2785  }
2786  else
2787  {
2788  options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate
2789  {
2790  GameMain.Client?.CreateKickReasonPrompt(client.Name, true);
2791  }));
2792  }
2793 
2794  GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray());
2795  }
2796 
2797  #endregion
2798 
2799  public bool SelectPlayer(Client selectedClient)
2800  {
2801  bool myClient = selectedClient.SessionId == GameMain.Client.SessionId;
2802  bool hasManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions);
2803 
2804  PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null)
2805  {
2806  OnClicked = (btn, userdata) =>
2807  {
2808  if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock)
2809  {
2810  ClosePlayerFrame(btn, userdata);
2811  }
2812  return true;
2813  }
2814  };
2815 
2816  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
2817  Vector2 frameSize = hasManagePermissions ? new Vector2(.28f, .5f) : new Vector2(.28f, .15f);
2818 
2819  var playerFrameInner = new GUIFrame(new RectTransform(frameSize, PlayerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) });
2820  var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center))
2821  {
2822  Stretch = true,
2823  RelativeSpacing = 0.03f
2824  };
2825 
2826  var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: false);
2827 
2828  var headerTextContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2829  {
2830  Stretch = true
2831  };
2832 
2833  var headerVolumeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2834  {
2835  Stretch = true
2836  };
2837 
2838  var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerTextContainer.RectTransform),
2839  text: selectedClient.Name, font: GUIStyle.LargeFont);
2840  nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f));
2841 
2842  if (hasManagePermissions && !selectedClient.IsOwner)
2843  {
2844  PlayerFrame.UserData = selectedClient;
2845 
2846  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform),
2847  TextManager.Get("Rank"), font: GUIStyle.SubHeadingFont);
2848  var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform),
2849  TextManager.Get("Rank"))
2850  {
2851  UserData = selectedClient,
2852  Enabled = !myClient
2853  };
2854  foreach (PermissionPreset permissionPreset in PermissionPreset.List)
2855  {
2856  rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description);
2857  }
2858  rankDropDown.AddItem(TextManager.Get("CustomRank"), null);
2859 
2860  PermissionPreset currentPreset = PermissionPreset.List.Find(p =>
2861  p.Permissions == selectedClient.Permissions &&
2862  p.PermittedCommands.Count == selectedClient.PermittedConsoleCommands.Count && !p.PermittedCommands.Except(selectedClient.PermittedConsoleCommands).Any());
2863  rankDropDown.SelectItem(currentPreset);
2864 
2865  rankDropDown.OnSelected += (c, userdata) =>
2866  {
2867  PermissionPreset selectedPreset = (PermissionPreset)userdata;
2868  if (selectedPreset != null)
2869  {
2870  var client = PlayerFrame.UserData as Client;
2871  client.SetPermissions(selectedPreset.Permissions, selectedPreset.PermittedCommands);
2873 
2874  PlayerFrame = null;
2875  SelectPlayer(client);
2876  }
2877  return true;
2878  };
2879 
2880  var permissionLabels = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true)
2881  {
2882  Stretch = true,
2883  RelativeSpacing = 0.05f
2884  };
2885  var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUIStyle.SubHeadingFont);
2886  var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform),
2887  TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont);
2888  GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel);
2889 
2890  var permissionContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedPlayerFrame.RectTransform), isHorizontal: true)
2891  {
2892  Stretch = true,
2893  RelativeSpacing = 0.05f
2894  };
2895 
2896  var listBoxContainerLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionContainer.RectTransform))
2897  {
2898  Stretch = true,
2899  RelativeSpacing = 0.05f
2900  };
2901 
2902  new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", "clientpermission.all"))
2903  {
2904  Enabled = !myClient,
2905  OnSelected = (tickbox) =>
2906  {
2907  //reset rank to custom
2908  rankDropDown.SelectItem(null);
2909 
2910  if (PlayerFrame.UserData is not Client client) { return false; }
2911 
2912  foreach (GUIComponent child in tickbox.Parent.GetChild<GUIListBox>().Content.Children)
2913  {
2914  var permissionTickBox = child as GUITickBox;
2915  permissionTickBox.Enabled = false;
2916  permissionTickBox.Selected = tickbox.Selected;
2917  permissionTickBox.Enabled = true;
2918  }
2920  return true;
2921  }
2922  };
2923  var permissionsBox = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerLeft.RectTransform))
2924  {
2925  UserData = selectedClient
2926  };
2927 
2928  foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
2929  {
2930  if (permission == ClientPermissions.None || permission == ClientPermissions.All) { continue; }
2931 
2932  var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform),
2933  TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont)
2934  {
2935  UserData = permission,
2936  Selected = selectedClient.HasPermission(permission),
2937  Enabled = !myClient,
2938  OnSelected = (tickBox) =>
2939  {
2940  //reset rank to custom
2941  rankDropDown.SelectItem(null);
2942 
2943  if (PlayerFrame.UserData is not Client client) { return false; }
2944 
2945  var thisPermission = (ClientPermissions)tickBox.UserData;
2946  if (tickBox.Selected)
2947  {
2948  client.GivePermission(thisPermission);
2949  }
2950  else
2951  {
2952  client.RemovePermission(thisPermission);
2953  }
2954  if (tickBox.Enabled)
2955  {
2957  }
2958  return true;
2959  }
2960  };
2961  permissionTick.ToolTip = permissionTick.TextBlock.ToolTip = TextManager.Get("ClientPermission." + permission + ".description");
2962  }
2963 
2964  var listBoxContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionContainer.RectTransform))
2965  {
2966  Stretch = true,
2967  RelativeSpacing = 0.05f
2968  };
2969 
2970  new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", "clientpermission.all"))
2971  {
2972  Enabled = !myClient,
2973  OnSelected = (tickbox) =>
2974  {
2975  //reset rank to custom
2976  rankDropDown.SelectItem(null);
2977 
2978  if (PlayerFrame.UserData is not Client client) { return false; }
2979 
2980  foreach (GUIComponent child in tickbox.Parent.GetChild<GUIListBox>().Content.Children)
2981  {
2982  var commandTickBox = child as GUITickBox;
2983  commandTickBox.Enabled = false;
2984  commandTickBox.Selected = tickbox.Selected;
2985  commandTickBox.Enabled = true;
2986  }
2988  return true;
2989  }
2990  };
2991  var commandList = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerRight.RectTransform))
2992  {
2993  UserData = selectedClient
2994  };
2995  foreach (DebugConsole.Command command in DebugConsole.Commands)
2996  {
2997  var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform),
2998  command.Names[0].Value, font: GUIStyle.SmallFont)
2999  {
3000  Selected = selectedClient.PermittedConsoleCommands.Contains(command),
3001  Enabled = !myClient,
3002  ToolTip = command.Help,
3003  UserData = command
3004  };
3005  commandTickBox.OnSelected += (GUITickBox tickBox) =>
3006  {
3007  //reset rank to custom
3008  rankDropDown.SelectItem(null);
3009 
3010  DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command;
3011  if (PlayerFrame.UserData is not Client client) { return false; }
3012 
3013  if (!tickBox.Selected)
3014  {
3015  client.PermittedConsoleCommands.Remove(selectedCommand);
3016  }
3017  else if (!client.PermittedConsoleCommands.Contains(selectedCommand))
3018  {
3019  client.PermittedConsoleCommands.Add(selectedCommand);
3020  }
3021  if (tickBox.Enabled)
3022  {
3024  }
3025  return true;
3026  };
3027  }
3028  }
3029 
3030  var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true);
3031  var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
3032 
3033  if (!myClient)
3034  {
3036  {
3037  GUIButton banButton;
3038  if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(selectedClient)) ?? false)
3039  {
3040  banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform),
3041  TextManager.Get("clientpermission.unban"))
3042  {
3043  UserData = selectedClient,
3044  OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; }
3045  };
3046  }
3047  else
3048  {
3049  banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform),
3050  TextManager.Get("Ban"))
3051  {
3052  UserData = selectedClient,
3053  OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; }
3054  };
3055  }
3056  banButton.OnClicked += ClosePlayerFrame;
3057  }
3058 
3059  if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient))
3060  {
3061  if (GameMain.Client.ServerSettings.AllowVoteKick &&
3062  selectedClient != null && selectedClient.AllowKicking)
3063  {
3064  var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform),
3065  TextManager.Get("VoteToKick"))
3066  {
3067  OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; },
3068  UserData = selectedClient
3069  };
3070  }
3071 
3073  selectedClient != null && selectedClient.AllowKicking)
3074  {
3075  var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform),
3076  TextManager.Get("Kick"))
3077  {
3078  UserData = selectedClient,
3079  OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; }
3080  };
3081  kickButton.OnClicked += ClosePlayerFrame;
3082  }
3083 
3084  var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), headerVolumeContainer.RectTransform), isHorizontal: false);
3085  var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
3086  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume"));
3087  var volumePercentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(selectedClient.VoiceVolume), textAlignment: Alignment.Right);
3088  new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider")
3089  {
3090  Range = new Vector2(0f, 1f),
3091  BarScroll = selectedClient.VoiceVolume / Client.MaxVoiceChatBoost,
3092  OnMoved = (_, barScroll) =>
3093  {
3094  float newVolume = barScroll * Client.MaxVoiceChatBoost;
3095 
3096  selectedClient.VoiceVolume = newVolume;
3097  volumePercentageText.Text = ToolBox.GetFormattedPercentage(newVolume);
3098  return true;
3099  }
3100  };
3101 
3102  new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerVolumeContainer.RectTransform, Anchor.TopRight),
3103  TextManager.Get("Mute"))
3104  {
3105  Selected = selectedClient.MutedLocally,
3106  OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; }
3107  };
3108  }
3109 
3110  if (buttonAreaTop.CountChildren > 0)
3111  {
3112  GUITextBlock.AutoScaleAndNormalize(buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock)));
3113  }
3114  }
3115 
3116  if (selectedClient.AccountId.TryUnwrap(out var accountId))
3117  {
3118  var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerTextContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) },
3119  accountId.ViewProfileLabel())
3120  {
3121  UserData = selectedClient
3122  };
3123  viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true;
3124  viewSteamProfileButton.OnClicked = (bt, userdata) =>
3125  {
3126  accountId.OpenProfile();
3127  return true;
3128  };
3129  }
3130 
3131  var closeButton = new GUIButton(new RectTransform(new Vector2(0f, 1.0f), buttonAreaLower.RectTransform, Anchor.CenterRight),
3132  TextManager.Get("Close"))
3133  {
3134  IgnoreLayoutGroups = true,
3135  OnClicked = ClosePlayerFrame
3136  };
3137 
3138  float xSize = 1f / buttonAreaLower.CountChildren;
3139  for (int i = 0; i < buttonAreaLower.CountChildren; i++)
3140  {
3141  buttonAreaLower.GetChild(i).RectTransform.RelativeSize = new Vector2(xSize, 1f);
3142  }
3143 
3144  buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y));
3145 
3146  if (buttonAreaTop != null)
3147  {
3148  if (buttonAreaTop.CountChildren == 0)
3149  {
3150  paddedPlayerFrame.RemoveChild(buttonAreaTop);
3151  }
3152  else
3153  {
3154  for (int i = 0; i < buttonAreaTop.CountChildren; i++)
3155  {
3156  buttonAreaTop.GetChild(i).RectTransform.RelativeSize = new Vector2(1f / 3f, 1f);
3157  }
3158 
3159  buttonAreaTop.RectTransform.NonScaledSize =
3160  buttonAreaLower.RectTransform.NonScaledSize =
3161  new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y)));
3162  }
3163  }
3164 
3165  return false;
3166  }
3167 
3168  private bool ClosePlayerFrame(GUIButton button, object userData)
3169  {
3170  PlayerFrame = null;
3171  PlayerList.Deselect();
3172  return true;
3173  }
3174 
3175  public static void KickPlayer(Client client)
3176  {
3177  if (GameMain.NetworkMember == null || client == null) { return; }
3178  GameMain.Client.CreateKickReasonPrompt(client.Name, false);
3179  }
3180 
3181  public static void BanPlayer(Client client)
3182  {
3183  if (GameMain.NetworkMember == null || client == null) { return; }
3184  GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true);
3185  }
3186 
3187  public override void AddToGUIUpdateList()
3188  {
3189  base.AddToGUIUpdateList();
3190 
3191  //CampaignSetupUI?.AddToGUIUpdateList();
3192  JobInfoFrame?.AddToGUIUpdateList();
3193 
3194  CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList();
3195  JobSelectionFrame?.AddToGUIUpdateList();
3196  }
3197 
3198  public override void Update(double deltaTime)
3199  {
3200  if (GameMain.Client == null) { return; }
3201 
3202  UpdateMicIcon((float)deltaTime);
3203 
3204  foreach (GUIComponent child in PlayerList.Content.Children)
3205  {
3206  if (child.UserData is Client client)
3207  {
3208  if (child.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon") is GUIImage soundIcon)
3209  {
3210  double voipAmplitude = 0.0f;
3211  if (client.SessionId != GameMain.Client.SessionId)
3212  {
3213  voipAmplitude = client.VoipSound?.CurrentAmplitude ?? 0.0f;
3214  }
3215  else
3216  {
3217  var voip = VoipCapture.Instance;
3218  if (voip == null)
3219  {
3220  voipAmplitude = 0;
3221  }
3222  else if (voip.LastEnqueueAudio > DateTime.Now - new TimeSpan(0, 0, 0, 0, milliseconds: 100))
3223  {
3224  voipAmplitude = voip.LastAmplitude;
3225  }
3226  }
3227  VoipClient.UpdateVoiceIndicator(soundIcon, (float)voipAmplitude, (float)deltaTime);
3228  }
3229  }
3230  }
3231 
3232  autoRestartText.Visible = autoRestartTimer > 0.0f && autoRestartBox.Selected;
3233  if (!MathUtils.NearlyEqual(autoRestartTimer, 0.0f) && autoRestartBox.Selected)
3234  {
3235  autoRestartTimer = Math.Max(autoRestartTimer - (float)deltaTime, 0.0f);
3236  if (autoRestartTimer > 0.0f)
3237  {
3238  autoRestartText.Text = TextManager.Get("RestartingIn") + " " + ToolBox.SecondsToReadableTime(Math.Max(autoRestartTimer, 0));
3239  }
3240  }
3241 
3242  CharacterAppearanceCustomizationMenu?.Update();
3243  if (JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(JobSelectionFrame))
3244  {
3245  JobList.Deselect();
3246  JobSelectionFrame.Visible = false;
3247  }
3248 
3249  if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton")
3250  {
3251  if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant)
3252  {
3253  CreateJobVariantTooltip(jobPrefab.Prefab, jobPrefab.Variant, GUI.MouseOn.Parent);
3254  }
3255  }
3256  if (jobVariantTooltip != null)
3257  {
3258  jobVariantTooltip?.AddToGUIUpdateList();
3259  Rectangle mouseRect = jobVariantTooltip.MouseRect;
3260  mouseRect.Inflate(60 * GUI.Scale, 60 * GUI.Scale);
3261  if (!mouseRect.Contains(PlayerInput.MousePosition)) { jobVariantTooltip = null; }
3262  }
3263  }
3264 
3265  private void UpdateMicIcon(float deltaTime)
3266  {
3267  micCheckTimer -= deltaTime;
3268  if (micCheckTimer > 0.0f) { return; }
3269 
3270  Identifier newMicIconStyle = "GUIMicrophoneEnabled".ToIdentifier();
3271  if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled)
3272  {
3273  newMicIconStyle = "GUIMicrophoneDisabled".ToIdentifier();
3274  }
3275  else
3276  {
3277  var voipCaptureDeviceNames = VoipCapture.GetCaptureDeviceNames();
3278  if (voipCaptureDeviceNames.Count == 0)
3279  {
3280  newMicIconStyle = "GUIMicrophoneUnavailable".ToIdentifier();
3281  }
3282  }
3283 
3284  if (newMicIconStyle != micIconStyle)
3285  {
3286  micIconStyle = newMicIconStyle;
3287  GUIStyle.Apply(micIcon, newMicIconStyle);
3288  }
3289 
3290  micCheckTimer = MicCheckInterval;
3291  }
3292 
3293  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
3294  {
3295  if (backgroundSprite?.Texture == null) { return; }
3296  graphics.Clear(Color.Black);
3297  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
3298  GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White);
3299  GUI.Draw(Cam, spriteBatch);
3300  spriteBatch.End();
3301  }
3302 
3303  private PlayStyle? prevPlayStyle = null;
3304  private bool? prevIsPublic = null;
3305 
3306  private void DrawServerBanner(SpriteBatch spriteBatch, GUICustomComponent component)
3307  {
3308  if (GameMain.NetworkMember?.ServerSettings == null) { return; }
3309 
3310  PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle;
3311 
3312  Sprite sprite = GUIStyle
3313  .GetComponentStyle($"PlayStyleBanner.{playStyle}")?
3314  .GetSprite(GUIComponent.ComponentState.None);
3315  if (sprite is null) { return; }
3316 
3317  GUI.DrawBackgroundSprite(spriteBatch, sprite, Color.White, drawArea: component.Rect);
3318 
3319  if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value)
3320  {
3321  playstyleText.Text = TextManager.Get($"ServerTag.{playStyle}");
3322  playstyleText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White;
3323  playstyleText.RectTransform.NonScaledSize = (playstyleText.Font.MeasureString(playstyleText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
3324  prevPlayStyle = playStyle;
3325  (playstyleText.Parent as GUILayoutGroup)?.Recalculate();
3326  playstyleText.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}");
3327  }
3328  if (!prevIsPublic.HasValue || GameMain.NetworkMember.ServerSettings.IsPublic != prevIsPublic.Value)
3329  {
3330  publicOrPrivateText.Text = GameMain.NetworkMember.ServerSettings.IsPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag");
3331  publicOrPrivateText.RectTransform.NonScaledSize = (publicOrPrivateText.Font.MeasureString(publicOrPrivateText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
3332  (publicOrPrivateText.Parent as GUILayoutGroup)?.Recalculate();
3333  prevIsPublic = GameMain.NetworkMember.ServerSettings.IsPublic;
3334  }
3335  }
3336 
3337  private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobPrefab, int itemsPerRow)
3338  {
3339  var itemIdentifiers = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant]
3340  .Where(it => it.ShowPreview)
3341  .Select(it => it.ItemIdentifier)
3342  .Distinct();
3343 
3344  Point slotSize = new Point(component.Rect.Height);
3345  int spacing = (int)(5 * GUI.Scale);
3346  int slotCount = itemIdentifiers.Count();
3347  int slotCountPerRow = Math.Min(slotCount, itemsPerRow);
3348  int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1);
3349 
3350  float totalWidth = slotSize.X * slotCountPerRow + spacing * (slotCountPerRow - 1);
3351  float totalHeight = slotSize.Y * rows + spacing * (rows - 1);
3352  if (totalWidth > component.Rect.Width)
3353  {
3354  slotSize = new Point(
3355  Math.Min((int)Math.Floor((slotSize.X - spacing) * (component.Rect.Width / totalWidth)),
3356  (int)Math.Floor((slotSize.Y - spacing) * (component.Rect.Height / totalHeight))));
3357  }
3358  int i = 0;
3359  Rectangle tooltipRect = Rectangle.Empty;
3360  LocalizedString tooltip = null;
3361  foreach (Identifier itemIdentifier in itemIdentifiers)
3362  {
3363  if (!(MapEntityPrefab.Find(null, identifier: itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)) { continue; }
3364 
3365  int row = (int)Math.Floor(i / (float)slotCountPerRow);
3366  int slotsPerThisRow = Math.Min((slotCount - row * slotCountPerRow), slotCountPerRow);
3367  Vector2 slotPos = new Vector2(
3368  component.Rect.Center.X + (slotSize.X + spacing) * (i % slotCountPerRow - slotsPerThisRow * 0.5f),
3369  component.Rect.Bottom - (rows * (slotSize.Y + spacing)) + (slotSize.Y + spacing) * row);
3370 
3371  Rectangle slotRect = new Rectangle(slotPos.ToPoint(), slotSize);
3372  Inventory.SlotSpriteSmall.Draw(spriteBatch, slotPos,
3373  scale: slotSize.X / (float)Inventory.SlotSpriteSmall.SourceRect.Width,
3374  color: slotRect.Contains(PlayerInput.MousePosition) ? Color.White : Color.White * 0.6f);
3375 
3376  Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.Sprite;
3377  float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f;
3378  icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale);
3379 
3380  int count = jobPrefab.Prefab.PreviewItems[jobPrefab.Variant].Count(it => it.ShowPreview && it.ItemIdentifier == itemIdentifier);
3381  if (count > 1)
3382  {
3383  string itemCountText = "x" + count;
3384  GUIStyle.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUIStyle.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White);
3385  }
3386 
3387  if (slotRect.Contains(PlayerInput.MousePosition))
3388  {
3389  tooltipRect = slotRect;
3390  tooltip = itemPrefab.Name + '\n' + itemPrefab.Description;
3391  }
3392  i++;
3393  }
3394  if (!tooltip.IsNullOrEmpty())
3395  {
3396  GUIComponent.DrawToolTip(spriteBatch, tooltip, tooltipRect);
3397  }
3398  }
3399 
3400  public void NewChatMessage(ChatMessage message)
3401  {
3402  float prevSize = chatBox.BarSize;
3403 
3404  while (chatBox.Content.CountChildren > 60)
3405  {
3406  chatBox.RemoveChild(chatBox.Content.Children.First());
3407  }
3408 
3409  GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform),
3410  text: RichString.Rich(ChatMessage.GetTimeStamp() + (message.Type == ChatMessageType.Private ? TextManager.Get("PrivateMessageTag") + " " : "") + message.TextWithSender),
3411  textColor: message.Color,
3412  color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f,
3413  wrap: true, font: GUIStyle.SmallFont)
3414  {
3415  UserData = message,
3416  CanBeFocused = false
3417  };
3419  if (msg.RichTextData != null)
3420  {
3421  foreach (var data in msg.RichTextData)
3422  {
3424  {
3425  Data = data,
3426  OnClick = GameMain.NetLobbyScreen.SelectPlayer,
3427  OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu
3428  });
3429  }
3430  }
3431  msg.RectTransform.SizeChanged += Recalculate;
3432  void Recalculate()
3433  {
3434  msg.RectTransform.SizeChanged -= Recalculate;
3436  msg.RectTransform.SizeChanged += Recalculate;
3437  }
3438 
3439  if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f))
3440  {
3441  chatBox.BarScroll = 1.0f;
3442  }
3443  }
3444 
3445  private bool SelectJobPreferencesTab(GUIButton button, object userData)
3446  {
3447  jobPreferencesButton.Selected = true;
3448  appearanceButton.Selected = false;
3449 
3450  JobPreferenceContainer.Visible = true;
3451  appearanceFrame.Visible = false;
3452 
3453  return false;
3454  }
3455 
3456  private bool SelectAppearanceTab(GUIButton button, object _)
3457  {
3458  jobPreferencesButton.Selected = false;
3459  appearanceButton.Selected = true;
3460 
3461  JobPreferenceContainer.Visible = false;
3462  appearanceFrame.Visible = true;
3463 
3464  appearanceFrame.ClearChildren();
3465 
3466  var info = GameMain.Client.CharacterInfo ?? Character.Controlled?.Info;
3467  CharacterAppearanceCustomizationMenu?.Dispose();
3468  CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame)
3469  {
3470  OnHeadSwitch = menu =>
3471  {
3472  UpdateJobPreferences(info);
3473  SelectAppearanceTab(button, _);
3474  }
3475  };
3476  return false;
3477  }
3478 
3479  public bool SaveAppearance()
3480  {
3481  var info = GameMain.Client?.CharacterInfo;
3482  if (info?.Head == null) { return false; }
3483 
3484  var characterConfig = MultiplayerPreferences.Instance;
3485 
3486  characterConfig.TagSet.Clear();
3487  characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet);
3488  characterConfig.HairIndex = info.Head.HairIndex;
3489  characterConfig.BeardIndex = info.Head.BeardIndex;
3490  characterConfig.MoustacheIndex = info.Head.MoustacheIndex;
3491  characterConfig.FaceAttachmentIndex = info.Head.FaceAttachmentIndex;
3492  characterConfig.HairColor = info.Head.HairColor;
3493  characterConfig.FacialHairColor = info.Head.FacialHairColor;
3494  characterConfig.SkinColor = info.Head.SkinColor;
3495 
3496  if (GameMain.GameSession?.IsRunning ?? false)
3497  {
3498  TabMenu.PendingChanges = true;
3499  CreateChangesPendingText();
3500  }
3501  GameSettings.SaveCurrentConfig();
3502  return true;
3503  }
3504 
3505  private bool SwitchJob(GUIButton _, object obj)
3506  {
3507  if (JobList == null || GameMain.Client == null) { return false; }
3508 
3509  int childIndex = JobList.SelectedIndex;
3510  var child = JobList.SelectedComponent;
3511  if (child == null) { return false; }
3512 
3513  bool moveToNext = obj != null;
3514 
3515  var jobPrefab = (obj as JobVariant)?.Prefab;
3516  object prevObj = child.UserData;
3517 
3518  var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab));
3519  if (existingChild != null && obj != null)
3520  {
3521  existingChild.UserData = prevObj;
3522  }
3523  child.UserData = obj;
3524 
3525  for (int i = 0; i < 2; i++)
3526  {
3527  if (i < 2 && JobList.Content.GetChild(i).UserData == null)
3528  {
3529  JobList.Content.GetChild(i).UserData = JobList.Content.GetChild(i + 1).UserData;
3530  JobList.Content.GetChild(i + 1).UserData = null;
3531  }
3532  }
3533 
3534  UpdateJobPreferences(GameMain.Client.CharacterInfo ?? Character.Controlled?.Info);
3535 
3536  if (moveToNext)
3537  {
3538  var emptyChild = JobList.Content.FindChild(c => c.UserData == null && c.CanBeFocused);
3539  if (emptyChild != null)
3540  {
3541  JobList.Select(JobList.Content.GetChildIndex(emptyChild));
3542  }
3543  else
3544  {
3545  JobList.Deselect();
3546  if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; }
3547  }
3548  }
3549  else
3550  {
3551  OpenJobSelection(child, child.UserData);
3552  }
3553 
3554  return false;
3555  }
3556 
3557  private bool OpenJobSelection(GUIComponent _, object __)
3558  {
3559  if (JobSelectionFrame != null)
3560  {
3561  JobSelectionFrame.Visible = true;
3562  return true;
3563  }
3564 
3565  Point frameSize = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f));
3566  JobSelectionFrame = new GUIFrame(new RectTransform(frameSize, GUI.Canvas, Anchor.TopLeft)
3567  { AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - frameSize.X, characterInfoFrame.Rect.Bottom) }, style:"GUIFrameListBox");
3568 
3569  characterInfoFrame.RectTransform.SizeChanged += () =>
3570  {
3571  if (characterInfoFrame == null || JobSelectionFrame?.RectTransform == null) { return; }
3572  Point size = new Point(characterInfoFrame.Rect.Width, (int)(characterInfoFrame.Rect.Height * 2 * 0.6f));
3573  JobSelectionFrame.RectTransform.Resize(size);
3574  JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.Right - size.X, characterInfoFrame.Rect.Bottom);
3575  };
3576 
3577  new GUIFrame(new RectTransform(new Vector2(1.25f, 1.25f), JobSelectionFrame.RectTransform, anchor: Anchor.Center), style: "OuterGlow", color: Color.Black)
3578  {
3579  UserData = "outerglow",
3580  CanBeFocused = false
3581  };
3582 
3583  var rows = new GUILayoutGroup(new RectTransform(Vector2.One, JobSelectionFrame.RectTransform)) { Stretch = true };
3584  var row = new GUILayoutGroup(new RectTransform(Vector2.One, rows.RectTransform), true);
3585 
3586  GUIButton jobButton = null;
3587 
3588  var availableJobs = JobPrefab.Prefabs.Where(jobPrefab =>
3589  !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.All(c => c.UserData is not JobVariant prefab || prefab.Prefab != jobPrefab)
3590  ).Select(j => new JobVariant(j, 0));
3591 
3592  availableJobs = availableJobs.Concat(
3593  JobPrefab.Prefabs.Where(jobPrefab =>
3594  !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0 && JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab)
3595  ).Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData));
3596 
3597  availableJobs = availableJobs.ToList();
3598 
3599  int itemsInRow = 0;
3600 
3601  foreach (var jobPrefab in availableJobs)
3602  {
3603  if (itemsInRow >= 3)
3604  {
3605  row = new GUILayoutGroup(new RectTransform(Vector2.One, rows.RectTransform), true);
3606  itemsInRow = 0;
3607  }
3608 
3609  jobButton = new GUIButton(new RectTransform(new Vector2(1.0f / 3.0f, 1.0f), row.RectTransform), style: "ListBoxElementSquare")
3610  {
3611  UserData = jobPrefab,
3612  OnClicked = (btn, usdt) =>
3613  {
3614  if (btn.IsParentOf(GUI.MouseOn)) return false;
3615  return SwitchJob(btn, usdt);
3616  }
3617  };
3618  itemsInRow++;
3619 
3620  var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, selectedByPlayer: false);
3621  if (images != null && images.Length > 1)
3622  {
3623  jobPrefab.Variant = Math.Min(jobPrefab.Variant, images.Length);
3624  int currVisible = jobPrefab.Variant;
3625  GUIButton currSelected = null;
3626  for (int variantIndex = 0; variantIndex < images.Length; variantIndex++)
3627  {
3628  foreach (GUIImage image in images[variantIndex])
3629  {
3630  image.Visible = currVisible == variantIndex;
3631  }
3632 
3633  var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, jobButton);
3634  variantButton.OnClicked = (btn, obj) =>
3635  {
3636  if (currSelected != null) { currSelected.Selected = false; }
3637  int k = ((JobVariant)obj).Variant;
3638  btn.Parent.UserData = obj;
3639  for (int j = 0; j < images.Length; j++)
3640  {
3641  foreach (GUIImage image in images[j])
3642  {
3643  image.Visible = k == j;
3644  }
3645  }
3646  currSelected = btn;
3647  currSelected.Selected = true;
3648  return false;
3649  };
3650 
3651  if (currVisible == variantIndex)
3652  {
3653  currSelected = variantButton;
3654  }
3655  }
3656 
3657  if (currSelected != null)
3658  {
3659  currSelected.Selected = true;
3660  }
3661  }
3662  }
3663 
3664  return true;
3665  }
3666 
3667  private static GUIImage[][] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, bool selectedByPlayer)
3668  {
3669  GUIFrame innerFrame = null;
3670  List<JobPrefab.OutfitPreview> outfitPreviews = jobPrefab.GetJobOutfitSprites(CharacterPrefab.HumanPrefab.CharacterInfoPrefab, useInventoryIcon: true, out var maxDimensions);
3671 
3672  innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.85f, parent.RectTransform, Anchor.Center), style: null)
3673  {
3674  CanBeFocused = false
3675  };
3676 
3677  GUIImage[][] retVal = Array.Empty<GUIImage[]>();
3678  if (outfitPreviews != null && outfitPreviews.Any())
3679  {
3680  retVal = new GUIImage[outfitPreviews.Count][];
3681  for (int i = 0; i < outfitPreviews.Count; i++)
3682  {
3683  JobPrefab.OutfitPreview outfitPreview = outfitPreviews[i];
3684  retVal[i] = new GUIImage[outfitPreview.Sprites.Count];
3685  for (int j = 0; j < outfitPreview.Sprites.Count; j++)
3686  {
3687  Sprite sprite = outfitPreview.Sprites[j].sprite;
3688  Vector2 drawOffset = outfitPreview.Sprites[j].drawOffset;
3689  float aspectRatio = outfitPreview.Dimensions.Y / outfitPreview.Dimensions.X;
3690  retVal[i][j] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center)
3691  { RelativeOffset = drawOffset / outfitPreview.Dimensions }, sprite, scaleToFit: true)
3692  {
3693  PressedColor = Color.White,
3694  CanBeFocused = false
3695  };
3696  }
3697  }
3698  }
3699 
3700  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), parent.RectTransform, Anchor.BottomCenter), style: "OuterGlow")
3701  {
3702  Color = Color.Black,
3703  HoverColor = Color.Black,
3704  PressedColor = Color.Black,
3705  SelectedColor = Color.Black,
3706  CanBeFocused = false
3707  };
3708 
3709  var textBlock = new GUITextBlock(
3710  innerFrame.CountChildren == 0 ?
3711  new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center) :
3712  new RectTransform(new Vector2(selectedByPlayer ? 0.55f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter),
3713  jobPrefab.Name, wrap: true, textAlignment: Alignment.BottomCenter)
3714  {
3715  Padding = Vector4.Zero,
3716  HoverColor = Color.Transparent,
3717  SelectedColor = Color.Transparent,
3718  TextColor = jobPrefab.UIColor,
3719  HoverTextColor = Color.Lerp(jobPrefab.UIColor, Color.White, 0.5f),
3720  CanBeFocused = false,
3721  AutoScaleHorizontal = true
3722  };
3723  textBlock.TextAlignment = textBlock.WrappedText.Contains('\n') ? Alignment.BottomCenter : Alignment.Center;
3724  textBlock.RectTransform.SizeChanged += () => { textBlock.TextScale = 1.0f; };
3725 
3726  return retVal;
3727  }
3728 
3729  public void SelectMode(int modeIndex)
3730  {
3731  if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; }
3732 
3733  if ((GameModePreset)ModeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign)
3734  {
3735  ToggleCampaignMode(false);
3736  }
3737 
3738  var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset;
3739 
3740  if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, GUIListBox.Force.Yes); }
3741  selectedModeIndex = modeIndex;
3742 
3743  if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP))
3744  {
3745  SaveAppearance();
3746  UpdatePlayerFrame(null);
3747  GameMain.Client.ConnectedClients.ForEach(c => SetPlayerNameAndJobPreference(c));
3748  }
3749 
3751  {
3752  GameMain.GameSession = null;
3753  }
3754 
3755  respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes
3756  RefreshGameModeContent();
3757  RefreshEnabledElements();
3758  }
3759 
3760  public void HighlightMode(int modeIndex)
3761  {
3762  if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; }
3763 
3764  HighlightedModeIndex = modeIndex;
3765  RefreshGameModeContent();
3766  RefreshEnabledElements();
3767  }
3768 
3769  private void RefreshMissionTypes()
3770  {
3771  for (int i = 0; i < missionTypeTickBoxes.Length; i++)
3772  {
3773  MissionType missionType = (MissionType)(int)missionTypeTickBoxes[i].UserData;
3774  if (MissionPrefab.HiddenMissionClasses.Contains(missionType))
3775  {
3776  missionTypeTickBoxes[i].Parent.Visible = false;
3777  continue;
3778  }
3779  if (SelectedMode == GameModePreset.Mission)
3780  {
3781  missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.CoOpMissionClasses.ContainsKey(missionType);
3782  }
3783  else if (SelectedMode == GameModePreset.PvP)
3784  {
3785  missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.PvPMissionClasses.ContainsKey(missionType);
3786  }
3787  }
3788  }
3789 
3790  private void RefreshGameModeContent()
3791  {
3792  if (GameMain.Client == null) { return; }
3793 
3794  foreach (var subElement in SubList.Content.Children)
3795  {
3796  subElement.CanBeFocused = true;
3797  foreach (var textBlock in subElement.GetAllChildren<GUITextBlock>())
3798  {
3799  textBlock.Enabled = true;
3800  }
3801  }
3802 
3803  SubList.Content.RectTransform.SortChildren((rt1, rt2) =>
3804  {
3805  SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo;
3806  SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo;
3807  return s1.Name.CompareTo(s2.Name);
3808  });
3809 
3810  autoRestartBox.Parent.Visible = true;
3811  if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP)
3812  {
3813  MissionTypeFrame.Visible = true;
3814  CampaignFrame.Visible = CampaignSetupFrame.Visible = false;
3815  RefreshMissionTypes();
3816  }
3817  else if (SelectedMode == GameModePreset.MultiPlayerCampaign)
3818  {
3819  MissionTypeFrame.Visible = autoRestartBox.Parent.Visible = false;
3820 
3821  if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map != null)
3822  {
3823  //campaign running
3824  CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound);
3825  CampaignSetupFrame.Visible = false;
3826  }
3827  else
3828  {
3829  CampaignFrame.Visible = false;
3830  CampaignSetupFrame.Visible = true;
3831  if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound))
3832  {
3833  CampaignSetupFrame.ClearChildren();
3834  new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center),
3835  TextManager.Get("campaignstarting"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true);
3836  }
3837  }
3838 
3839  if (CampaignSetupUI != null)
3840  {
3841  foreach (var subElement in SubList.Content.Children)
3842  {
3843  var sub = subElement.UserData as SubmarineInfo;
3844  bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney;
3845  if (tooExpensive || !sub.IsCampaignCompatible)
3846  {
3847  foreach (var textBlock in subElement.GetAllChildren<GUITextBlock>())
3848  {
3849  textBlock.DisabledTextColor = (textBlock.UserData as string == "pricetext" && tooExpensive ? GUIStyle.Red : GUIStyle.TextColorNormal) * 0.7f;
3850  textBlock.Enabled = false;
3851  }
3852  }
3853  }
3854  SubList.Content.RectTransform.SortChildren((rt1, rt2) =>
3855  {
3856  SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo;
3857  SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo;
3858  int p1 = s1.Price;
3859  if (!s1.IsCampaignCompatible) { p1 += 100000; }
3860  int p2 = s2.Price;
3861  if (!s2.IsCampaignCompatible) { p2 += 100000; }
3862  return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name);
3863  });
3864  }
3865  }
3866  else
3867  {
3868  MissionTypeFrame.Visible = CampaignFrame.Visible = CampaignSetupFrame.Visible = false;
3869  CampaignFrame.Visible = CampaignSetupFrame.Visible = false;
3870  }
3871 
3872  ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted;
3873  RefreshStartButtonVisibility();
3874  }
3875 
3877  {
3878  if (CampaignSetupUI != null && CampaignSetupFrame is { Visible: true })
3879  {
3880  //setting up a campaign -> start button only visible if we're in the "new game" tab (load game menu not visible)
3881  StartButton.Visible =
3882  !GameMain.Client.GameStarted &&
3883  !CampaignSetupUI.LoadGameMenuVisible &&
3885  }
3886  else
3887  {
3888  //if a campaign is currently running, we must show the start button to allow continuing
3889  bool campaignActive = GameMain.GameSession?.GameMode is CampaignMode;
3890  StartButton.Visible =
3891  (SelectedMode != GameModePreset.MultiPlayerCampaign || campaignActive) &&
3892  !GameMain.Client.GameStarted && GameMain.Client.HasPermission(ClientPermissions.ManageRound);
3893  }
3894  }
3895 
3896  public void ToggleCampaignMode(bool enabled)
3897  {
3898  if (!enabled)
3899  {
3900  //remove campaign character from the panel
3901  if (campaignCharacterInfo != null)
3902  {
3903  campaignCharacterInfo = null;
3904  UpdatePlayerFrame(null);
3905  SetSpectate(spectateBox.Selected);
3906  }
3907  CampaignCharacterDiscarded = false;
3908  }
3909  RefreshEnabledElements();
3910  if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign)
3911  {
3912  ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes);
3913  }
3914  }
3915 
3917  {
3918  string name = submarine?.Name;
3919  bool displayed = false;
3920  SubList.OnSelected -= VotableClicked;
3921  SubList.Deselect();
3922  subPreviewContainer.ClearChildren();
3923  foreach (GUIComponent child in SubList.Content.Children)
3924  {
3925  if (child.UserData is not SubmarineInfo sub) { continue; }
3926  //just check the name, even though the campaign sub may not be the exact same version
3927  //we're selecting the sub just for show, the selection is not actually used for anything
3928  if (sub.Name == name)
3929  {
3930  SubList.Select(sub);
3931  if (SubmarineInfo.SavedSubmarines.Contains(sub))
3932  {
3933  CreateSubPreview(sub);
3934  displayed = true;
3935  }
3936  break;
3937  }
3938  }
3939  SubList.OnSelected += VotableClicked;
3940  if (!displayed)
3941  {
3942  CreateSubPreview(submarine);
3943  }
3944  UpdateSubVisibility();
3945  }
3946 
3947  private bool ViewJobInfo(GUIButton button, object obj)
3948  {
3949  if (button.UserData is not JobVariant jobPrefab) { return false; }
3950 
3951  JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(out GUIComponent buttonContainer);
3952  GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight),
3953  TextManager.Get("Close"))
3954  {
3955  OnClicked = CloseJobInfo
3956  };
3957  JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; };
3958 
3959  return true;
3960  }
3961 
3962  private bool CloseJobInfo(GUIButton button, object obj)
3963  {
3964  JobInfoFrame = null;
3965  return true;
3966  }
3967 
3968  private void UpdateJobPreferences(CharacterInfo characterInfo)
3969  {
3970  if (characterInfo == null) { return; }
3971 
3972  GUICustomComponent characterIcon = JobPreferenceContainer.GetChild<GUICustomComponent>();
3973  JobPreferenceContainer.RemoveChild(characterIcon);
3974  characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) });
3975 
3976  GUIListBox listBox = JobPreferenceContainer.GetChild<GUIListBox>();
3977  /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); }
3978  jobPreferenceSprites.Clear();*/
3979 
3980  List<MultiplayerPreferences.JobPreference> jobPreferences = new List<MultiplayerPreferences.JobPreference>();
3981 
3982  bool disableNext = false;
3983  for (int i = 0; i < listBox.Content.CountChildren; i++)
3984  {
3985  GUIComponent slot = listBox.Content.GetChild(i);
3986 
3987  slot.ClearChildren();
3988 
3989  slot.CanBeFocused = !disableNext;
3990  if (slot.UserData is JobVariant jobPrefab)
3991  {
3992  var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, selectedByPlayer: true);
3993  for (int variantIndex = 0; variantIndex < images.Length; variantIndex++)
3994  {
3995  foreach (GUIImage image in images[variantIndex])
3996  {
3997  //jobPreferenceSprites.Add(image.Sprite);
3998  int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length);
3999  image.Visible = images.Length == 1 || selectedVariantIndex == variantIndex;
4000  }
4001  if (images.Length > 1)
4002  {
4003  var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, slot);
4004  variantButton.OnClicked = (btn, obj) =>
4005  {
4006  btn.Parent.UserData = obj;
4007  UpdateJobPreferences(characterInfo);
4008  return false;
4009  };
4010  }
4011  }
4012 
4013  // Info button
4014  new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.075f) },
4015  style: "GUIButtonInfo")
4016  {
4017  UserData = jobPrefab,
4018  OnClicked = ViewJobInfo
4019  };
4020 
4021  // Remove button
4022  new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.075f) },
4023  style: "GUICancelButton")
4024  {
4025  UserData = i,
4026  OnClicked = (btn, obj) =>
4027  {
4028  JobList.Select((int)obj, GUIListBox.Force.Yes);
4029  SwitchJob(btn, null);
4030  if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; }
4031  JobList.Deselect();
4032 
4033  return false;
4034  }
4035  };
4036 
4037  jobPreferences.Add(new MultiplayerPreferences.JobPreference(jobPrefab.Prefab.Identifier, jobPrefab.Variant));
4038  }
4039  else
4040  {
4041  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), slot.RectTransform), (i + 1).ToString(),
4042  textColor: Color.White * (disableNext ? 0.15f : 0.5f),
4043  textAlignment: Alignment.Center,
4044  font: GUIStyle.LargeFont)
4045  {
4046  CanBeFocused = false
4047  };
4048 
4049  if (!disableNext)
4050  {
4051  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), slot.RectTransform, Anchor.BottomCenter), TextManager.Get("clicktoselectjob"),
4052  font: GUIStyle.SmallFont,
4053  wrap: true,
4054  textAlignment: Alignment.Center)
4055  {
4056  CanBeFocused = false
4057  };
4058  }
4059 
4060  disableNext = true;
4061  }
4062  }
4063  GameMain.Client.ForceNameAndJobUpdate();
4064 
4065  if (!MultiplayerPreferences.Instance.AreJobPreferencesEqual(jobPreferences))
4066  {
4067  if (GameMain.GameSession?.IsRunning ?? false)
4068  {
4069  TabMenu.PendingChanges = true;
4070  CreateChangesPendingText();
4071  }
4072 
4073  MultiplayerPreferences.Instance.JobPreferences.Clear();
4074  MultiplayerPreferences.Instance.JobPreferences.AddRange(jobPreferences);
4075  GameSettings.SaveCurrentConfig();
4076  }
4077  }
4078 
4079  private static GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot)
4080  {
4081  float relativeSize = 0.15f;
4082 
4083  var btn = new GUIButton(new RectTransform(new Vector2(relativeSize), slot.RectTransform, Anchor.TopCenter, scaleBasis: ScaleBasis.BothHeight)
4084  { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) },
4085  (variantIndex + 1).ToString(), style: "JobVariantButton")
4086  {
4087  Selected = jobPrefab.Variant == variantIndex,
4088  UserData = new JobVariant(jobPrefab.Prefab, variantIndex),
4089  };
4090 
4091  return btn;
4092  }
4093 
4094  public readonly struct FailedSubInfo
4095  {
4096  public readonly string Name;
4097  public readonly string Hash;
4098  public FailedSubInfo(string name, string hash) { Name = name; Hash = hash; }
4099  public void Deconstruct(out string name, out string hash) { name = Name; hash = Hash; }
4100 
4101  private static bool StringsEqual(string a, string b)
4102  => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
4103 
4104  public static bool operator ==(FailedSubInfo a, FailedSubInfo b)
4105  => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash);
4106 
4107  public static bool operator !=(FailedSubInfo a, FailedSubInfo b)
4108  => !(a == b);
4109 
4110  public override int GetHashCode()
4111  {
4112  return HashCode.Combine(Name, Hash);
4113  }
4114 
4115  public override bool Equals(object obj)
4116  {
4117  return obj is FailedSubInfo info &&
4118  Name == info.Name &&
4119  Hash == info.Hash;
4120  }
4121  }
4122 
4125 
4126  public List<FailedSubInfo> FailedCampaignSubs = new List<FailedSubInfo>();
4127  public List<FailedSubInfo> FailedOwnedSubs = new List<FailedSubInfo>();
4128 
4129  public bool TrySelectSub(string subName, string md5Hash, GUIListBox subList)
4130  {
4131  UpdateSubVisibility();
4132  if (GameMain.Client == null) { return false; }
4133 
4134  //already downloading the selected sub file
4135  if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == subName + ".sub"))
4136  {
4137  return false;
4138  }
4139 
4140  SubmarineInfo sub = subList.Content.Children
4141  .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.StringRepresentation == md5Hash)?
4142  .UserData as SubmarineInfo;
4143 
4144  //matching sub found and already selected, all good
4145  if (sub != null)
4146  {
4147  if (subList == SubList)
4148  {
4149  CreateSubPreview(sub);
4150  }
4151 
4152  if (subList.SelectedData is SubmarineInfo selectedSub && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath))
4153  {
4154  return true;
4155  }
4156  }
4157 
4158  //sub not found, see if we have a sub with the same name
4159  if (sub == null)
4160  {
4161  sub = subList.Content.Children
4162  .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName)?
4163  .UserData as SubmarineInfo;
4164  }
4165 
4166  //found a sub that at least has the same name, select it
4167  if (sub != null)
4168  {
4169  if (subList.Parent is GUIDropDown subDropDown)
4170  {
4171  subDropDown.SelectItem(sub);
4172  }
4173  else
4174  {
4175  subList.OnSelected -= VotableClicked;
4176  subList.Select(sub, GUIListBox.Force.Yes);
4177  subList.OnSelected += VotableClicked;
4178  }
4179 
4180  if (subList == SubList)
4181  {
4182  FailedSelectedSub = null;
4183  }
4184  else
4185  {
4186  FailedSelectedShuttle = null;
4187  }
4188 
4189  //hashes match, all good
4190  if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub))
4191  {
4192  return true;
4193  }
4194  }
4195 
4196  //-------------------------------------------------------------------------------------
4197  //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash
4198 
4199  if (subList == SubList)
4200  {
4201  FailedSelectedSub = new FailedSubInfo(subName, md5Hash);
4202  }
4203  else
4204  {
4205  FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash);
4206  }
4207 
4208  LocalizedString errorMsg = "";
4209  if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub))
4210  {
4211  errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " ";
4212  }
4213  else if (sub.MD5Hash?.StringRepresentation == null)
4214  {
4215  errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", subName) + " ";
4216  GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild<GUITextBlock>();
4217  if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; }
4218  }
4219  else
4220  {
4221  errorMsg = TextManager.GetWithVariables("SubDoesntMatchError",
4222  ("[subname]", sub.Name),
4223  ("[myhash]", sub.MD5Hash.ShortRepresentation),
4224  ("[serverhash]", Md5Hash.GetShortHash(md5Hash))) + " ";
4225  }
4226 
4227  if (GameMain.Client.ServerSettings.AllowFileTransfers)
4228  {
4229  GameMain.Client?.RequestFile(FileTransferType.Submarine, subName, md5Hash);
4230  }
4231  else
4232  {
4233  new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg);
4234  }
4235  return false;
4236  }
4237 
4239  {
4240  Owned,
4241  Campaign
4242  }
4243 
4244  public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData)
4245  {
4246  if (GameMain.Client == null) { return false; }
4247 
4248  //already downloading the selected sub file
4249  if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == serverSubmarine.Name + ".sub"))
4250  {
4251  return false;
4252  }
4253 
4254  SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.StringRepresentation == serverSubmarine.MD5Hash?.StringRepresentation);
4255  if (purchasableSub != null)
4256  {
4257  return true;
4258  }
4259 
4260  FailedSubInfo fileInfo = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation);
4261 
4262  switch (deliveryData)
4263  {
4264  case SubmarineDeliveryData.Owned:
4265  FailedOwnedSubs.Add(fileInfo);
4266  break;
4267  case SubmarineDeliveryData.Campaign:
4268  FailedCampaignSubs.Add(fileInfo);
4269  break;
4270  }
4271 
4272  GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo.Name, fileInfo.Hash);
4273 
4274  return false;
4275  }
4276 
4277  private void CreateSubPreview(SubmarineInfo sub)
4278  {
4279  subPreviewContainer?.ClearChildren();
4280  sub.CreatePreviewWindow(subPreviewContainer);
4281  RecalculateSubDescription();
4282  }
4283 
4284  private void RecalculateSubDescription()
4285  {
4286  var descriptionBox = subPreviewContainer?.FindChild("descriptionbox", recursive: true);
4287  if (descriptionBox != null && characterInfoFrame != null)
4288  {
4289  //if description box and character info box are roughly the same size, scale them to the same size
4290  if (Math.Abs(descriptionBox.Rect.Height - characterInfoFrame.Rect.Height) < 80 * GUI.Scale)
4291  {
4292  descriptionBox.RectTransform.MaxSize = new Point(descriptionBox.Rect.Width, characterInfoFrame.Rect.Height);
4293  }
4294  }
4295  }
4296 
4297  private readonly List<SubmarineInfo> visibilityMenuOrder = new List<SubmarineInfo>();
4298  private void CreateSubmarineVisibilityMenu()
4299  {
4300  var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "",
4301  buttons: Array.Empty<LocalizedString>(),
4302  relativeSize: new Vector2(0.75f, 0.75f));
4303  messageBox.Content.ChildAnchor = Anchor.TopCenter;
4304  var columns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), messageBox.Content.RectTransform), isHorizontal: true);
4305 
4306  GUILayoutGroup createColumn(float width)
4307  => new GUILayoutGroup(new RectTransform(new Vector2(width, 1.0f), columns.RectTransform))
4308  { Stretch = true };
4309 
4310  GUIListBox createColumnListBox(string labelTag)
4311  {
4312  var column = createColumn(0.45f);
4313  var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), column.RectTransform),
4314  TextManager.Get(labelTag), textAlignment: Alignment.Center);
4315  return new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), column.RectTransform))
4316  {
4317  CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple,
4318  CurrentDragMode = GUIListBox.DragMode.DragOutsideBox,
4319  HideDraggedElement = true
4320  };
4321  }
4322 
4323  void handleDraggingAcrossLists(GUIListBox from, GUIListBox to)
4324  {
4325  //TODO: put this in a static class once modding-refactor gets merged
4326 
4327  if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null)
4328  {
4329  //move the dragged elements to the index determined previously
4330  var draggedElement = from.DraggedElement;
4331 
4332  var selected = from.AllSelected.ToList();
4333  selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b));
4334 
4335  float oldCount = to.Content.CountChildren;
4336  float newCount = oldCount + selected.Count;
4337 
4338  var offset = draggedElement.RectTransform.AbsoluteOffset;
4339  offset += from.Content.Rect.Location;
4340  offset -= to.Content.Rect.Location;
4341 
4342  for (int i = 0; i < selected.Count; i++)
4343  {
4344  var c = selected[i];
4345  c.Parent.RemoveChild(c);
4346  c.RectTransform.Parent = to.Content.RectTransform;
4347  c.RectTransform.RepositionChildInHierarchy((int)oldCount+i);
4348  }
4349 
4350  from.DraggedElement = null;
4351  from.Deselect();
4352  from.RecalculateChildren();
4353  from.RectTransform.RecalculateScale(true);
4354  to.RecalculateChildren();
4355  to.RectTransform.RecalculateScale(true);
4356  to.Select(selected);
4357 
4358  //recalculate the dragged element's offset so it doesn't jump around
4359  draggedElement.RectTransform.AbsoluteOffset = offset;
4360 
4361  to.DraggedElement = draggedElement;
4362 
4363  to.BarScroll *= (oldCount / newCount);
4364  }
4365  }
4366 
4367  var visibleSubsList = createColumnListBox("VisibleSubmarines");
4368  var centerColumn = createColumn(0.1f);
4369 
4370  void centerSpacing()
4371  {
4372  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), centerColumn.RectTransform), style: null);
4373  }
4374 
4375  GUIButton centerButton(string style)
4376  => new GUIButton(
4377  new RectTransform(new Vector2(1.0f, 0.1f), centerColumn.RectTransform),
4378  style: style);
4379 
4380  var hiddenSubsList = createColumnListBox("HiddenSubmarines");
4381 
4382  void addSubToList(SubmarineInfo sub, GUIListBox list)
4383  {
4384  var modFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.08f), list.Content.RectTransform),
4385  style: "ListBoxElement")
4386  {
4387  UserData = sub
4388  };
4389 
4390  var frameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft)
4391  {
4392  Stretch = true,
4393  RelativeSpacing = 0.02f
4394  };
4395 
4396  var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight),
4397  style: "GUIDragIndicator")
4398  {
4399  CanBeFocused = false
4400  };
4401 
4402  var subName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), frameContent.RectTransform),
4403  text: sub.DisplayName)
4404  {
4405  CanBeFocused = false
4406  };
4407 
4408  CreateSubmarineClassText(
4409  frameContent,
4410  sub,
4411  subName,
4412  list.Content);
4413  }
4414 
4415  foreach (var sub in GameMain.Client.ServerSubmarines
4416  .OrderBy(s => visibilityMenuOrder.Contains(s))
4417  .ThenBy(s => visibilityMenuOrder.IndexOf(s)))
4418  {
4419  addSubToList(sub,
4420  GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) ? hiddenSubsList : visibleSubsList);
4421  }
4422 
4423  void onRearranged(GUIListBox listBox, object userData)
4424  {
4425  visibilityMenuOrder.Clear();
4426  visibilityMenuOrder.AddRange(visibleSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo));
4427  visibilityMenuOrder.AddRange(hiddenSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo));
4428  }
4429 
4430  visibleSubsList.OnRearranged = onRearranged;
4431  hiddenSubsList.OnRearranged = onRearranged;
4432 
4433  void swapListItems(GUIListBox from, GUIListBox to)
4434  {
4435  to.Deselect();
4436  var selected = from.AllSelected.ToArray();
4437  int lastIndex = from.Content.GetChildIndex(selected.LastOrDefault());
4438  int nextIndex = lastIndex + 1;
4439  GUIComponent nextComponent = null;
4440  if (lastIndex >= 0 && nextIndex < from.Content.CountChildren)
4441  {
4442  nextComponent = from.Content.GetChild(nextIndex);
4443  }
4444  foreach (var frame in selected)
4445  {
4446  frame.Parent.RemoveChild(frame);
4447  frame.RectTransform.Parent = to.Content.RectTransform;
4448  }
4449  from.RecalculateChildren();
4450  from.RectTransform.RecalculateScale(true);
4451  to.RecalculateChildren();
4452  to.RectTransform.RecalculateScale(true);
4453  to.Select(selected);
4454  if (nextComponent != null) { from.Select(nextComponent.ToEnumerable()); }
4455  }
4456 
4457  centerSpacing();
4458  var visibleToHidden = centerButton("GUIButtonToggleRight");
4459  visibleToHidden.OnClicked = (button, o) =>
4460  {
4461  swapListItems(visibleSubsList, hiddenSubsList);
4462  return false;
4463  };
4464  var hiddenToVisible = centerButton("GUIButtonToggleLeft");
4465  hiddenToVisible.OnClicked = (button, o) =>
4466  {
4467  swapListItems(hiddenSubsList, visibleSubsList);
4468  return false;
4469  };
4470  centerSpacing();
4471 
4472  var buttonLayout
4473  = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.1f), messageBox.Content.RectTransform),
4474  isHorizontal: true)
4475  {
4476  RelativeSpacing = 0.01f
4477  };
4478  var cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform),
4479  TextManager.Get("Cancel"))
4480  {
4481  OnClicked = (button, o) =>
4482  {
4483  messageBox.Close();
4484  return false;
4485  }
4486  };
4487  var okButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform),
4488  TextManager.Get("OK"))
4489  {
4490  OnClicked = (button, o) =>
4491  {
4492  var hiddenSubs = GameMain.Client.ServerSettings.HiddenSubs;
4493  hiddenSubs.Clear();
4494  hiddenSubs.UnionWith(hiddenSubsList.Content.Children.Select(c => (c.UserData as SubmarineInfo).Name));
4495  GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.HiddenSubs);
4496  messageBox.Close();
4497  return false;
4498  }
4499  };
4500 
4501  new GUICustomComponent(new RectTransform(Vector2.Zero, messageBox.RectTransform),
4502  onUpdate: (f, component) =>
4503  {
4504  handleDraggingAcrossLists(visibleSubsList, hiddenSubsList);
4505  handleDraggingAcrossLists(hiddenSubsList, visibleSubsList);
4506  if (PlayerInput.PrimaryMouseButtonClicked()
4507  && !GUI.IsMouseOn(visibleToHidden)
4508  && !GUI.IsMouseOn(hiddenToVisible))
4509  {
4510  if (!GUI.IsMouseOn(hiddenSubsList)
4511  || !hiddenSubsList.Content.IsParentOf(GUI.MouseOn))
4512  {
4513  hiddenSubsList.Deselect();
4514  }
4515 
4516  if (!GUI.IsMouseOn(visibleSubsList)
4517  || !visibleSubsList.Content.IsParentOf(GUI.MouseOn))
4518  {
4519  visibleSubsList.Deselect();
4520  }
4521  }
4522  },
4523  onDraw: (spriteBatch, component) =>
4524  {
4525  visibleSubsList.DraggedElement?.DrawManually(spriteBatch, true, true);
4526  hiddenSubsList.DraggedElement?.DrawManually(spriteBatch, true, true);
4527  });
4528  }
4529 
4530  public void UpdateSubVisibility()
4531  {
4532  foreach (GUIComponent child in SubList.Content.Children)
4533  {
4534  if (child.UserData is not SubmarineInfo sub) { continue; }
4535  child.Visible =
4536  (!GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name)
4537  || (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.Name.Equals(sub.Name, StringComparison.OrdinalIgnoreCase)))
4538  && (string.IsNullOrEmpty(subSearchBox.Text) || sub.DisplayName.Contains(subSearchBox.Text, StringComparison.OrdinalIgnoreCase));
4539  }
4540  }
4541 
4542  public void OnRoundEnded()
4543  {
4544  CampaignCharacterDiscarded = false;
4545  }
4546  }
4547 }
Stores information about the Character that is needed between rounds in the menu etc....
void RecreateHead(ImmutableHashSet< Identifier > tags, int hairIndex, int beardIndex, int moustacheIndex, int faceAttachmentIndex)
bool OmitJobInMenus
Can be used to disable displaying the job in any info panels
readonly ChatManager ChatManager
Definition: ChatBox.cs:18
A class used for handling special key actions in chat boxes. For example tab completion or up/down ar...
Definition: ChatManager.cs:14
static void RegisterKeys(GUITextBox element, ChatManager manager)
Registers special input actions to the selected input field
Definition: ChatManager.cs:53
void Clear()
Call this function whenever we should stop doing special stuff and return normal behavior....
Definition: ChatManager.cs:112
static CoroutineStatus Running
static CoroutineStatus Success
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
override bool Enabled
Definition: GUIButton.cs:27
GUITextBlock TextBlock
Definition: GUIButton.cs:11
GUIComponent GetChild(int index)
Definition: GUIComponent.cs:54
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
void InheritTotalChildrenMinHeight()
Sets the minimum height of the transfrom to equal to the sum of the minimum heights of the children (...
GUIComponent GetChildByUserData(object obj)
Definition: GUIComponent.cs:66
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual RichString ToolTip
int GetChildIndex(GUIComponent child)
Definition: GUIComponent.cs:60
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
GUIComponent that can be used to render custom content on the UI
Vector2 MeasureString(LocalizedString str, bool removeExtraSpacing=false)
Definition: GUIPrefab.cs:284
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1249
OnSelectedHandler OnSelected
Definition: GUIListBox.cs:17
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
void Select(object userData, Force force=Force.No, AutoScroll autoScroll=AutoScroll.Enabled)
Definition: GUIListBox.cs:440
override GUIFont Font
Definition: GUITextBlock.cs:66
void CalculateHeightFromText(int padding=0, bool removeExtraSpacing=false)
bool AutoScaleHorizontal
When enabled, the text is automatically scaled down to fit the textblock horizontally.
static void AutoScaleAndNormalize(params GUITextBlock[] textBlocks)
Set the text scale of the GUITextBlocks so that they all use the same scale and can fit the text with...
List< ClickableArea > ClickableAreas
ImmutableArray< RichTextData >? RichTextData
TextBoxEvent OnDeselected
Definition: GUITextBox.cs:17
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
TextBoxEvent OnSelected
Definition: GUITextBox.cs:16
GUITextBlock TextBlock
Definition: GUITickBox.cs:93
override bool Enabled
Definition: GUITickBox.cs:40
override bool Selected
Definition: GUITickBox.cs:18
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetLobbyScreen NetLobbyScreen
Definition: GameMain.cs:55
static RasterizerState ScissorTestEnable
Definition: GameMain.cs:195
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static bool IsSingleplayer
Definition: GameMain.cs:34
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static GameModePreset PvP
static GameModePreset MultiPlayerCampaign
readonly Identifier Identifier
IEnumerable< Skill > GetSkills()
Definition: Job.cs:84
LocalizedString Name
Definition: Job.cs:14
JobPrefab Prefab
Definition: Job.cs:18
static readonly PrefabCollection< JobPrefab > Prefabs
readonly ImmutableDictionary< int, ImmutableArray< PreviewItem > > PreviewItems
Sprite GetPortrait(int randomSeed)
static LocationType Random(Random rand, int? zone=null, bool requireOutpost=false, Func< LocationType, bool > predicate=null)
Mersenne Twister based random
Definition: MTRandom.cs:9
readonly string StringRepresentation
Definition: Md5Hash.cs:32
readonly string ShortRepresentation
Definition: Md5Hash.cs:33
static string GetShortHash(string fullHash)
Definition: Md5Hash.cs:52
bool StartGameClicked(GUIButton button, object userdata)
readonly HashSet< Identifier > TagSet
static MultiplayerPreferences Instance
CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu
void UpdateSubList(GUIComponent subList, IEnumerable< SubmarineInfo > submarines)
static GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag=null, GUIFont font=null)
static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag=null)
static IEnumerable< CoroutineStatus > WaitForStartRound(GUIButton startButton)
void ShowPlayerContextMenu(GUITextBlock component, GUITextBlock.ClickableArea area)
IReadOnlyList< SubmarineInfo > GetSubList()
void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo)
override void AddToGUIUpdateList()
By default, submits the screen's main GUIFrame and, if requested upon construction,...
static GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag=null)
void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally)
static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step=null, Vector2? range=null)
static Client ExtractClientFromClickableArea(GUITextBlock.ClickableArea area)
void CreatePlayerFrame(GUIComponent parent, bool createPendingText=true, bool alwaysAllowEditing=false)
bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData)
bool TrySelectSub(string subName, string md5Hash, GUIListBox subList)
bool VotableClicked(GUIComponent component, object userData)
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area)
readonly HashSet< DebugConsole.Command > PermittedConsoleCommands
void SetPermissions(ClientPermissions permissions, IEnumerable< Identifier > permittedConsoleCommands)
Option< AccountId > AccountId
The ID of the account used to authenticate this session. This value can be used as a persistent value...
readonly byte SessionId
An ID for this client for the current session. THIS IS NOT A PERSISTENT VALUE. DO NOT STORE THIS LONG...
IReadOnlyList< FileTransferIn > ActiveTransfers
void VoteForKick(Client votedClient)
Definition: GameClient.cs:2705
void Vote(VoteType voteType, object data)
Definition: GameClient.cs:2684
void RequestFile(FileTransferType fileType, string file, string fileHash)
Definition: GameClient.cs:2415
bool TypingChatMessage(GUITextBox textBox, string text)
Definition: GameClient.cs:3065
override IReadOnlyList< Client > ConnectedClients
Definition: GameClient.cs:133
void UpdateClientPermissions(Client targetClient)
Definition: GameClient.cs:2809
bool HasPermission(ClientPermissions permission)
Definition: GameClient.cs:2620
readonly FileReceiver FileReceiver
Definition: GameClient.cs:156
void RequestSelectSub(SubmarineInfo sub, bool isShuttle)
Tell the server to select a submarine (permission required)
Definition: GameClient.cs:2868
void RequestSelectMode(int modeIndex)
Tell the server to select a mode (permission required)
Definition: GameClient.cs:2883
readonly List< SubmarineInfo > ServerSubmarines
Definition: GameClient.cs:94
override void UnbanPlayer(string playerName)
Definition: GameClient.cs:2789
bool JoinOnGoingClicked(GUIButton button, object _)
Definition: GameClient.cs:2950
void CreateKickReasonPrompt(string clientName, bool ban)
Definition: GameClient.cs:3425
bool EnterChatMessage(GUITextBox textBox, string message)
Definition: GameClient.cs:3070
bool SetReadyToStart(GUITickBox tickBox)
Definition: GameClient.cs:2981
static readonly List< PermissionPreset > List
readonly HashSet< DebugConsole.Command > PermittedCommands
readonly ClientPermissions Permissions
IEnumerable< Identifier > GetPlayStyleTags()
Definition: ServerInfo.cs:429
static ServerInfo FromServerEndpoints(ImmutableArray< Endpoint > endpoints, ServerSettings serverSettings)
Definition: ServerInfo.cs:104
float ReplaceCostPercentage
Percentage modifier for the cost of hiring a new character to replace a permanently killed one.
float SkillLossPercentageOnImmediateRespawn
How much more the skills drop towards the job's default skill levels when dying, in addition to Skill...
bool IronmanMode
This is an optional setting for permadeath mode. When it's enabled, a player client whose character d...
float SkillLossPercentageOnDeath
How much skills drop towards the job's default skill levels when dying
bool AllowBotTakeoverOnPermadeath
Are players allowed to take over bots when permadeath is enabled?
static IReadOnlyList< string > GetCaptureDeviceNames()
Definition: VoipCapture.cs:170
static void UpdateVoiceIndicator(GUIImage soundIcon, float voipAmplitude, float deltaTime)
Definition: VoipClient.cs:172
Prefab(ContentFile file, Identifier identifier)
Definition: Prefab.cs:40
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
Point?? MinSize
Min size in pixels. Does not affect scaling.
Point NonScaledSize
Size before scale multiplications.
Point?? MaxSize
Max size in pixels. Does not affect scaling.
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
static IEnumerable< SubmarineInfo > SavedSubmarines
static bool PendingChanges
Definition: TabMenu.cs:15
void ResetVotes(IEnumerable< Client > connectedClients)
NumberType
Definition: Enums.cs:715