Client LuaCsForBarotrauma
TabMenu.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using Microsoft.Xna.Framework;
5 using Microsoft.Xna.Framework.Graphics;
6 using System.Linq;
8 using System.Globalization;
10 
11 namespace Barotrauma
12 {
13  class TabMenu
14  {
15  public static bool PendingChanges = false;
16 
17  private static bool initialized = false;
18 
19  private static UISprite spectateIcon, disconnectedIcon;
20  private static Sprite ownerIcon, moderatorIcon;
21 
22  public enum InfoFrameTab { Crew, Mission, Reputation, Submarine, Talents };
23  public static InfoFrameTab SelectedTab { get; private set; }
24  private GUIFrame infoFrame, contentFrame;
25 
26  private readonly List<GUIButton> tabButtons = new List<GUIButton>();
27  private GUIFrame infoFrameHolder;
28  private List<LinkedGUI> linkedGUIList;
29  private GUIListBox logList;
30  private GUIListBox[] crewListArray;
31  private float sizeMultiplier = 1f;
32 
33  private IEnumerable<Character> crew;
34  private List<CharacterTeamType> teamIDs;
35  private const string inLobbyString = "\u2022 \u2022 \u2022";
36 
37  public static GUIFrame PendingChangesFrame = null;
38 
39  public static Color OwnCharacterBGColor = Color.Gold * 0.7f;
40  private bool isTransferMenuOpen;
41  private bool isSending;
42  private GUIComponent transferMenu;
43  private GUIButton transferMenuButton;
44  private float transferMenuOpenState;
45  private bool transferMenuStateCompleted;
46  private readonly HashSet<Identifier> registeredEvents = new HashSet<Identifier>();
47  private readonly TalentMenu talentMenu = new TalentMenu();
48 
49  private class LinkedGUI
50  {
51  private const ushort lowPingThreshold = 100;
52  private const ushort mediumPingThreshold = 200;
53 
54  public readonly Client Client;
55 
56  private ushort currentPing;
57  private readonly Character character;
58  private readonly bool wasCharacterAlive;
59  private readonly GUITextBlock textBlock;
60  private readonly GUIFrame frame;
61 
62  private readonly GUIImage permissionIcon;
63 
64  public LinkedGUI(Client client, GUIFrame frame, GUITextBlock textBlock, GUIImage permissionIcon)
65  {
66  this.Client = client;
67  this.textBlock = textBlock;
68  this.frame = frame;
69  this.permissionIcon = permissionIcon;
70  character = client?.Character;
71  wasCharacterAlive = client?.Character != null && !client.Character.IsDead;
72  }
73 
74  public LinkedGUI(Character character, GUIFrame frame, GUITextBlock textBlock)
75  {
76  this.character = character;
77  this.textBlock = textBlock;
78  this.frame = frame;
79  wasCharacterAlive = character != null && !character.IsDead;
80  }
81 
82  public bool HasMultiplayerCharacterChanged()
83  {
84  if (Client == null) { return false; }
85 
86  if (GameSettings.CurrentConfig.VerboseLogging)
87  {
88  if (Client.Character != character)
89  {
90  DebugConsole.Log($"Refreshing tab menu crew list (client \"{Client.Name}\"'s character changed from \"{character?.Name ?? "null"}\" to \"{Client.Character?.Name ?? "null"}\")");
91  }
92  }
93  return Client.Character != character;
94  }
95 
96  public bool HasCharacterDied()
97  {
98  if (character == null) { return false; }
99  bool isAlive = !(character?.IsDead ?? true);
100  if (GameSettings.CurrentConfig.VerboseLogging)
101  {
102  if (wasCharacterAlive && !isAlive)
103  {
104  DebugConsole.Log(Client == null ?
105  $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" died)" :
106  $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" died)");
107  }
108  else if (!wasCharacterAlive && isAlive)
109  {
110  DebugConsole.Log(Client == null ?
111 
112  $"Refreshing tab menu crew list (character \"{character?.Name ?? "null"}\" came back to life)" :
113  $"Refreshing tab menu crew list (client \"{Client.Name}\"'s character \"{character?.Name ?? "null"}\" came back to life)");
114  }
115  }
116  return isAlive != wasCharacterAlive;
117  }
118 
119  public void TryPingRefresh()
120  {
121  if (Client == null) { return; }
122  if (currentPing == Client.Ping) { return; }
123  currentPing = Client.Ping;
124  textBlock.Text = currentPing.ToString();
125  textBlock.TextColor = GetPingColor();
126  }
127 
128  public void TryPermissionIconRefresh(Sprite icon)
129  {
130  if (Client == null || permissionIcon == null) { return; }
131  permissionIcon.Sprite = icon;
132  }
133 
134  private Color GetPingColor()
135  {
136  if (currentPing < lowPingThreshold)
137  {
138  return GUIStyle.Green;
139  }
140  else if (currentPing < mediumPingThreshold)
141  {
142  return GUIStyle.Yellow;
143  }
144  else
145  {
146  return GUIStyle.Red;
147  }
148  }
149 
150  public void Remove(GUIFrame parent)
151  {
152  parent.RemoveChild(frame);
153  }
154  }
155 
156  public void Initialize()
157  {
158  spectateIcon = GUIStyle.GetComponentStyle("SpectateIcon").Sprites[GUIComponent.ComponentState.None][0];
159  disconnectedIcon = GUIStyle.GetComponentStyle("DisconnectedIcon").Sprites[GUIComponent.ComponentState.None][0];
160  ownerIcon = GUIStyle.GetComponentStyle("OwnerIcon").GetDefaultSprite();
161  moderatorIcon = GUIStyle.GetComponentStyle("ModeratorIcon").GetDefaultSprite();
162  initialized = true;
163  }
164 
165  public TabMenu()
166  {
167  if (!initialized) { Initialize(); }
168  if (Level.Loaded == null)
169  {
170  //make sure we're not trying to view e.g. mission or reputation info if the tab menu is opened in the test mode
171  SelectedTab = InfoFrameTab.Crew;
172  }
173  CreateInfoFrame(SelectedTab);
175  }
176 
177  public void Update(float deltaTime)
178  {
179  float menuOpenSpeed = deltaTime * 10f;
180  if (isTransferMenuOpen)
181  {
182  if (transferMenuStateCompleted)
183  {
184  transferMenuOpenState = transferMenuOpenState < 0.25f ? Math.Min(0.25f, transferMenuOpenState + (menuOpenSpeed / 2f)) : 0.25f;
185  }
186  else
187  {
188  if (transferMenuOpenState > 0.15f)
189  {
190  transferMenuStateCompleted = false;
191  transferMenuOpenState = Math.Max(0.15f, transferMenuOpenState - menuOpenSpeed);
192  }
193  else
194  {
195  transferMenuStateCompleted = true;
196  }
197  }
198  }
199  else
200  {
201  transferMenuStateCompleted = false;
202  if (transferMenuOpenState < 1f)
203  {
204  transferMenuOpenState = Math.Min(1f, transferMenuOpenState + menuOpenSpeed);
205  }
206  }
207 
208  if (transferMenu != null && transferMenuButton != null)
209  {
210  int pos = (int)(transferMenuOpenState * -transferMenu.Rect.Height);
211  transferMenu.RectTransform.AbsoluteOffset = new Point(0, pos);
212  transferMenuButton.RectTransform.AbsoluteOffset = new Point(0, -pos - transferMenu.Rect.Height);
213  }
214  GameSession.UpdateTalentNotificationIndicator(talentPointNotification);
215 
216  talentMenu?.Update();
217 
218  if (SelectedTab != InfoFrameTab.Crew) { return; }
219  if (linkedGUIList == null) { return; }
220 
222  {
223  for (int i = 0; i < linkedGUIList.Count; i++)
224  {
225  linkedGUIList[i].TryPingRefresh();
226  linkedGUIList[i].TryPermissionIconRefresh(GetPermissionIcon(linkedGUIList[i].Client));
227  if (linkedGUIList[i].HasMultiplayerCharacterChanged() || linkedGUIList[i].HasCharacterDied())
228  {
229  RemoveCurrentElements();
230  CreateMultiPlayerList(true);
231  return;
232  }
233  }
234  }
235  else
236  {
237  for (int i = 0; i < linkedGUIList.Count; i++)
238  {
239  if (linkedGUIList[i].HasCharacterDied())
240  {
241  RemoveCurrentElements();
242  CreateSinglePlayerList(true);
243  return;
244  }
245  }
246  }
247  }
248 
249  public void AddToGUIUpdateList()
250  {
251  infoFrame?.AddToGUIUpdateList();
253  }
254 
255  public static void OnRoundEnded()
256  {
257  storedMessages.Clear();
258  PendingChanges = false;
259  }
260 
261  private void CreateInfoFrame(InfoFrameTab selectedTab)
262  {
263  tabButtons.Clear();
264 
265  infoFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null);
266  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, infoFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
267 
268  //this used to be a switch expression but i changed it because it killed enc :(
269  //now it's not even a switch statement anymore :(
270  Vector2 contentFrameSize = new Vector2(0.45f, 0.667f);
271  contentFrame = new GUIFrame(new RectTransform(contentFrameSize, infoFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.12f) });
272 
273  var horizontalLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.958f, 0.943f), contentFrame.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, GUI.IntScale(25f)) }, isHorizontal: true)
274  {
275  RelativeSpacing = 0.01f
276  };
277 
278  var buttonArea = new GUILayoutGroup(new RectTransform(new Vector2(0.07f, 1f), parent: horizontalLayoutGroup.RectTransform), isHorizontal: false)
279  {
280  AbsoluteSpacing = GUI.IntScale(5f)
281  };
282  var innerLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.92f, 1f), horizontalLayoutGroup.RectTransform))
283  {
284  RelativeSpacing = 0.01f,
285  Stretch = true
286  };
287 
288  float absoluteSpacing = innerLayoutGroup.RelativeSpacing * innerLayoutGroup.Rect.Height;
289  int multiplier = GameMain.GameSession?.GameMode is CampaignMode ? 2 : 1;
290  int infoFrameHolderHeight = Math.Min((int)(0.97f * innerLayoutGroup.Rect.Height), (int)(innerLayoutGroup.Rect.Height - multiplier * (GUI.IntScale(15f) + absoluteSpacing)));
291  infoFrameHolder = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: null);
292 
293  GUIButton createTabButton(InfoFrameTab tab, string textTag)
294  {
295  var newButton = new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.BothWidth), style: $"InfoFrameTabButton.{tab}")
296  {
297  UserData = tab,
298  ToolTip = TextManager.Get(textTag),
299  OnClicked = (btn, userData) => { SelectInfoFrameTab((InfoFrameTab)userData); return true; }
300  };
301  tabButtons.Add(newButton);
302  return newButton;
303  }
304 
305  var crewButton = createTabButton(InfoFrameTab.Crew, "crew");
306 
307  if (GameMain.GameSession?.GameMode is not TestGameMode)
308  {
309  var missionBtn = createTabButton(InfoFrameTab.Mission, "mission");
310  eventLogNotification = GameSession.CreateNotificationIcon(missionBtn);
311  eventLogNotification.Visible = GameMain.GameSession?.EventManager?.EventLog?.UnreadEntries ?? false;
312  if (eventLogNotification.Visible)
313  {
314  eventLogNotification.Pulsate(Vector2.One, Vector2.One * 2, 1.0f);
315  }
316  }
317 
318  if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
319  {
320  var reputationButton = createTabButton(InfoFrameTab.Reputation, "reputation");
321 
322  var balanceFrame = new GUIFrame(new RectTransform(new Point(innerLayoutGroup.Rect.Width, innerLayoutGroup.Rect.Height - infoFrameHolderHeight), parent: innerLayoutGroup.RectTransform), style: "InnerFrame");
323  GUILayoutGroup salaryFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), balanceFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
324 
325  GUIScrollBar salaryScrollBar = null;
326  GUITextBlock salaryPercentage = null;
327  if (GameMain.GameSession?.GameMode is MultiPlayerCampaign)
328  {
329  float value = campaignMode.Bank.RewardDistribution;
330  GUITextBlock salaryText = new GUITextBlock(new RectTransform(new Vector2(0.25f, 1f), salaryFrame.RectTransform), TextManager.Get("defaultsalary"), textAlignment: Alignment.Center)
331  {
332  AutoScaleHorizontal = true
333  };
334  salaryScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.4f, 1f), salaryFrame.RectTransform), barSize: 0.1f, style: "GUISlider")
335  {
336  Range = new Vector2(0, 1),
337  BarScrollValue = value / 100f,
338  Step = 0.01f,
339  BarSize = 0.1f,
340  };
341 
342  salaryPercentage = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1f), salaryFrame.RectTransform), "0", textAlignment: Alignment.Center)
343  {
344  Text = ValueToPercentage(RoundRewardDistribution(salaryScrollBar.BarScroll, salaryScrollBar.Step))
345  };
346 
347  salaryScrollBar.OnMoved = (scrollBar, value) =>
348  {
349  salaryPercentage.Text = ValueToPercentage(RoundRewardDistribution(value, scrollBar.Step));
350  return true;
351  };
352  salaryScrollBar.OnReleased = (bar, scroll) =>
353  {
354  int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
355  SetRewardDistribution(Option.None, newRewardDistribution);
356  return true;
357  };
358 
359  var resetButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1f), salaryFrame.RectTransform), TextManager.Get("ResetSalaries"), style: "GUIButtonSmall")
360  {
361  TextBlock = { AutoScaleHorizontal = true },
362  ToolTip = TextManager.Get("resetsalaries.tooltip"),
363  OnClicked = (button, userData) =>
364  {
365  GUI.AskForConfirmation(TextManager.Get("ResetSalaries"), TextManager.Get("ResetSalaries.Warning"), onConfirm: ResetRewardDistributions);
366  return true;
367  }
368  };
369 
370  void UpdateSliderEnabled()
371  => salaryScrollBar.Enabled = resetButton.Enabled = CampaignMode.AllowedToManageWallets();
372  UpdateSliderEnabled();
373 
374  Identifier defaultSalaryEventIdentifier = "DefaultSalarySlider".ToIdentifier();
375  GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(defaultSalaryEventIdentifier, _ => UpdateSliderEnabled());
376  }
377  GUITextBlock balanceText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), balanceFrame.RectTransform, Anchor.TopRight), string.Empty, textAlignment: Alignment.Right);
378  if (GameMain.IsMultiplayer)
379  {
380  balanceText.ToolTip = TextManager.Get("bankdescription");
381  }
382  GUIFrame bottomDisclaimerFrame = new GUIFrame(new RectTransform(new Vector2(contentFrameSize.X, 0.1f), infoFrame.RectTransform)
383  {
384  AbsoluteOffset = new Point(contentFrame.Rect.X, contentFrame.Rect.Bottom + GUI.IntScale(8))
385  }, style: null);
386 
387  PendingChangesFrame = new GUIFrame(new RectTransform(Vector2.One, bottomDisclaimerFrame.RectTransform, Anchor.Center), style: null);
388 
389  if (GameMain.NetLobbyScreen?.CampaignCharacterDiscarded ?? false)
390  {
391  NetLobbyScreen.CreateChangesPendingFrame(PendingChangesFrame);
392  }
393 
394  SetBalanceText(balanceText, campaignMode.Bank.Balance);
395  Identifier eventIdentifier = nameof(CreateInfoFrame).ToIdentifier();
396  campaignMode.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
397  {
398  if (!e.Owner.IsNone()) { return; }
399  SetBalanceText(balanceText, e.Wallet.Balance);
400 
401  if (salaryPercentage is not null && salaryScrollBar is not null)
402  {
403  float rewardDistribution = e.Wallet.RewardDistribution;
404  salaryScrollBar.BarScrollValue = rewardDistribution / 100f;
405  salaryPercentage.Text = ValueToPercentage(rewardDistribution);
406  }
407  });
408  registeredEvents.Add(eventIdentifier);
409 
410  static void SetBalanceText(GUITextBlock text, int balance)
411  {
412  text.Text = TextManager.GetWithVariable("bankbalanceformat", "[money]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", balance));
413  }
414 
415  LocalizedString ValueToPercentage(float value)
416  => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(value)}");
417  }
418 
419  if (Submarine.MainSub != null)
420  {
421  createTabButton(InfoFrameTab.Submarine, "submarine");
422  }
423 
424  var talentsButton = createTabButton(InfoFrameTab.Talents, "tabmenu.character");
425  talentsButton.OnAddedToGUIUpdateList += (component) =>
426  {
427  talentsButton.Enabled = Character.Controlled?.Info != null || GameMain.Client?.CharacterInfo != null;
428  if (!talentsButton.Enabled && selectedTab == InfoFrameTab.Talents)
429  {
431  }
432  };
433 
434  talentPointNotification = GameSession.CreateNotificationIcon(talentsButton);
435  }
436 
437  public void SelectInfoFrameTab(InfoFrameTab selectedTab)
438  {
439  SelectedTab = selectedTab;
440 
441  CreateInfoFrame(selectedTab);
442  tabButtons.ForEach(tb => tb.Selected = (InfoFrameTab)tb.UserData == selectedTab);
443 
444  switch (selectedTab)
445  {
446  case InfoFrameTab.Crew:
447  CreateCrewListFrame(infoFrameHolder);
448  break;
449  case InfoFrameTab.Mission:
450  CreateMissionInfo(infoFrameHolder);
451  break;
452  case InfoFrameTab.Reputation:
453  if (GameMain.GameSession?.RoundSummary != null && GameMain.GameSession?.GameMode is CampaignMode campaignMode)
454  {
455  infoFrameHolder.ClearChildren();
456  GUIFrame reputationFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrameHolder.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
457  GameMain.GameSession.RoundSummary.CreateReputationInfoPanel(reputationFrame, campaignMode);
458  }
459  break;
460  case InfoFrameTab.Submarine:
461  CreateSubmarineInfo(infoFrameHolder, Submarine.MainSub);
462  break;
463  case InfoFrameTab.Talents:
464  talentMenu.CreateGUI(infoFrameHolder, Character.Controlled ?? GameMain.Client?.Character);
465  break;
466  }
467  }
468 
469  private const float JobColumnWidthPercentage = 0.138f,
470  CharacterColumnWidthPercentage = 0.45f,
471  KillColumnWidthPercentage = 0.1f,
472  DeathColumnWidthPercentage = 0.1f,
473  PingColumnWidthPercentage = 0.15f,
474  WalletColumnWidthPercentage = 0.206f;
475 
476  private int jobColumnWidth, characterColumnWidth, pingColumnWidth, walletColumnWidth, deathColumnWidth, killColumnWidth;
477 
478  private void CreateCrewListFrame(GUIFrame crewFrame)
479  {
480  crew = GameMain.GameSession?.CrewManager?.GetCharacters() ?? new List<Character>() { TestScreen.dummyCharacter};
481  teamIDs = crew.Select(c => c.TeamID).Distinct().ToList();
482 
483  // Show own team first when there's more than one team
484  if (teamIDs.Count > 1 && GameMain.Client?.Character != null)
485  {
486  CharacterTeamType ownTeam = GameMain.Client.Character.TeamID;
487  teamIDs = teamIDs.OrderBy(i => i != ownTeam).ThenBy(i => i).ToList();
488  }
489 
490  if (!teamIDs.Any()) { teamIDs.Add(CharacterTeamType.None); }
491 
492  var content = new GUILayoutGroup(new RectTransform(Vector2.One, crewFrame.RectTransform));
493 
494  crewListArray = new GUIListBox[teamIDs.Count];
495  GUILayoutGroup[] headerFrames = new GUILayoutGroup[teamIDs.Count];
496 
497  float nameHeight = 0.075f;
498 
499  Vector2 crewListSize = new Vector2(1f, 1f / teamIDs.Count - (teamIDs.Count > 1 ? nameHeight * 1.1f : 0f));
500  for (int i = 0; i < teamIDs.Count; i++)
501  {
502  if (teamIDs.Count > 1)
503  {
504  var nameText = new GUITextBlock(new RectTransform(new Vector2(1.0f, nameHeight), content.RectTransform), CombatMission.GetTeamName(teamIDs[i]), textColor: CombatMission.GetTeamColor(teamIDs[i]))
505  {
507  };
508  var teamIcon = new GUIImage(new RectTransform(Vector2.One, nameText.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.BothHeight),
509  style: teamIDs[i] == CharacterTeamType.Team2 ? "SeparatistIcon" : "CoalitionIcon")
510  {
511  Color = nameText.TextColor
512  };
513  nameText.Padding = new Vector4(teamIcon.Rect.Width + nameText.Padding.X, nameText.Padding.Y, nameText.Padding.Z, nameText.Padding.W);
514  }
515 
516  headerFrames[i] = new GUILayoutGroup(new RectTransform(Vector2.Zero, content.RectTransform, Anchor.TopLeft, Pivot.BottomLeft) { AbsoluteOffset = new Point(2, -1) }, isHorizontal: true)
517  {
518  Stretch = true,
519  AbsoluteSpacing = 2,
520  UserData = i
521  };
522 
523  GUIListBox crewList = new GUIListBox(new RectTransform(crewListSize, content.RectTransform))
524  {
525  Padding = new Vector4(2, 5, 0, 0),
526  AutoHideScrollBar = false,
527  PlaySoundOnSelect = true
528  };
529  crewList.UpdateDimensions();
530 
531  if (teamIDs.Count > 1)
532  {
533  crewList.OnSelected = (component, obj) =>
534  {
535  for (int i = 0; i < crewListArray.Length; i++)
536  {
537  if (crewListArray[i] == crewList) continue;
538  crewListArray[i].Deselect();
539  }
540  SelectElement(component.UserData, crewList);
541  return true;
542  };
543  }
544  else
545  {
546  crewList.OnSelected = (component, obj) =>
547  {
548  SelectElement(component.UserData, crewList);
549  return true;
550  };
551  }
552 
553  crewListArray[i] = crewList;
554  }
555 
556  for (int i = 0; i < teamIDs.Count; i++)
557  {
558  headerFrames[i].RectTransform.RelativeSize = new Vector2(1f - crewListArray[i].ScrollBar.Rect.Width / (float)crewListArray[i].Rect.Width, GUIStyle.HotkeyFont.Size / (float)crewFrame.RectTransform.Rect.Height * 1.5f);
559 
560  if (!GameMain.IsMultiplayer)
561  {
562  CreateSinglePlayerListContentHolder(headerFrames[i]);
563  }
564  else
565  {
566  CreateMultiPlayerListContentHolder(headerFrames[i]);
567  }
568  }
569 
570  crewFrame.RectTransform.AbsoluteOffset = new Point(0, (int)(headerFrames[0].Rect.Height * headerFrames.Length) - (teamIDs.Count > 1 ? GUI.IntScale(10f) : 0));
571 
572  float totalRelativeHeight = 0.0f;
573  if (teamIDs.Count > 1) { totalRelativeHeight += teamIDs.Count * nameHeight; }
574  headerFrames.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y);
575  crewListArray.ForEach(f => totalRelativeHeight += f.RectTransform.RelativeSize.Y);
576  if (totalRelativeHeight > 1.0f)
577  {
578  float heightOverflow = totalRelativeHeight - 1.0f;
579  float heightToReduce = heightOverflow / crewListArray.Length;
580  crewListArray.ForEach(l =>
581  {
582  l.RectTransform.Resize(l.RectTransform.RelativeSize - new Vector2(0.0f, heightToReduce));
583  l.UpdateDimensions();
584  });
585  }
586 
587  if (GameMain.IsMultiplayer)
588  {
589  CreateMultiPlayerList(false);
590  CreateMultiPlayerLogContent(crewFrame);
591  }
592  else
593  {
594  CreateSinglePlayerList(false);
595  }
596  }
597 
598  private void CreateSinglePlayerListContentHolder(GUILayoutGroup headerFrame)
599  {
600  GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
601  GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(0f, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
602 
603  sizeMultiplier = (headerFrame.Rect.Width - headerFrame.AbsoluteSpacing * (headerFrame.CountChildren - 1)) / (float)headerFrame.Rect.Width;
604 
605  jobButton.RectTransform.RelativeSize = new Vector2(JobColumnWidthPercentage * sizeMultiplier, 1f);
606  characterButton.RectTransform.RelativeSize = new Vector2((1f - JobColumnWidthPercentage * sizeMultiplier) * sizeMultiplier, 1f);
607 
608  jobButton.TextBlock.Font = characterButton.TextBlock.Font = GUIStyle.HotkeyFont;
609  jobButton.CanBeFocused = characterButton.CanBeFocused = false;
610  jobButton.TextBlock.ForceUpperCase = characterButton.TextBlock.ForceUpperCase = ForceUpperCase.Yes;
611 
612  jobColumnWidth = jobButton.Rect.Width;
613  characterColumnWidth = characterButton.Rect.Width;
614  }
615 
616  private void CreateSinglePlayerList(bool refresh)
617  {
618  if (refresh)
619  {
620  crew = GameMain.GameSession.CrewManager.GetCharacters();
621  }
622 
623  linkedGUIList = new List<LinkedGUI>();
624 
625  for (int i = 0; i < teamIDs.Count; i++)
626  {
627  foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
628  {
629  CreateSinglePlayerCharacterElement(character, i);
630  }
631  }
632  }
633 
634  private void CreateSinglePlayerCharacterElement(Character character, int i)
635  {
636  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement")
637  {
638  UserData = character,
639  Color = (Character.Controlled == character) ? OwnCharacterBGColor : Color.Transparent
640  };
641 
642  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
643  {
644  AbsoluteSpacing = 2,
645  Stretch = true
646  };
647 
648  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
649  {
650  CanBeFocused = false,
651  HoverColor = Color.White,
652  SelectedColor = Color.White
653  };
654 
655  GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
656  ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
657 
658  paddedFrame.Recalculate();
659 
660  linkedGUIList.Add(new LinkedGUI(character, frame, textBlock: null));
661  }
662 
663  private void CreateMultiPlayerListContentHolder(GUILayoutGroup headerFrame)
664  {
665  bool isCampaign = GameMain.GameSession?.Campaign is MultiPlayerCampaign;
666  GUIButton jobButton = new GUIButton(new RectTransform(new Vector2(JobColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("tabmenu.job"), style: "GUIButtonSmallFreeScale");
667  GUIButton characterButton = new GUIButton(new RectTransform(new Vector2(CharacterColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale");
668 
669  if (GameMain.GameSession?.GameMode is PvPMode)
670  {
671  var killButton = new GUIButton(new RectTransform(new Vector2(KillColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("killcount"), style: "GUIButtonSmallFreeScale");
672  killColumnWidth = killButton.Rect.Width;
673  var deathButton = new GUIButton(new RectTransform(new Vector2(DeathColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("deathcount"), style: "GUIButtonSmallFreeScale");
674  deathColumnWidth = deathButton.Rect.Width;
675  }
676 
677  GUIButton pingButton = new GUIButton(new RectTransform(new Vector2(PingColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("serverlistping"), style: "GUIButtonSmallFreeScale");
678  if (isCampaign)
679  {
680  GUIButton walletButton = new GUIButton(new RectTransform(new Vector2(WalletColumnWidthPercentage, 1f), headerFrame.RectTransform), TextManager.Get("crewwallet.wallet"), style: "GUIButtonSmallFreeScale")
681  {
682  TextBlock = { Font = GUIStyle.HotkeyFont },
683  CanBeFocused = false,
685  };
686  walletColumnWidth = walletButton.Rect.Width;
687  }
688 
689  foreach (var btn in headerFrame.GetAllChildren<GUIButton>())
690  {
691  btn.TextBlock.Font = GUIStyle.HotkeyFont;
692  btn.CanBeFocused = false;
693  btn.ForceUpperCase = ForceUpperCase.Yes;
694  }
695 
696  jobColumnWidth = jobButton.Rect.Width;
697  characterColumnWidth = characterButton.Rect.Width;
698  pingColumnWidth = pingButton.Rect.Width;
699  }
700 
701  private void CreateMultiPlayerList(bool refresh)
702  {
703  if (refresh)
704  {
705  crew = GameMain.GameSession.CrewManager.GetCharacters();
706  }
707 
708  linkedGUIList = new List<LinkedGUI>();
709 
710  var connectedClients = GameMain.Client.ConnectedClients;
711 
712  for (int i = 0; i < teamIDs.Count; i++)
713  {
714  foreach (Character character in crew.Where(c => c.TeamID == teamIDs[i]))
715  {
716  if (character is not AICharacter && connectedClients.Any(c => c.Character == null && c.Name == character.Name)) { continue; }
717  CreateMultiPlayerCharacterElement(character, GameMain.Client.PreviouslyConnectedClients.FirstOrDefault(c => c.Character == character), i);
718  }
719  }
720 
721  for (int j = 0; j < connectedClients.Count; j++)
722  {
723  Client client = connectedClients[j];
724  if (!client.InGame || client.Character == null || client.Character.IsDead)
725  {
726  CreateMultiPlayerClientElement(client);
727  }
728  }
729  }
730 
731  private void CreateMultiPlayerCharacterElement(Character character, Client client, int i)
732  {
733  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[i].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[i].Content.RectTransform), style: "ListBoxElement")
734  {
735  UserData = character,
736  Color = (GameMain.NetworkMember != null && GameMain.Client.Character == character) ? OwnCharacterBGColor : Color.Transparent
737  };
738 
739  frame.OnSecondaryClicked += (component, data) =>
740  {
741  NetLobbyScreen.CreateModerationContextMenu(client);
742  return true;
743  };
744 
745  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
746  {
747  AbsoluteSpacing = 2,
748  Stretch = true
749  };
750 
751  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => character.Info.DrawJobIcon(sb, component.Rect))
752  {
753  CanBeFocused = false,
754  HoverColor = Color.White,
755  SelectedColor = Color.White
756  };
757 
758  if (client != null)
759  {
760  CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
761 
762  if (GameMain.GameSession?.GameMode is PvPMode)
763  {
764  new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
765  {
766  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
767  };
768  new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
769  {
770  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
771  };
772  }
773 
774  linkedGUIList.Add(new LinkedGUI(client, frame,
775  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
776  permissionIcon));
777  }
778  else
779  {
780  GUITextBlock characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
781  ToolBox.LimitString(character.Info.Name, GUIStyle.Font, characterColumnWidth), textAlignment: Alignment.Center, textColor: character.Info.Job.Prefab.UIColor);
782 
783  if (GameMain.GameSession?.GameMode is PvPMode)
784  {
785  new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
786  {
787  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotKillCount(character.Info) ?? 0).ToString()
788  };
789  new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
790  {
791  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetBotDeathCount(character.Info) ?? 0).ToString()
792  };
793  }
794 
795  if (character is AICharacter)
796  {
797  linkedGUIList.Add(new LinkedGUI(character, frame,
798  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), TextManager.Get("tabmenu.bot"), textAlignment: Alignment.Center) { ForceUpperCase = ForceUpperCase.Yes }));
799  }
800  else
801  {
802  linkedGUIList.Add(new LinkedGUI(client: null, frame, textBlock: null, permissionIcon: null));
803 
804  new GUICustomComponent(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), onDraw: (sb, component) => DrawDisconnectedIcon(sb, component.Rect))
805  {
806  CanBeFocused = false,
807  HoverColor = Color.White,
808  SelectedColor = Color.White
809  };
810  }
811  }
812 
813  CreateWalletCrewFrame(character, paddedFrame);
814 
815  paddedFrame.Recalculate();
816  }
817 
818  private void CreateMultiPlayerClientElement(Client client)
819  {
820  int teamIndex = GetTeamIndex(client);
821  if (teamIndex == -1) teamIndex = 0;
822 
823  GUIFrame frame = new GUIFrame(new RectTransform(new Point(crewListArray[teamIndex].Content.Rect.Width, GUI.IntScale(33f)), crewListArray[teamIndex].Content.RectTransform), style: "ListBoxElement")
824  {
825  UserData = client,
826  Color = Color.Transparent
827  };
828 
829  frame.OnSecondaryClicked += (component, data) =>
830  {
831  NetLobbyScreen.CreateModerationContextMenu(client);
832  return true;
833  };
834 
835  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), frame.RectTransform, Anchor.Center), isHorizontal: true)
836  {
837  AbsoluteSpacing = 2,
838  Stretch = true
839  };
840 
841  new GUICustomComponent(new RectTransform(new Point(jobColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center),
842  onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client))
843  {
844  CanBeFocused = false,
845  HoverColor = Color.White,
846  SelectedColor = Color.White
847  };
848 
849  CreateNameWithPermissionIcon(client, paddedFrame, out GUIImage permissionIcon);
850 
851  if (GameMain.GameSession?.GameMode is PvPMode)
852  {
853  new GUITextBlock(new RectTransform(new Point(killColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
854  {
855  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientKillCount(client) ?? 0).ToString()
856  };
857  new GUITextBlock(new RectTransform(new Point(deathColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), string.Empty, textAlignment: Alignment.Center)
858  {
859  TextGetter = () => GameMain.GameSession.Missions.Sum(m => (m as CombatMission)?.GetClientDeathCount(client) ?? 0).ToString()
860  };
861  }
862 
863  linkedGUIList.Add(new LinkedGUI(client, frame,
864  new GUITextBlock(new RectTransform(new Point(pingColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform), client.Ping.ToString(), textAlignment: Alignment.Center),
865  permissionIcon));
866 
867  if (client.Character is { } character)
868  {
869  CreateWalletCrewFrame(character, paddedFrame);
870  }
871 
872  paddedFrame.Recalculate();
873  }
874 
875  private int GetTeamIndex(Client client)
876  {
877  if (teamIDs.Count <= 1) { return 0; }
878 
879  if (client.Character != null)
880  {
881  return teamIDs.IndexOf(client.Character.TeamID);
882  }
883 
884  if (client.CharacterID != 0)
885  {
886  foreach (Character c in crew)
887  {
888  if (client.CharacterID == c.ID)
889  {
890  return teamIDs.IndexOf(c.TeamID);
891  }
892  }
893  }
894  else
895  {
896  foreach (Character c in crew)
897  {
898  if (client.Name == c.Name)
899  {
900  return teamIDs.IndexOf(c.TeamID);
901  }
902  }
903  }
904 
905  return 0;
906  }
907 
908  private void CreateWalletCrewFrame(Character character, GUILayoutGroup paddedFrame)
909  {
910  if (GameMain.GameSession?.Campaign is not MultiPlayerCampaign) { return; }
911 
912  GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(new Point(walletColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.Center)
913  {
914  CanBeFocused = false
915  };
916 
917  GUILayoutGroup paddedLayoutGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 1f), walletLayout.RectTransform, Anchor.Center), isHorizontal: true)
918  {
919  Stretch = true
920  };
921 
922  new GUIFrame(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), style: null)
923  {
924  IgnoreLayoutGroups = true,
925  ToolTip = TextManager.Get("walletdescription")
926  };
927 
928  if (character.IsBot) { return; }
929 
930  Sprite walletSprite = GUIStyle.CrewWalletIconSmall.Value.Sprite;
931 
932  GUIImage icon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform, scaleBasis: ScaleBasis.BothHeight), walletSprite, scaleToFit: true) { CanBeFocused = false };
933  GUITextBlock walletBlock = new GUITextBlock(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), string.Empty, textAlignment: Alignment.Right, font: GUIStyle.Font)
934  {
935  AutoScaleHorizontal = true,
936  Padding = Vector4.Zero,
937  CanBeFocused = false
938  };
939 
940  GUIImage largeIcon = new GUIImage(new RectTransform(Vector2.One, paddedLayoutGroup.RectTransform), walletSprite, scaleToFit: true)
941  {
942  CanBeFocused = false,
943  IgnoreLayoutGroups = true,
944  Visible = false
945  };
946 
947  if (character.IsBot)
948  {
949  largeIcon.Visible = true;
950  icon.Visible = false;
951  walletBlock.Visible = false;
952  largeIcon.Enabled = false;
953  return;
954  }
955 
956  walletLayout.Recalculate();
957  paddedLayoutGroup.Recalculate();
958  SetWalletText(walletBlock, character.Wallet, icon, largeIcon);
959 
960  if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
961  {
962  Identifier eventIdentifier = new Identifier($"{nameof(CreateWalletCrewFrame)}.{character.ID}");
963  campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
964  {
965  if (!e.Owner.TryUnwrap(out var owner) || owner != character) { return; }
966  SetWalletText(walletBlock, e.Wallet, icon, largeIcon);
967  });
968  registeredEvents.Add(eventIdentifier);
969  }
970 
971  static void SetWalletText(GUITextBlock block, Wallet wallet, GUIImage icon, GUIImage largeIcon)
972  {
973  const int million = 1000000,
974  tooSmallPixelTreshold = 50; // 50 pixels is just not enough to see any meaningful info
975 
976  block.Text = TextManager.FormatCurrency(wallet.Balance);
977  block.ToolTip = string.Empty;
978 
979  if (wallet.Balance >= million)
980  {
981  block.Text = TextManager.Get("crewwallet.balance.toomuchtoshow");
982  block.ToolTip = block.Text;
983  }
984 
985  largeIcon.Visible = false;
986  icon.Visible = true;
987  block.Visible = true;
988 
989  if (tooSmallPixelTreshold > block.Rect.Width)
990  {
991  largeIcon.Visible = true;
992  icon.Visible = false;
993  block.Visible = false;
994  largeIcon.ToolTip = block.Text;
995  }
996  }
997  }
998 
999  private void CreateNameWithPermissionIcon(Client client, GUILayoutGroup paddedFrame, out GUIImage permissionIcon)
1000  {
1001  GUITextBlock characterNameBlock;
1002  Sprite permissionIconSprite = GetPermissionIcon(client);
1003  JobPrefab prefab = client.Character?.Info?.Job?.Prefab;
1004  Color nameColor = prefab != null ? prefab.UIColor : Color.White;
1005 
1006  Point iconSize = new Point((int)(paddedFrame.Rect.Height * 0.8f));
1007  float characterNameWidthAdjustment = (iconSize.X + paddedFrame.AbsoluteSpacing) / characterColumnWidth;
1008 
1009  characterNameBlock = new GUITextBlock(new RectTransform(new Point(characterColumnWidth, paddedFrame.Rect.Height), paddedFrame.RectTransform),
1010  ToolBox.LimitString(client.Name, GUIStyle.Font, (int)(characterColumnWidth - paddedFrame.Rect.Width * characterNameWidthAdjustment)), textAlignment: Alignment.Center, textColor: nameColor);
1011 
1012  float iconWidth = iconSize.X / (float)characterColumnWidth;
1013  int xOffset = (int)(jobColumnWidth + characterNameBlock.TextPos.X - GUIStyle.Font.MeasureString(characterNameBlock.Text).X / 2f - paddedFrame.AbsoluteSpacing - iconWidth * paddedFrame.Rect.Width);
1014  permissionIcon = new GUIImage(new RectTransform(new Vector2(iconWidth, 1f), paddedFrame.RectTransform) { AbsoluteOffset = new Point(xOffset + 2, 0) }, permissionIconSprite) { IgnoreLayoutGroups = true };
1015 
1016  if (client.Character != null && client.Character.IsDead)
1017  {
1018  characterNameBlock.Strikethrough = new GUITextBlock.StrikethroughSettings(null, GUI.IntScale(1f), GUI.IntScale(5f));
1019  }
1020  }
1021 
1022  private Sprite GetPermissionIcon(Client client)
1023  {
1024  if (GameMain.NetworkMember == null || client == null || !client.HasPermissions) { return null; }
1025 
1026  if (client.IsOwner) // Owner cannot be kicked
1027  {
1028  return ownerIcon;
1029  }
1030  else
1031  {
1032  return moderatorIcon;
1033  }
1034  }
1035 
1036  private void DrawNotInGameIcon(SpriteBatch spriteBatch, Rectangle area, Client client)
1037  {
1038  if (client.Spectating)
1039  {
1040  spectateIcon.Draw(spriteBatch, area, Color.White);
1041  }
1042  else if (client.Character != null && client.Character.IsDead)
1043  {
1044  client.Character.Info?.DrawJobIcon(spriteBatch, area);
1045  }
1046  else
1047  {
1048  Vector2 stringOffset = GUIStyle.Font.MeasureString(inLobbyString) / 2f;
1049  GUIStyle.Font.DrawString(spriteBatch, inLobbyString, area.Center.ToVector2() - stringOffset, Color.White);
1050  }
1051  }
1052 
1053  private void DrawDisconnectedIcon(SpriteBatch spriteBatch, Rectangle area)
1054  {
1055  disconnectedIcon.Draw(spriteBatch, area, GUIStyle.Red);
1056  }
1057 
1061  private bool SelectElement(object userData, GUIComponent crewList)
1062  {
1063  Character character = userData as Character;
1064  Client client = userData as Client;
1065 
1066  GUIComponent existingPreview = infoFrameHolder.FindChild("SelectedCharacter");
1067  if (existingPreview != null) { infoFrameHolder.RemoveChild(existingPreview); }
1068 
1069  GUIFrame background = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.69f), infoFrameHolder.RectTransform, Anchor.TopRight, Pivot.TopLeft) { RelativeOffset = new Vector2(-0.061f, 0) })
1070  {
1071  UserData = "SelectedCharacter"
1072  };
1073 
1074  if (character != null)
1075  {
1076  if (GameMain.Client is null)
1077  {
1078  GUIComponent preview = character.Info.CreateInfoFrame(background, false, null);
1079  }
1080  else
1081  {
1082  GUIComponent preview = character.Info.CreateInfoFrame(background, false, GetPermissionIcon(GameMain.Client.ConnectedClients.Find(c => c.Character == character)));
1083 
1084  GameMain.Client.SelectCrewCharacter(character, preview);
1085  if (!character.IsBot && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign) { CreateWalletFrame(background, character, mpCampaign); }
1086  }
1087 
1088  if (background.FindChild(TalentMenu.ManageBotTalentsButtonUserData, recursive: true) is GUIButton { Enabled: true } talentButton)
1089  {
1090  talentButton.OnClicked = (button, o) =>
1091  {
1092  talentMenu.CreateGUI(infoFrameHolder, character);
1093  return true;
1094  };
1095  }
1096  }
1097  else if (client != null)
1098  {
1099  GUIComponent preview = CreateClientInfoFrame(background, client, GetPermissionIcon(client));
1100  GameMain.Client?.SelectCrewClient(client, preview);
1101  if (client.Character != null && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
1102  {
1103  CreateWalletFrame(background, client.Character, mpCampaign);
1104  }
1105  }
1106 
1107  return true;
1108  }
1109 
1110  private void CreateWalletFrame(GUIComponent parent, Character character, MultiPlayerCampaign campaign)
1111  {
1112  if (campaign is null) { throw new ArgumentNullException(nameof(campaign), "Tried to create a wallet frame when campaign was null"); }
1113  if (character is null) { throw new ArgumentNullException(nameof(character), "Tried to create a wallet frame for a null character");}
1114  isTransferMenuOpen = false;
1115  transferMenuOpenState = 1f;
1116  ImmutableHashSet<Character> salaryCrew = GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != character).ToImmutableHashSet();
1117 
1118  Wallet targetWallet = character.Wallet;
1119 
1120  GUIFrame walletFrame = new GUIFrame(new RectTransform(new Vector2(1f, 0.35f), parent.RectTransform, anchor: Anchor.TopLeft)
1121  {
1122  RelativeOffset = new Vector2(0, 1.02f)
1123  });
1124 
1125  GUILayoutGroup walletLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(walletFrame.RectTransform, 0.9f), walletFrame.RectTransform, anchor: Anchor.Center));
1126 
1127  GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.33f), walletLayout.RectTransform), isHorizontal: true);
1128  GUIImage icon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "CrewWalletIconLarge", scaleToFit: true);
1129  float relativeX = icon.RectTransform.NonScaledSize.X / (float)icon.Parent.RectTransform.NonScaledSize.X;
1130  GUILayoutGroup headerTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - relativeX, 1f), headerLayout.RectTransform), isHorizontal: true) { Stretch = true };
1131  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.Get("crewwallet.wallet"), font: GUIStyle.LargeFont);
1132  GUIFrame walletTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, headerLayout.RectTransform), style: null)
1133  {
1134  IgnoreLayoutGroups = true,
1135  ToolTip = TextManager.Get("walletdescription")
1136  };
1137  GUITextBlock moneyBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), headerTextLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Right);
1138 
1139  GUILayoutGroup middleLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.66f), walletLayout.RectTransform));
1140  GUILayoutGroup salaryTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true);
1141  GUITextBlock salaryTitle = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), TextManager.Get("crewwallet.salary"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
1142  GUITextBlock rewardBlock = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), salaryTextLayout.RectTransform), string.Empty, textAlignment: Alignment.BottomRight);
1143  GUIFrame salaryTooltipFrame = new GUIFrame(new RectTransform(Vector2.One, middleLayout.RectTransform), style: null)
1144  {
1145  IgnoreLayoutGroups = true,
1146  ToolTip = TextManager.Get("crewwallet.salary.tooltip")
1147  };
1148  GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), middleLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center);
1149  GUIScrollBar salarySlider = new GUIScrollBar(new RectTransform(new Vector2(0.9f, 1f), sliderLayout.RectTransform), style: "GUISlider", barSize: 0.03f)
1150  {
1151  Range = new Vector2(0, 1),
1152  BarScrollValue = targetWallet.RewardDistribution / 100f,
1153  Step = 0.01f,
1154  BarSize = 0.1f,
1155  OnMoved = (bar, scroll) =>
1156  {
1157  int rewardDistribution = RoundRewardDistribution(scroll, bar.Step);
1158  SetRewardText(rewardDistribution, rewardBlock);
1159  return true;
1160  },
1161  OnReleased = (bar, scroll) =>
1162  {
1163  int newRewardDistribution = RoundRewardDistribution(scroll, bar.Step);
1164  if (newRewardDistribution == targetWallet.RewardDistribution) { return false; }
1165  SetRewardDistribution(Option.Some(character), newRewardDistribution);
1166  return true;
1167  }
1168  };
1169 
1170  SetRewardText(targetWallet.RewardDistribution, rewardBlock);
1171 
1172 // @formatter:off
1173  GUIScissorComponent scissorComponent = new GUIScissorComponent(new RectTransform(new Vector2(0.85f, 1.25f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter))
1174  {
1175  CanBeFocused = false
1176  };
1177  transferMenu = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform));
1178 
1179  GUILayoutGroup transferMenuLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.8f), transferMenu.RectTransform, Anchor.BottomLeft), childAnchor: Anchor.Center);
1180  GUILayoutGroup paddedTransferMenuLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(transferMenuLayout.RectTransform, 0.85f), transferMenuLayout.RectTransform));
1181  GUILayoutGroup mainLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), paddedTransferMenuLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
1182  GUILayoutGroup leftLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform));
1183  GUITextBlock leftName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), character.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont);
1184  GUITextBlock leftBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), leftLayout.RectTransform), TextManager.FormatCurrency(targetWallet.Balance), textAlignment: Alignment.Left) { TextColor = GUIStyle.Blue };
1185  GUILayoutGroup rightLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), mainLayout.RectTransform), childAnchor: Anchor.TopRight);
1186  GUITextBlock rightName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight);
1187  GUITextBlock rightBalance = new GUITextBlock(new RectTransform(new Vector2(1f, 0.5f), rightLayout.RectTransform), string.Empty, textAlignment: Alignment.Right) { TextColor = GUIStyle.Red };
1188  GUILayoutGroup centerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), mainLayout.RectTransform, Anchor.Center), childAnchor: Anchor.Center) { IgnoreLayoutGroups = true };
1189  new GUIFrame(new RectTransform(new Vector2(0f, 1f), centerLayout.RectTransform, Anchor.Center), style: "VerticalLine") { IgnoreLayoutGroups = true };
1190  GUIButton centerButton = new GUIButton(new RectTransform(new Vector2(1f), centerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight, anchor: Anchor.Center), style: "GUIButtonTransferArrow");
1191 
1192  GUILayoutGroup inputLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center);
1193  GUINumberInput transferAmountInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1f), inputLayout.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden)
1194  {
1195  MinValueInt = 0
1196  };
1197 
1198  GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), paddedTransferMenuLayout.RectTransform), childAnchor: Anchor.Center);
1199  GUILayoutGroup centerButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), buttonLayout.RectTransform), isHorizontal: true);
1200  GUIButton resetButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("reset"), style: "GUIButtonFreeScale") { Enabled = false };
1201  GUIButton confirmButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), centerButtonLayout.RectTransform), TextManager.Get("confirm"), style: "GUIButtonFreeScale") { Enabled = false };
1202 // @formatter:on
1203  ImmutableArray<GUILayoutGroup> layoutGroups = ImmutableArray.Create(transferMenuLayout, paddedTransferMenuLayout, mainLayout, leftLayout, rightLayout);
1204  MedicalClinicUI.EnsureTextDoesntOverflow(character.Name, leftName, leftLayout.Rect, layoutGroups);
1205  transferMenuButton = new GUIButton(new RectTransform(new Vector2(0.5f, 0.2f), walletFrame.RectTransform, Anchor.BottomCenter, Pivot.TopCenter), style: "UIToggleButtonVertical")
1206  {
1207  ToolTip = TextManager.Get("crewwallet.transfer.tooltip"),
1208  OnClicked = (button, o) =>
1209  {
1210  isTransferMenuOpen = !isTransferMenuOpen;
1211  if (!isTransferMenuOpen)
1212  {
1213  transferAmountInput.IntValue = 0;
1214  }
1215  ToggleTransferMenuIcon(button, open: isTransferMenuOpen);
1216  return true;
1217  }
1218  };
1219 
1220  Identifier eventIdentifier = nameof(CreateWalletFrame).ToIdentifier();
1221 
1222  ToggleTransferMenuIcon(transferMenuButton, open: isTransferMenuOpen);
1223  ToggleCenterButton(centerButton, isSending);
1224 
1225  Wallet otherWallet;
1226  GameMain.Client?.OnPermissionChanged.RegisterOverwriteExisting(eventIdentifier, e => UpdateWalletInterface(registerEvents: false));
1227  UpdateWalletInterface(registerEvents: true);
1228 
1229  void UpdateWalletInterface(bool registerEvents)
1230  {
1231  if (!(Character.Controlled is { } myCharacter))
1232  {
1233  salarySlider.Enabled = false;
1234  transferAmountInput.Enabled = false;
1235  centerButton.Enabled = false;
1236  confirmButton.Enabled = false;
1237  return;
1238  }
1239 
1240  bool hasMoneyPermissions = CampaignMode.AllowedToManageWallets();
1241  salarySlider.Enabled = hasMoneyPermissions;
1242 
1243  switch (hasMoneyPermissions)
1244  {
1245  case true:
1246  rightName.Text = TextManager.Get("crewwallet.bank");
1247  otherWallet = campaign.Bank;
1248  break;
1249  case false when character == myCharacter:
1250  rightName.Text = TextManager.Get("crewwallet.bank");
1251  otherWallet = campaign.Bank;
1252  isSending = true;
1253  ToggleCenterButton(centerButton, isSending);
1254  break;
1255  default:
1256  rightName.Text = myCharacter.Name;
1257  otherWallet = campaign.PersonalWallet;
1258  break;
1259  }
1260 
1261  MedicalClinicUI.EnsureTextDoesntOverflow(rightName.Text.ToString(), rightName, rightLayout.Rect, layoutGroups);
1262 
1263  UpdatedConfirmButtonText();
1264 
1265  if (!hasMoneyPermissions)
1266  {
1267  if (character != Character.Controlled)
1268  {
1269  centerButton.Enabled = centerButton.CanBeFocused = false;
1270  }
1271 
1272  salarySlider.Enabled = salarySlider.CanBeFocused = false;
1273  }
1274 
1275  leftBalance.Text = TextManager.FormatCurrency(otherWallet.Balance);
1276 
1277  UpdateAllInputs();
1278 
1279  if (!registerEvents) { return; }
1280 
1281  centerButton.OnClicked = (btn, o) =>
1282  {
1283  isSending = !isSending;
1284  UpdatedConfirmButtonText();
1285  ToggleCenterButton(btn, isSending);
1286  UpdateAllInputs();
1287  return true;
1288  };
1289 
1290  transferAmountInput.OnValueChanged = input =>
1291  {
1292  UpdateInputs();
1293  };
1294 
1295  transferAmountInput.OnValueEntered = input =>
1296  {
1297  UpdateAllInputs();
1298  };
1299 
1300  resetButton.OnClicked = (button, o) =>
1301  {
1302  transferAmountInput.IntValue = 0;
1303  UpdateAllInputs();
1304  return true;
1305  };
1306 
1307  confirmButton.OnClicked = (button, o) =>
1308  {
1309  int amount = transferAmountInput.IntValue;
1310  if (amount == 0) { return false; }
1311 
1312  Option<Character> target1 = Option<Character>.Some(character),
1313  target2 = otherWallet == campaign.Bank ? Option<Character>.None() : Option<Character>.Some(myCharacter);
1314  if (isSending) { (target1, target2) = (target2, target1); }
1315 
1316  SendTransaction(target1, target2, amount);
1317  isTransferMenuOpen = false;
1318  ToggleTransferMenuIcon(transferMenuButton, isTransferMenuOpen);
1319  return true;
1320  };
1321 
1322  campaign.OnMoneyChanged.RegisterOverwriteExisting(eventIdentifier, e =>
1323  {
1324  if (e.Wallet == targetWallet)
1325  {
1326  moneyBlock.Text = TextManager.FormatCurrency(e.Info.Balance);
1327  salarySlider.BarScrollValue = e.Info.RewardDistribution / 100f;
1328  SetRewardText(e.Info.RewardDistribution, rewardBlock);
1329  }
1330 
1331  UpdateAllInputs();
1332  });
1333 
1334  registeredEvents.Add(eventIdentifier);
1335 
1336  void UpdatedConfirmButtonText()
1337  {
1338  confirmButton.Text = TextManager.Get(hasMoneyPermissions || isSending ? "confirm" : "crewwallet.requestmoney");
1339  }
1340 
1341  void UpdateAllInputs()
1342  {
1343  UpdateInputs();
1344  UpdateMaxInput();
1345  }
1346 
1347  void UpdateInputs()
1348  {
1349  confirmButton.Enabled = resetButton.Enabled = transferAmountInput.IntValue > 0;
1350  if (transferAmountInput.IntValue == 0)
1351  {
1352  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance);
1353  rightBalance.TextColor = GUIStyle.TextColorNormal;
1354  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance);
1355  leftBalance.TextColor = GUIStyle.TextColorNormal;
1356  }
1357  else if (isSending)
1358  {
1359  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance + transferAmountInput.IntValue);
1360  rightBalance.TextColor = GUIStyle.Blue;
1361  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance - transferAmountInput.IntValue);
1362  leftBalance.TextColor = GUIStyle.Red;
1363  }
1364  else
1365  {
1366  rightBalance.Text = TextManager.FormatCurrency(otherWallet.Balance - transferAmountInput.IntValue);
1367  rightBalance.TextColor = GUIStyle.Red;
1368  leftBalance.Text = TextManager.FormatCurrency(targetWallet.Balance + transferAmountInput.IntValue);
1369  leftBalance.TextColor = GUIStyle.Blue;
1370  }
1371  }
1372 
1373  void UpdateMaxInput()
1374  {
1375  int maxValue = isSending ? targetWallet.Balance : otherWallet.Balance;
1376  transferAmountInput.MaxValueInt = maxValue;
1377 
1378  transferAmountInput.Enabled = true;
1379  transferAmountInput.ToolTip = string.Empty;
1380 
1381  if (!hasMoneyPermissions && GameMain.Client?.ServerSettings is { } serverSettings)
1382  {
1383  transferAmountInput.MaxValueInt = Math.Min(maxValue, serverSettings.MaximumMoneyTransferRequest);
1384  if (serverSettings.MaximumMoneyTransferRequest <= 0)
1385  {
1386  transferAmountInput.Enabled = false;
1387  transferAmountInput.ToolTip = TextManager.Get("wallettransferrequestdisabled");
1388  }
1389  }
1390  }
1391  }
1392 
1393  void SetRewardText(int value, GUITextBlock block)
1394  {
1395  var (_, percentage, sum) = Mission.GetRewardShare(value, salaryCrew, Option<int>.None());
1396  LocalizedString tooltip = string.Empty;
1397  block.TextColor = GUIStyle.TextColorNormal;
1398 
1399  if (sum > 100)
1400  {
1401  tooltip = TextManager.GetWithVariables("crewwallet.salary.over100toolitp", ("[sum]", $"{(int)sum}"), ("[newvalue]", $"{percentage}"));
1402  block.TextColor = GUIStyle.Orange;
1403  }
1404 
1405  LocalizedString text = TextManager.GetWithVariable("percentageformat", "[value]", $"{value}");
1406 
1407  block.Text = text;
1408  block.ToolTip = RichString.Rich(tooltip);
1409  }
1410 
1411  static void ToggleTransferMenuIcon(GUIButton btn, bool open)
1412  {
1413  foreach (GUIComponent child in btn.Children)
1414  {
1415  child.SpriteEffects = open ? SpriteEffects.None : SpriteEffects.FlipVertically;
1416  }
1417  }
1418 
1419  static void ToggleCenterButton(GUIButton btn, bool isSending)
1420  {
1421  foreach (GUIComponent child in btn.Children)
1422  {
1423  child.SpriteEffects = isSending ? SpriteEffects.None : SpriteEffects.FlipHorizontally;
1424  }
1425  }
1426 
1427  static void SendTransaction(Option<Character> to, Option<Character> from, int amount)
1428  {
1429  INetSerializableStruct transfer = new NetWalletTransfer
1430  {
1431  Sender = from.Select(option => option.ID),
1432  Receiver = to.Select(option => option.ID),
1433  Amount = amount
1434  };
1435  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.TRANSFER_MONEY);
1436  transfer.Write(msg);
1437  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1438  }
1439  }
1440 
1441  static void SetRewardDistribution(Option<Character> character, int newValue)
1442  {
1443  INetSerializableStruct transfer = new NetWalletSetSalaryUpdate
1444  {
1445  Target = character.Select(c => c.ID),
1446  NewRewardDistribution = newValue
1447  };
1448  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.REWARD_DISTRIBUTION);
1449  transfer.Write(msg);
1450  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1451  }
1452 
1453  static void ResetRewardDistributions()
1454  {
1455  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESET_REWARD_DISTRIBUTION);
1456  GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable);
1457  }
1458 
1459  static int RoundRewardDistribution(float scroll, float step)
1460  => (int)MathUtils.RoundTowardsClosest(scroll * 100, step * 100);
1461 
1462  private GUIComponent CreateClientInfoFrame(GUIFrame frame, Client client, Sprite permissionIcon = null)
1463  {
1464  GUIComponent paddedFrame;
1465 
1466  if (client.Character?.Info == null)
1467  {
1468  paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.874f, 0.58f), frame.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.05f) })
1469  {
1470  RelativeSpacing = 0.05f
1471  //Stretch = true
1472  };
1473 
1474  var headerArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.322f), paddedFrame.RectTransform), isHorizontal: true);
1475 
1476  new GUICustomComponent(new RectTransform(new Vector2(0.425f, 1.0f), headerArea.RectTransform),
1477  onDraw: (sb, component) => DrawNotInGameIcon(sb, component.Rect, client));
1478 
1479  GUIFont font = paddedFrame.Rect.Width < 280 ? GUIStyle.SmallFont : GUIStyle.Font;
1480 
1481  var headerTextArea = new GUILayoutGroup(new RectTransform(new Vector2(0.575f, 1.0f), headerArea.RectTransform))
1482  {
1483  RelativeSpacing = 0.02f,
1484  Stretch = true
1485  };
1486 
1487  GUITextBlock clientNameBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), ToolBox.LimitString(client.Name, GUIStyle.Font, headerTextArea.Rect.Width), textColor: Color.White, font: GUIStyle.Font)
1488  {
1490  Padding = Vector4.Zero
1491  };
1492 
1493  if (permissionIcon != null)
1494  {
1495  Point iconSize = permissionIcon.SourceRect.Size;
1496  int iconWidth = (int)((float)clientNameBlock.Rect.Height / iconSize.Y * iconSize.X);
1497  new GUIImage(new RectTransform(new Point(iconWidth, clientNameBlock.Rect.Height), clientNameBlock.RectTransform) { AbsoluteOffset = new Point(-iconWidth - 2, 0) }, permissionIcon) { IgnoreLayoutGroups = true };
1498  }
1499 
1500  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), headerTextArea.RectTransform), client.Spectating ? TextManager.Get("playingasspectator") : TextManager.Get("tabmenu.inlobby"), textColor: Color.White, font: font, wrap: true)
1501  {
1502  Padding = Vector4.Zero
1503  };
1504  }
1505  else
1506  {
1507  paddedFrame = client.Character.Info.CreateInfoFrame(frame, false, permissionIcon);
1508  }
1509 
1510  return paddedFrame;
1511  }
1512 
1513  private void CreateMultiPlayerLogContent(GUIFrame crewFrame)
1514  {
1515  var logContainer = new GUIFrame(new RectTransform(new Vector2(0.543f, 0.717f), infoFrameHolder.RectTransform, Anchor.TopLeft, Pivot.TopRight) { RelativeOffset = new Vector2(-0.145f, 0) });
1516  var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.900f, 0.900f), logContainer.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, 0.0475f) }, style: null);
1517  var content = new GUILayoutGroup(new RectTransform(Vector2.One, innerFrame.RectTransform))
1518  {
1519  Stretch = true
1520  };
1521 
1522  logList = new GUIListBox(new RectTransform(Vector2.One, content.RectTransform))
1523  {
1524  Padding = new Vector4(0, 10 * GUI.Scale, 0, 10 * GUI.Scale),
1525  UserData = crewFrame,
1526  AutoHideScrollBar = false,
1527  Spacing = (int)(5 * GUI.Scale)
1528  };
1529 
1530  foreach ((string message, PlayerConnectionChangeType type) in storedMessages)
1531  {
1532  AddLineToLog(message, type);
1533  }
1534 
1535  logList.BarScroll = 1f;
1536  }
1537 
1538  private static readonly List<(string message, PlayerConnectionChangeType type)> storedMessages = new List<(string message, PlayerConnectionChangeType type)>();
1539 
1541  {
1542  if (!GameMain.GameSession?.IsRunning ?? true) { return; }
1543 
1544  string msg = ChatMessage.GetTimeStamp() + message.TextWithSender;
1545  storedMessages.Add((msg, message.ChangeType));
1546 
1547  if (GameSession.IsTabMenuOpen && SelectedTab == InfoFrameTab.Crew)
1548  {
1549  TabMenu instance = GameSession.TabMenuInstance;
1550  instance.AddLineToLog(msg, message.ChangeType);
1551  instance.RemoveCurrentElements();
1552  instance.CreateMultiPlayerList(true);
1553  }
1554  }
1555 
1556  private void RemoveCurrentElements()
1557  {
1558  for (int i = 0; i < crewListArray.Length; i++)
1559  {
1560  for (int j = 0; j < linkedGUIList.Count; j++)
1561  {
1562  linkedGUIList[j].Remove(crewListArray[i].Content);
1563  }
1564  }
1565 
1566  linkedGUIList.Clear();
1567  }
1568 
1569  private void AddLineToLog(string line, PlayerConnectionChangeType type)
1570  {
1571  Color textColor = Color.White;
1572 
1573  switch (type)
1574  {
1575  case PlayerConnectionChangeType.Joined:
1576  textColor = GUIStyle.Green;
1577  break;
1578  case PlayerConnectionChangeType.Kicked:
1579  textColor = GUIStyle.Orange;
1580  break;
1581  case PlayerConnectionChangeType.Disconnected:
1582  textColor = GUIStyle.Yellow;
1583  break;
1584  case PlayerConnectionChangeType.Banned:
1585  textColor = GUIStyle.Red;
1586  break;
1587  }
1588 
1589  if (logList != null)
1590  {
1591  var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), logList.Content.RectTransform), RichString.Rich(line), wrap: true, font: GUIStyle.SmallFont)
1592  {
1593  TextColor = textColor,
1594  CanBeFocused = false,
1595  UserData = line
1596  };
1597  textBlock.CalculateHeightFromText();
1598  if (textBlock.HasColorHighlight)
1599  {
1600  foreach (var data in textBlock.RichTextData)
1601  {
1602  textBlock.ClickableAreas.Add(new GUITextBlock.ClickableArea()
1603  {
1604  Data = data,
1605  OnClick = GameMain.NetLobbyScreen.SelectPlayer,
1606  OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu
1607  });
1608  }
1609  }
1610  }
1611  }
1612 
1613  private void CreateMissionInfo(GUIFrame infoFrame)
1614  {
1615  if (Level.Loaded?.LevelData == null)
1616  {
1617  DebugConsole.ThrowError("Failed to display mission info in the tab menu (no level loaded).\n" + Environment.StackTrace);
1618  return;
1619  }
1620 
1621  infoFrame.ClearChildren();
1622  GUIFrame missionFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
1623  int padding = (int)(0.0245f * missionFrame.Rect.Height);
1624  GUIFrame missionFrameContent = new GUIFrame(new RectTransform(new Point(missionFrame.Rect.Width - padding * 2, missionFrame.Rect.Height - padding * 2), infoFrame.RectTransform, Anchor.Center), style: null);
1625  Location location = GameMain.GameSession.StartLocation;
1626  if (Level.Loaded.Type == LevelData.LevelType.LocationConnection)
1627  {
1628  location ??= GameMain.GameSession.EndLocation;
1629  }
1630 
1631  GUILayoutGroup locationInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.3f), missionFrameContent.RectTransform))
1632  {
1633  AbsoluteSpacing = GUI.IntScale(10)
1634  };
1635 
1636  Sprite portrait = location.Type.GetPortrait(location.PortraitId);
1637  bool hasPortrait = portrait != null && portrait.SourceRect.Width > 0 && portrait.SourceRect.Height > 0;
1638  int contentWidth = missionFrameContent.Rect.Width;
1639  if (hasPortrait)
1640  {
1641  float portraitAspectRatio = portrait.SourceRect.Width / portrait.SourceRect.Height;
1642  GUIImage portraitImage = new GUIImage(new RectTransform(new Vector2(0.5f, 1f), locationInfoContainer.RectTransform, Anchor.CenterRight), portrait, scaleToFit: true)
1643  {
1644  IgnoreLayoutGroups = true
1645  };
1646  locationInfoContainer.Recalculate();
1647  portraitImage.RectTransform.NonScaledSize = new Point(Math.Min((int)(portraitImage.Rect.Size.Y * portraitAspectRatio), portraitImage.Rect.Width), portraitImage.Rect.Size.Y);
1648  }
1649 
1650  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.DisplayName, font: GUIStyle.LargeFont);
1651  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), locationInfoContainer.RectTransform), location.Type.Name, font: GUIStyle.SubHeadingFont);
1652 
1653  if (location.Faction?.Prefab != null)
1654  {
1655  var factionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1656  TextManager.Get("Faction"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1657  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), factionLabel.RectTransform), location.Faction.Prefab.Name, textAlignment: Alignment.CenterRight);
1658  }
1659  var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1660  TextManager.Get("Biome", "location"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1661  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), biomeLabel.RectTransform), Level.Loaded.LevelData.Biome.DisplayName, textAlignment: Alignment.CenterRight);
1662  var difficultyLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.0f), locationInfoContainer.RectTransform),
1663  TextManager.Get("LevelDifficulty"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft);
1664  new GUITextBlock(new RectTransform(new Vector2(1.0f, 1.0f), difficultyLabel.RectTransform), TextManager.GetWithVariable("percentageformat", "[value]", ((int)Level.Loaded.LevelData.Difficulty).ToString()), textAlignment: Alignment.CenterRight);
1665 
1666  new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), missionFrameContent.RectTransform) { AbsoluteOffset = new Point(0, locationInfoContainer.Rect.Height + padding) }, style: "HorizontalLine")
1667  {
1668  CanBeFocused = false
1669  };
1670 
1671  int locationInfoYOffset = locationInfoContainer.Rect.Height + padding * 2;
1672 
1673  GUIListBox missionList = new GUIListBox(new RectTransform(new Point(contentWidth, missionFrameContent.Rect.Height - locationInfoYOffset), missionFrameContent.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, locationInfoYOffset) });
1674  missionList.ContentBackground.Color = Color.Transparent;
1675  missionList.Spacing = GUI.IntScale(15);
1676 
1677  if (GameMain.GameSession?.Missions != null)
1678  {
1679  foreach (Mission mission in GameMain.GameSession.Missions)
1680  {
1681  if (!mission.Prefab.ShowInMenus) { continue; }
1682 
1683  var textContent = new List<LocalizedString>()
1684  {
1685  mission.GetMissionRewardText(Submarine.MainSub),
1686  mission.GetReputationRewardText(),
1687  mission.Description
1688  };
1689  textContent.AddRange(mission.ShownMessages);
1690 
1691  RoundSummary.CreateMissionEntry(
1692  missionList.Content,
1693  mission.Name,
1694  textContent,
1695  mission.Difficulty ?? 0,
1696  mission.Prefab.Icon, mission.Prefab.IconColor,
1697  out GUIImage missionIcon);
1698  if (missionIcon != null)
1699  {
1700  UpdateMissionStateIcon();
1701  mission.OnMissionStateChanged += (mission) => UpdateMissionStateIcon();
1702 
1703  void UpdateMissionStateIcon()
1704  {
1705  if (mission.DisplayAsCompleted || mission.DisplayAsFailed)
1706  {
1707  RoundSummary.UpdateMissionStateIcon(mission.DisplayAsCompleted, missionIcon);
1708  }
1709  }
1710  }
1711  }
1712  }
1713  else
1714  {
1715  GUILayoutGroup missionTextGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0f), missionList.RectTransform, Anchor.CenterLeft), false, childAnchor: Anchor.TopLeft);
1716  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextGroup.RectTransform), TextManager.Get("NoMission"), font: GUIStyle.LargeFont);
1717  }
1718 
1719  GameMain.GameSession?.EventManager?.EventLog?.CreateEventLogUI(missionList.Content);
1720  GameMain.GameSession.EnableEventLogNotificationIcon(enabled: false);
1721 
1722  RoundSummary.AddSeparators(missionList.Content);
1723  }
1724 
1725  private static void CreateSubmarineInfo(GUIFrame infoFrame, Submarine sub)
1726  {
1727  if (sub == null) { return; }
1728 
1729  GUIFrame subInfoFrame = new GUIFrame(new RectTransform(Vector2.One, infoFrame.RectTransform, Anchor.TopCenter), style: "GUIFrameListBox");
1730  GUIFrame paddedFrame = new GUIFrame(new RectTransform(Vector2.One * 0.97f, subInfoFrame.RectTransform, Anchor.Center), style: null);
1731 
1732  var previewButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.43f), paddedFrame.RectTransform), style: null)
1733  {
1734  OnClicked = (btn, obj) => { SubmarinePreview.Create(sub.Info); return false; },
1735  };
1736 
1737  var previewImage = sub.Info.PreviewImage ?? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name.Equals(sub.Info.Name, StringComparison.OrdinalIgnoreCase))?.PreviewImage;
1738  if (previewImage == null)
1739  {
1740  new GUITextBlock(new RectTransform(Vector2.One, previewButton.RectTransform), TextManager.Get("SubPreviewImageNotFound"));
1741  }
1742  else
1743  {
1744  var submarinePreviewBackground = new GUIFrame(new RectTransform(Vector2.One, previewButton.RectTransform), style: null)
1745  {
1746  Color = Color.Black,
1747  HoverColor = Color.Black,
1748  SelectedColor = Color.Black,
1749  PressedColor = Color.Black,
1750  CanBeFocused = false,
1751  };
1752  new GUIImage(new RectTransform(new Vector2(0.98f), submarinePreviewBackground.RectTransform, Anchor.Center), previewImage, scaleToFit: true) { CanBeFocused = false };
1753  new GUIFrame(new RectTransform(Vector2.One, submarinePreviewBackground.RectTransform), "InnerGlow", color: Color.Black) { CanBeFocused = false };
1754  }
1755 
1756  new GUIFrame(new RectTransform(Vector2.One * 0.12f, previewButton.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight)
1757  {
1758  AbsoluteOffset = new Point((int)(0.03f * previewButton.Rect.Height))
1759  },
1760  "ExpandButton", Color.White)
1761  {
1762  Color = Color.White,
1763  HoverColor = Color.White,
1764  PressedColor = Color.White
1765  };
1766 
1767  var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform));
1768 
1769  LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ?
1770  TextManager.GetWithVariables("submarine.classandtier",
1771  ("[class]", TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")),
1772  ("[tier]", TextManager.Get($"submarinetier.{sub.Info.Tier}"))) :
1773  TextManager.Get("shuttle");
1774 
1775  int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y;
1776  int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
1777 
1778  var submarineNameText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, nameHeight + HUDLayoutSettings.Padding / 2), subInfoTextLayout.RectTransform), sub.Info.DisplayName, textAlignment: Alignment.CenterLeft, font: GUIStyle.LargeFont) { CanBeFocused = false };
1779  submarineNameText.RectTransform.MinSize = new Point(0, (int)submarineNameText.TextSize.Y);
1780  var submarineClassText = new GUITextBlock(new RectTransform(new Point(subInfoTextLayout.Rect.Width, classHeight), subInfoTextLayout.RectTransform), className, textAlignment: Alignment.CenterLeft, font: GUIStyle.SubHeadingFont) { CanBeFocused = false };
1781  submarineClassText.RectTransform.MinSize = new Point(0, (int)submarineClassText.TextSize.Y);
1782 
1783  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1784  {
1785  GUILayoutGroup headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.09f), paddedFrame.RectTransform) { RelativeOffset = new Vector2(0f, 0.43f) }, isHorizontal: true) { Stretch = true };
1786  GUIImage headerIcon = new GUIImage(new RectTransform(Vector2.One, headerLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SubmarineIcon");
1787  new GUITextBlock(new RectTransform(Vector2.One, headerLayout.RectTransform), TextManager.Get("uicategory.upgrades"), font: GUIStyle.LargeFont);
1788 
1789  var upgradeRootLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.48f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft), isHorizontal: true);
1790 
1791  var upgradeCategoryPanel = UpgradeStore.CreateUpgradeCategoryList(new RectTransform(new Vector2(0.4f, 1f), upgradeRootLayout.RectTransform));
1792  upgradeCategoryPanel.HideChildrenOutsideFrame = true;
1793  UpgradeStore.UpdateCategoryList(upgradeCategoryPanel, campaign, sub, UpgradeStore.GetApplicableCategories(sub).ToArray());
1794  GUIComponent[] toRemove = upgradeCategoryPanel.Content.FindChildren(c => !c.Enabled).ToArray();
1795  toRemove.ForEach(c => upgradeCategoryPanel.RemoveChild(c));
1796 
1797  var upgradePanel = new GUIListBox(new RectTransform(new Vector2(0.6f, 1f), upgradeRootLayout.RectTransform));
1798  upgradeCategoryPanel.OnSelected = (component, userData) =>
1799  {
1800  upgradePanel.ClearChildren();
1801  if (userData is UpgradeStore.CategoryData categoryData && Submarine.MainSub != null)
1802  {
1803  foreach (UpgradePrefab prefab in categoryData.Prefabs)
1804  {
1805  var frame = UpgradeStore.CreateUpgradeFrame(prefab, categoryData.Category, campaign, new RectTransform(new Vector2(1f, 0.3f), upgradePanel.Content.RectTransform), addBuyButton: false).Frame;
1806  UpgradeStore.UpdateUpgradeEntry(frame, prefab, categoryData.Category, campaign);
1807  }
1808  }
1809  return true;
1810  };
1811  }
1812  else
1813  {
1814  var specsListBox = new GUIListBox(new RectTransform(new Vector2(1f, 0.57f), paddedFrame.RectTransform, Anchor.BottomLeft, Pivot.BottomLeft))
1815  {
1816  CurrentSelectMode = GUIListBox.SelectMode.None
1817  };
1818  sub.Info.CreateSpecsWindow(specsListBox, GUIStyle.Font,
1819  includeTitle: false,
1820  includeClass: false,
1821  includeDescription: true);
1822  }
1823  }
1824 
1825  private GUIImage talentPointNotification, eventLogNotification;
1826 
1827  public static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
1828  {
1829  parent.Content.ClearChildren();
1830  List<GUITextBlock> skillNames = new List<GUITextBlock>();
1831  foreach (Skill skill in info.Job.GetSkills().OrderByDescending(static s => s.Level))
1832  {
1833  GUILayoutGroup skillContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.0f), parent.Content.RectTransform), isHorizontal: true) { CanBeFocused = true };
1834  var skillName = new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), skillContainer.RectTransform), TextManager.Get($"skillname.{skill.Identifier}").Fallback(skill.Identifier.Value));
1835  skillNames.Add(skillName);
1836  skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
1837  skillContainer.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
1838 
1839  new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight);
1840 
1841  float modifiedSkillLevel = MathF.Floor(character?.GetSkillLevel(skill.Identifier) ?? skill.Level);
1842  if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level)))
1843  {
1844  int skillChange = (int)MathF.Floor(modifiedSkillLevel - MathF.Floor(skill.Level));
1845  string stringColor = skillChange switch
1846  {
1847  > 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
1848  < 0 => XMLExtensions.ToStringHex(GUIStyle.Red),
1849  _ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal)
1850  };
1851 
1852  RichString changeText = RichString.Rich($"(‖color:{stringColor}‖{(skillChange > 0 ? "+" : string.Empty) + skillChange}‖color:end‖)");
1853  new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), changeText) { Padding = Vector4.Zero };
1854  }
1855  skillContainer.Recalculate();
1856  }
1857 
1858  parent.RecalculateChildren();
1859  GUITextBlock.AutoScaleAndNormalize(skillNames);
1860  }
1861 
1862  public void OnExperienceChanged(Character character)
1863  {
1864  if (character != Character.Controlled) { return; }
1865  talentMenu.UpdateTalentInfo();
1866  }
1867 
1868  public void OnClose()
1869  {
1870  if (!(GameMain.GameSession?.Campaign is { } campaign)) { return; }
1871  foreach (Identifier identifier in registeredEvents)
1872  {
1873  campaign.OnMoneyChanged.TryDeregister(identifier);
1874  }
1875  }
1876  }
1877 }
Stores information about the Character that is needed between rounds in the menu etc....
GUIComponent CreateInfoFrame(GUIFrame frame, bool returnParent, Sprite permissionIcon=null)
void DrawJobIcon(SpriteBatch spriteBatch, Rectangle area, bool evaluateDisguise=false)
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
void Pulsate(Vector2 startScale, Vector2 endScale, float duration)
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual Rectangle Rect
RectTransform RectTransform
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:42
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...
static GameSession?? GameSession
Definition: GameMain.cs:88
static bool IsMultiplayer
Definition: GameMain.cs:35
static GameClient Client
Definition: GameMain.cs:188
JobPrefab Prefab
Definition: Job.cs:18
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
Vector2 RelativeSize
Relative to the parent rect.
Point?? MinSize
Min size in pixels. Does not affect scaling.
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
void CreateReputationInfoPanel(GUIComponent parent, CampaignMode campaignMode)
readonly Identifier Identifier
Definition: Skill.cs:7
float Level
Definition: Skill.cs:19
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static void CreateSkillList(Character character, CharacterInfo info, GUIListBox parent)
Definition: TabMenu.cs:1827
void Update(float deltaTime)
Definition: TabMenu.cs:177
static Color OwnCharacterBGColor
Definition: TabMenu.cs:39
void SelectInfoFrameTab(InfoFrameTab selectedTab)
Definition: TabMenu.cs:437
static GUIFrame PendingChangesFrame
Definition: TabMenu.cs:37
void AddToGUIUpdateList()
Definition: TabMenu.cs:249
static InfoFrameTab SelectedTab
Definition: TabMenu.cs:23
static bool PendingChanges
Definition: TabMenu.cs:15
void Initialize()
Definition: TabMenu.cs:156
void OnExperienceChanged(Character character)
Definition: TabMenu.cs:1862
static void OnRoundEnded()
Definition: TabMenu.cs:255
static void StorePlayerConnectionChangeMessage(ChatMessage message)
Definition: TabMenu.cs:1540
void Draw(SpriteBatch spriteBatch, RectangleF rect, Color color, SpriteEffects spriteEffects=SpriteEffects.None, Vector2? uvOffset=null)
NumberType
Definition: Enums.cs:741
CharacterType
Definition: Enums.cs:711
@ Character
Characters only