Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/GameSession/CrewManager.cs
4 using FarseerPhysics;
5 using Microsoft.Xna.Framework;
6 using Microsoft.Xna.Framework.Graphics;
7 using Microsoft.Xna.Framework.Input;
8 using System;
9 using System.Collections.Generic;
10 using System.Linq;
11 using System.Xml.Linq;
12 
13 namespace Barotrauma
14 {
15  partial class CrewManager
16  {
17  private Point screenResolution;
18 
20  public bool DragOrder;
21  private bool dropOrder;
22  private int framesToSkip = 2;
23  private float dragOrderTreshold;
24  private Vector2 dragPoint = Vector2.Zero;
25 
26  #region UI
27 
28  public GUIComponent ReportButtonFrame { get; set; }
29 
30  private GUIFrame guiFrame;
31  private GUIFrame crewArea;
32  private GUIListBox crewList;
33  private float crewListOpenState;
34  private bool _isCrewMenuOpen = true;
35  private Point crewListEntrySize;
36 
37  private readonly List<GUITickBox> traitorButtons = new List<GUITickBox>();
38 
42  public ChatBox ChatBox { get; private set; }
43 
44  private float prevUIScale;
45 
46  public bool AllowCharacterSwitch = true;
47 
52  public bool IsCrewMenuOpen
53  {
54  get { return _isCrewMenuOpen; }
55  set
56  {
57  if (_isCrewMenuOpen == value) { return; }
58  _isCrewMenuOpen = value;
59  PreferCrewMenuOpen = value;
60  }
61  }
62 
63  public static bool PreferCrewMenuOpen = true;
64 
65  public bool AutoShowCrewList() => _isCrewMenuOpen = true;
66 
67  public void AutoHideCrewList() => _isCrewMenuOpen = false;
68 
69  public void ResetCrewList() => _isCrewMenuOpen = PreferCrewMenuOpen;
70 
71  const float CommandNodeAnimDuration = 0.2f;
72 
73  public List<GUIButton> OrderOptionButtons = new List<GUIButton>();
74 
75  private Sprite jobIndicatorBackground, previousOrderArrow, cancelIcon;
76 
77  #endregion
78 
79  #region Constructors
80 
81  public CrewManager(XElement element, bool isSinglePlayer)
82  : this(isSinglePlayer)
83  {
84  AddCharacterElements(element);
85  }
86 
87  partial void InitProjectSpecific()
88  {
89  guiFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), null, Color.Transparent)
90  {
91  CanBeFocused = false
92  };
93 
94  #region Crew Area
95 
96  crewArea = new GUIFrame(HUDLayoutSettings.ToRectTransform(HUDLayoutSettings.CrewArea, guiFrame.RectTransform), style: null, color: Color.Transparent)
97  {
98  CanBeFocused = false
99  };
100  crewArea.RectTransform.NonScaledSize = HUDLayoutSettings.CrewArea.Size;
101 
102  // AbsoluteOffset is set in UpdateProjectSpecific based on crewListOpenState
103  crewList = new GUIListBox(new RectTransform(Vector2.One, crewArea.RectTransform), style: null, isScrollBarOnDefaultSide: false)
104  {
105  AutoHideScrollBar = false,
106  CanBeFocused = false,
107  CurrentDragMode = GUIListBox.DragMode.DragWithinBox,
108  CanInteractWhenUnfocusable = true,
109  OnSelected = (component, userData) => false,
110  SelectMultiple = false,
111  Spacing = (int)(GUI.Scale * 10),
112  OnRearranged = OnCrewListRearranged
113  };
114 
115  jobIndicatorBackground = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(0, 512, 128, 128));
116  previousOrderArrow = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(128, 512, 128, 128));
117  cancelIcon = new Sprite("Content/UI/CommandUIAtlas.png", new Rectangle(512, 384, 128, 128));
118 
119  // Calculate and store crew list entry size so it doesn't have to be calculated for every entry
120  crewListEntrySize = new Point(crewList.Content.Rect.Width - HUDLayoutSettings.Padding, 0);
121  int crewListEntryMinHeight = 32;
122  crewListEntrySize.Y = Math.Max(crewListEntryMinHeight, (int)(crewListEntrySize.X / 8f));
123  float charactersPerView = crewList.Content.Rect.Height / (float)(crewListEntrySize.Y + crewList.Spacing);
124  int adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Round(charactersPerView)) - crewList.Spacing;
125  if (adjustedHeight < crewListEntryMinHeight) { adjustedHeight = (int)Math.Ceiling(crewList.Content.Rect.Height / Math.Floor(charactersPerView)) - crewList.Spacing; }
126  crewListEntrySize.Y = adjustedHeight;
127 
128  #endregion
129 
130  #region Chatbox
131 
132  if (IsSinglePlayer)
133  {
134  ChatBox = new ChatBox(guiFrame, isSinglePlayer: true)
135  {
136  OnEnterMessage = (textbox, text) =>
137  {
138  if (Character.Controlled?.Info == null)
139  {
140  textbox.Deselect();
141  textbox.Text = "";
142  return true;
143  }
144 
145  textbox.TextColor = ChatMessage.MessageColor[(int)ChatMessageType.Default];
146 
147  if (!string.IsNullOrWhiteSpace(text))
148  {
149  string msgCommand = ChatMessage.GetChatMessageCommand(text, out string msg);
150  // add to local history
151  ChatBox.ChatManager.Store(text);
152  bool isUsingRadioMode = GameMain.ActiveChatMode == ChatMode.Radio;
153  bool containsRadioCommand = msgCommand == "r" || msgCommand == "radio";
154  bool canUseRadio = ChatMessage.CanUseRadio(Character.Controlled, out WifiComponent headset);
155  ChatMessageType messageType = ((isUsingRadioMode && msgCommand == "") || containsRadioCommand) && canUseRadio ? ChatMessageType.Radio : ChatMessageType.Default;
157  Character.Controlled.Info.Name,
158  msg, messageType,
159  Character.Controlled);
160  Character.Controlled.ShowSpeechBubble(ChatMessage.MessageColor[(int)messageType], text);
161  if (messageType == ChatMessageType.Radio && headset != null)
162  {
163  Signal s = new Signal(msg, sender: Character.Controlled, source: headset.Item);
164  headset.TransmitSignal(s, sentFromChat: true);
165  }
166  }
167  textbox.Deselect();
168  textbox.Text = "";
170  {
171  ChatBox.ToggleOpen = false;
173  }
174  return true;
175  }
176  };
177 
179  }
180 
181  #endregion
182 
183  #region Reports
184  var chatBox = ChatBox ?? GameMain.Client?.ChatBox;
185  if (chatBox != null)
186  {
187  chatBox.ToggleButton = new GUIButton(new RectTransform(new Point((int)(182f * GUI.Scale * 0.4f), (int)(99f * GUI.Scale * 0.4f)), chatBox.GUIFrame.Parent.RectTransform), style: "ChatToggleButton")
188  {
189  ToolTip = TextManager.GetWithVariable("hudbutton.chatbox", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.ChatBox)),
190  ClampMouseRectToParent = false
191  };
192  chatBox.ToggleButton.RectTransform.AbsoluteOffset = new Point(0, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height);
193  chatBox.ToggleButton.OnClicked += (GUIButton btn, object userdata) =>
194  {
195  chatBox.Toggle();
196  return true;
197  };
198  }
199 
200  var reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray();
201  if (reports.None())
202  {
203  DebugConsole.ThrowError("No valid orders for report buttons found! Cannot create report buttons. The orders for the report buttons must have 'targetallcharacters' attribute enabled and a valid 'symbolsprite' defined.");
204  return;
205  }
206 
207  ReportButtonFrame = new GUILayoutGroup(new RectTransform(
208  new Point((HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height - (int)((reports.Length - 1) * 5 * GUI.Scale)) / reports.Length, HUDLayoutSettings.ChatBoxArea.Height - chatBox.ToggleButton.Rect.Height), guiFrame.RectTransform))
209  {
210  AbsoluteSpacing = (int)(5 * GUI.Scale),
211  UserData = "reportbuttons",
212  CanBeFocused = false,
213  Visible = false
214  };
215 
216  ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(0, -chatBox.ToggleButton.Rect.Height);
217 
218  CreateReportButtons(this, ReportButtonFrame, reports, false);
219 
220  #endregion
221 
222  screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
223  prevUIScale = GUI.Scale;
224  _isCrewMenuOpen = PreferCrewMenuOpen = GameSettings.CurrentConfig.CrewMenuOpen;
225  }
226 
227  public static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList<OrderPrefab> reports, bool isHorizontal)
228  {
229  //report buttons
230  foreach (OrderPrefab orderPrefab in reports)
231  {
232  if (!orderPrefab.IsVisibleAsReportButton) { continue; }
233 
234  var btn = new GUIButton(new RectTransform(Vector2.One, parent.RectTransform, scaleBasis: isHorizontal ? ScaleBasis.BothHeight : ScaleBasis.BothWidth), style: null)
235  {
236  OnClicked = (button, userData) =>
237  {
238  if (!CanIssueOrders || crewManager?.DraggedOrderPrefab != null) { return false; }
239  var sub = Character.Controlled.Submarine;
240  if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; }
241 
242  if (crewManager != null)
243  {
244  Order order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity, orderGiver: Character.Controlled);
245  crewManager.SetCharacterOrder(null, order);
246  if (crewManager.IsSinglePlayer) { HumanAIController.ReportProblem(Character.Controlled, order); }
247  }
248  return true;
249  },
250  UserData = orderPrefab,
251  ClampMouseRectToParent = false
252  };
253  btn.ToolTip = RichString.Rich($"‖color:{XMLExtensions.ColorToString(orderPrefab.Color)}‖{orderPrefab.Name}‖color:end‖\n{TextManager.Get("draganddropreports")}");
254 
255  if (crewManager != null)
256  {
257  btn.OnButtonDown = () =>
258  {
259  crewManager.dragOrderTreshold = Math.Max(btn.Rect.Width, btn.Rect.Height) / 2f;
260  crewManager.DraggedOrderPrefab = orderPrefab;
261  crewManager.dropOrder = false;
262  crewManager.framesToSkip = 2;
263  crewManager.dragPoint = btn.Rect.Center.ToVector2();
264  return true;
265  };
266  }
267 
268  new GUIFrame(new RectTransform(new Vector2(1.5f), btn.RectTransform, Anchor.Center), "OuterGlowCircular")
269  {
270  Color = GUIStyle.Red * 0.8f,
271  HoverColor = GUIStyle.Red * 1.0f,
272  PressedColor = GUIStyle.Red * 0.6f,
273  UserData = "highlighted",
274  CanBeFocused = false,
275  Visible = false
276  };
277 
278  var img = new GUIImage(new RectTransform(Vector2.One, btn.RectTransform), orderPrefab.SymbolSprite, scaleToFit: true)
279  {
280  Color = orderPrefab.Color,
281  HoverColor = Color.Lerp(orderPrefab.Color, Color.White, 0.5f),
282  ToolTip = btn.ToolTip,
283  SpriteEffects = SpriteEffects.FlipHorizontally,
284  UserData = orderPrefab
285  };
286  }
287  }
288 
289  #endregion
290 
291  #region Character list management
292 
294  {
295  return crewArea.Rect;
296  }
297 
302  {
303  if (character == null) { return null; }
304  if (crewList.Content.Children.Any(c => c.UserData as Character == character)) { return null; }
305 
306  var background = new GUIFrame(
307  new RectTransform(crewListEntrySize, parent: crewList.Content.RectTransform, anchor: Anchor.TopRight),
308  style: "CrewListBackground")
309  {
310  UserData = character,
311  OnSecondaryClicked = (comp, data) =>
312  {
313  if (data == null) { return false; }
314  if (GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character == data) is Client client)
315  {
317  return true;
318  }
319  return false;
320  }
321  };
322  SetCharacterComponentTooltip(background);
323 
324  var iconRelativeWidth = (float)crewListEntrySize.Y / background.Rect.Width;
325 
326  var layoutGroup = new GUILayoutGroup(
327  new RectTransform(Vector2.One, parent: background.RectTransform),
328  isHorizontal: true,
329  childAnchor: Anchor.CenterLeft)
330  {
331  CanBeFocused = false,
332  RelativeSpacing = 0.1f * iconRelativeWidth,
333  UserData = character
334  };
335 
336  var commandButtonAbsoluteHeight = Math.Min(40.0f, 0.67f * background.Rect.Height);
337  var paddingRelativeWidth = 0.35f * commandButtonAbsoluteHeight / background.Rect.Width;
338 
339  // "Padding" to prevent member-specific command button from overlapping job indicator
340  new GUIFrame(new RectTransform(new Vector2(paddingRelativeWidth, 1.0f), layoutGroup.RectTransform), style: null)
341  {
342  CanBeFocused = false
343  };
344 
345  // Hide the icon to make more space for the name if the crew list's width is small enough
346  bool isJobIconVisible = crewListEntrySize.X >= 220;
347 
348  if (isJobIconVisible)
349  {
350  var jobIconBackground = new GUIImage(
351  new RectTransform(new Vector2(0.8f * iconRelativeWidth, 0.8f), layoutGroup.RectTransform),
352  jobIndicatorBackground,
353  scaleToFit: true)
354  {
355  CanBeFocused = false,
356  UserData = "job"
357  };
358  if (character?.Info?.Job.Prefab?.Icon != null)
359  {
360  new GUIImage(
361  new RectTransform(Vector2.One, jobIconBackground.RectTransform),
362  character.Info.Job.Prefab.Icon,
363  scaleToFit: true)
364  {
365  CanBeFocused = false,
366  Color = character.Info.Job.Prefab.UIColor,
367  HoverColor = character.Info.Job.Prefab.UIColor,
368  PressedColor = character.Info.Job.Prefab.UIColor,
369  SelectedColor = character.Info.Job.Prefab.UIColor
370  };
371  }
372  }
373 
374  int iconsVisible = isJobIconVisible ? 6 : 5;
375  var nameRelativeWidth = 1.0f
376  // Start padding
377  - paddingRelativeWidth
378  // icons (job, active orders, current task / voip)
379  - (iconsVisible * 0.8f * iconRelativeWidth)
380  // Vertical line
381  - (0.1f * iconRelativeWidth)
382  // Spacing
383  - (7 * layoutGroup.RelativeSpacing);
384  nameRelativeWidth = Math.Max(nameRelativeWidth, 0.25f);
385 
386  var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font;
387  var nameBlock = new GUITextBlock(
388  new RectTransform(
389  new Vector2(nameRelativeWidth, 1.0f),
390  layoutGroup.RectTransform)
391  {
392  MaxSize = new Point(150, background.Rect.Height)
393  }, "",
394  font: font,
395  textColor: character.Info?.Job?.Prefab?.UIColor)
396  {
397  CanBeFocused = false,
398  UserData = "name"
399  };
400  nameBlock.Text = ToolBox.LimitString(character.Name, font, (int)nameBlock.Rect.Width);
401 
402  new GUIImage(
403  new RectTransform(new Vector2(0.1f * iconRelativeWidth, 0.5f), layoutGroup.RectTransform),
404  style: "VerticalLine")
405  {
406  CanBeFocused = false
407  };
408 
409  var orderGroup = new GUILayoutGroup(new RectTransform(new Vector2(3 * 0.8f * iconRelativeWidth, 0.8f), parent: layoutGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
410  {
411  CanBeFocused = false,
412  Stretch = true
413  };
414 
415  // Current orders
416  var currentOrderList = new GUIListBox(new RectTransform(new Vector2(0.0f, 1.0f), parent: orderGroup.RectTransform), isHorizontal: true, style: null)
417  {
418  AllowMouseWheelScroll = false,
419  CurrentDragMode = GUIListBox.DragMode.DragWithinBox,
420  KeepSpaceForScrollBar = false,
421  OnRearranged = OnOrdersRearranged,
422  ScrollBarVisible = false,
423  Spacing = 2,
424  UserData = character
425  };
426  currentOrderList.RectTransform.IsFixedSize = true;
427  currentOrderList.OnAddedToGUIUpdateList += (component) =>
428  {
429  if (component is GUIListBox list)
430  {
431  list.CanBeFocused = CanIssueOrders;
432  list.CurrentDragMode = CanIssueOrders && list.Content.CountChildren > 1
433  ? GUIListBox.DragMode.DragWithinBox
434  : GUIListBox.DragMode.NoDragging;
435  }
436  };
437 
438  // Previous orders
439  new GUILayoutGroup(new RectTransform(Vector2.One, parent: orderGroup.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
440  {
441  CanBeFocused = false,
442  Stretch = false
443  };
444 
445  var extraIconFrame = new GUIFrame(new RectTransform(new Vector2(0.8f * iconRelativeWidth * 2, 0.8f), layoutGroup.RectTransform), style: null)
446  {
447  CanBeFocused = false,
448  UserData = "extraicons"
449  };
450 
451  var soundIconParent = new GUIFrame(new RectTransform(new Vector2(0.8f), extraIconFrame.RectTransform, Anchor.CenterLeft, scaleBasis: ScaleBasis.Smallest), style: null)
452  {
453  CanBeFocused = false,
454  UserData = "soundicons",
455  Visible = character.IsPlayer
456  };
457  new GUIImage(
458  new RectTransform(Vector2.One, soundIconParent.RectTransform),
459  GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(),
460  scaleToFit: true)
461  {
462  CanBeFocused = false,
463  UserData = new Pair<string, float>("soundicon", 0.0f),
464  Visible = true
465  };
466  new GUIImage(
467  new RectTransform(Vector2.One, soundIconParent.RectTransform),
468  "GUISoundIconDisabled",
469  scaleToFit: true)
470  {
471  CanBeFocused = true,
472  UserData = "soundicondisabled",
473  Visible = false
474  };
475 
476  new GUIButton(new RectTransform(new Point((int)commandButtonAbsoluteHeight), background.RectTransform), style: "CrewListCommandButton")
477  {
478  ToolTip = TextManager.Get("inputtype.command"),
479  OnClicked = (component, userData) =>
480  {
481  if (!CanIssueOrders) { return false; }
482  CreateCommandUI(character);
483  return true;
484  }
485  };
486  if (character.IsBot)
487  {
488  new GUIFrame(new RectTransform(Vector2.One, extraIconFrame.RectTransform, scaleBasis: ScaleBasis.Smallest), style: null)
489  {
490  CanBeFocused = false,
491  UserData = "objectiveicon",
492  Visible = false
493  };
494  }
495  else if (GameMain.GameSession is { TraitorsEnabled: true } && GameMain.Client != null && character != Character.Controlled)
496  {
497  Client targetClient = GameMain.Client.ConnectedClients.FirstOrDefault(c => c.Character == character);
498  if (targetClient != null)
499  {
500  if (OrderPrefab.Prefabs.TryGet("reporttraitor", out OrderPrefab order))
501  {
502  var voteTraitorBtn = new GUITickBox(new RectTransform(Vector2.One, extraIconFrame.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.Smallest), label: string.Empty, style: "TraitorVoteButton")
503  {
504  UserData = character,
505  ToolTip =
507  $"‖color:{XMLExtensions.ToStringHex(GUIStyle.TextColorBright)}‖{TextManager.Get("traitor.blamebutton")}‖color:end‖\n"
508  + TextManager.Get("traitor.blamebutton.tooltip")),
509  OnSelected = (GUITickBox obj) =>
510  {
511  foreach (var traitorBtn in traitorButtons)
512  {
513  //deselect other traitor buttons
514  if (traitorBtn != obj) { traitorBtn.SetSelected(false, callOnSelected: false); }
515  }
516  GameMain.Client?.Vote(VoteType.Traitor, obj.Selected ? targetClient : null);
517  return true;
518  }
519  };
520  traitorButtons.Add(voteTraitorBtn);
521  }
522  }
523  }
524 
525  if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
526  {
527  crewManagement.RefreshUI();
528  }
529 
530  return background;
531  }
532 
533  public void RemoveCharacterFromCrewList(Character character)
534  {
535  if (crewList?.Content.GetChildByUserData(character) is { } component)
536  {
537  crewList.RemoveChild(component);
538  traitorButtons.RemoveAll(t => t.IsChildOf(component, recursive: true));
539  }
540  if (GameMain.GameSession?.Campaign?.CampaignUI?.HRManagerUI is { } crewManagement)
541  {
542  crewManagement.RefreshUI();
543  }
544  }
545 
546  private static void SetCharacterComponentTooltip(GUIComponent characterComponent)
547  {
548  if (!(characterComponent?.UserData is Character character)) { return; }
549  if (character.Info?.Job?.Prefab == null) { return; }
550 
551  LocalizedString tooltip = TextManager.GetWithVariables("crewlistelementtooltip",
552  ("[name]", character.Name),
553  ("[job]", character.Info.Job.Name));
554  string color = XMLExtensions.ColorToString(character.Info.Job.Prefab.UIColor);
555  RichString richToolTip = RichString.Rich($"‖color:{color}‖"+tooltip+"‖color:end‖");
556  characterComponent.ToolTip = richToolTip;
557  }
558 
562  public bool CharacterClicked(GUIComponent component, object selection)
563  {
564  if (!AllowCharacterSwitch) { return false; }
565  if (!(selection is Character character) || character.IsDead || character.IsUnconscious) { return false; }
566  if (!character.IsOnPlayerTeam) { return false; }
567 
568  SelectCharacter(character);
569  if (GUI.KeyboardDispatcher.Subscriber == crewList) { GUI.KeyboardDispatcher.Subscriber = null; }
570  return true;
571  }
572 
573  public void ReviveCharacter(Character revivedCharacter)
574  {
575  if (crewList.Content.GetChildByUserData(revivedCharacter) is GUIComponent characterComponent)
576  {
577  crewList.Content.RemoveChild(characterComponent);
578  }
579  if (characterInfos.Contains(revivedCharacter.Info)) { AddCharacter(revivedCharacter); }
580  }
581 
582  public void KillCharacter(Character killedCharacter, bool resetCrewListIndex = true)
583  {
584  if (crewList.Content.GetChildByUserData(killedCharacter) is GUIComponent characterComponent)
585  {
586  CoroutineManager.StartCoroutine(KillCharacterAnim(characterComponent));
587  }
588  RemoveCharacter(killedCharacter, resetCrewListIndex: resetCrewListIndex);
589  }
590 
591  private IEnumerable<CoroutineStatus> KillCharacterAnim(GUIComponent component)
592  {
593  List<GUIComponent> components = component.GetAllChildren().ToList();
594  components.Add(component);
595  components.RemoveAll(c =>
596  c.UserData is Pair<string, float> pair && pair.First == "soundicon" ||
597  c.UserData as string == "soundicondisabled");
598  components.ForEach(c => c.Color = Color.DarkRed);
599 
600  yield return new WaitForSeconds(1.0f);
601 
602  float timer = 0.0f;
603  float hideDuration = 1.0f;
604  while (timer < hideDuration)
605  {
606  foreach (GUIComponent comp in components)
607  {
608  comp.Color = Color.Lerp(Color.DarkRed, Color.Transparent, timer / hideDuration);
609  comp.RectTransform.LocalScale = new Vector2(comp.RectTransform.LocalScale.X, 1.0f - (timer / hideDuration));
610  }
611  timer += CoroutineManager.DeltaTime;
612  yield return CoroutineStatus.Running;
613  }
614 
615  crewList.Content.RemoveChild(component);
616  // GUITextBlock.AutoScaleAndNormalize(list.Content.GetAllChildren<GUITextBlock>(), defaultScale: 1.0f);
617  crewList.UpdateScrollBarSize();
618 
619  yield return CoroutineStatus.Success;
620  }
621 
622  partial void RenameCharacterProjSpecific(CharacterInfo characterInfo)
623  {
624  if (!(crewList.Content.GetChildByUserData(characterInfo?.Character) is GUIComponent characterComponent)) { return; }
625  SetCharacterComponentTooltip(characterComponent);
626  if (!(characterComponent.FindChild("name", recursive: true) is GUITextBlock nameBlock)) { return; }
627  nameBlock.Text = ToolBox.LimitString(characterInfo.Name, nameBlock.Font, nameBlock.Rect.Width);
628  }
629 
630  private void OnCrewListRearranged(GUIListBox crewList, object draggedElementData)
631  {
632  if (crewList != this.crewList) { return; }
633  if (draggedElementData is not Character) { return; }
634  if (!IsSinglePlayer) { return; }
635  if (crewList.HasDraggedElementIndexChanged)
636  {
637  UpdateCrewListIndices();
638  }
639  else
640  {
641  CharacterClicked(crewList.DraggedElement, draggedElementData);
642  }
643  }
644 
645  private void ResetCrewListIndex(Character c)
646  {
647  if (c?.Info == null) { return; }
648  c.Info.CrewListIndex = -1;
649  UpdateCrewListIndices();
650  }
651 
652  private void UpdateCrewListIndices()
653  {
654  if (crewList == null) { return; }
655  for (int i = 0; i < crewList.Content.CountChildren; i++)
656  {
657  var characterComponent = crewList.Content.GetChild(i);
658  if (characterComponent?.UserData is not Character c) { continue; }
659  if (c.Info == null) { continue; }
660  c.Info.CrewListIndex = i;
661  }
662  }
663 
664  private void SortCrewList()
665  {
666  if (crewList == null) { return; }
667  crewList.Content.RectTransform.SortChildren((x, y) =>
668  {
669  var infoX = (x.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
670  var infoY = (y.GUIComponent.UserData as Character)?.Info?.CrewListIndex;
671  if (infoX.HasValue)
672  {
673  return infoY.HasValue ? infoX.Value.CompareTo(infoY.Value) : -1;
674  }
675  else
676  {
677  return infoY.HasValue ? 1 : 0;
678  }
679  });
680  UpdateCrewListIndices();
681  }
682 
683  #endregion
684 
685  #region Dialog
686 
690  public void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender)
691  {
692  AddSinglePlayerChatMessage(senderName.Value, text.Value, messageType, sender);
693  }
694 
695  public void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Entity sender)
696  {
697  if (!IsSinglePlayer)
698  {
699  DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace.CleanupStackTrace());
700  return;
701  }
702  if (string.IsNullOrEmpty(text)) { return; }
703 
704  if (sender is Character character)
705  {
707  if (!character.IsBot)
708  {
709  character.TextChatVolume = 1f;
710  }
711  }
712  ChatBox.AddMessage(ChatMessage.Create(senderName, text, messageType, sender));
713  }
714 
716  {
717  if (!IsSinglePlayer)
718  {
719  DebugConsole.ThrowError("Cannot add messages to single player chat box in multiplayer mode!\n" + Environment.StackTrace.CleanupStackTrace());
720  return;
721  }
722  if (string.IsNullOrEmpty(message.Text)) { return; }
723 
724  if (message.SenderCharacter != null)
725  {
727  }
728  ChatBox.AddMessage(message);
729  }
730 
731  partial void CreateRandomConversation()
732  {
733  if (GameMain.Client != null)
734  {
735  //let the server create random conversations in MP
736  return;
737  }
738  List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
739  c.AIController is HumanAIController &&
740  !c.IsDead &&
741  c.SpeechImpediment <= 100.0f &&
742  c.CharacterHealth.GetAllAfflictions(a => a is AfflictionHusk huskInfection && huskInfection.Prefab is AfflictionPrefabHusk { CauseSpeechImpediment: true }).None());
743  pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
744  }
745 
746  #endregion
747 
748  #region Voice chat
749 
750  public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally)
751  {
752  if (client?.Character == null) { return; }
753 
754  if (GetSoundIconParent(client.Character) is GUIComponent soundIcons)
755  {
756  var soundIcon = soundIcons.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon");
757  var soundIconDisabled = soundIcons.FindChild("soundicondisabled");
758  soundIcon.Visible = !muted && !mutedLocally;
759  soundIconDisabled.Visible = muted || mutedLocally;
760  soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally");
761  }
762  }
763 
764  public void SetClientSpeaking(Client client)
765  {
766  if (client?.Character != null)
767  {
768  SetCharacterSpeaking(client.Character);
769  }
770  }
771 
772  public void SetCharacterSpeaking(Character character)
773  {
774  if (character == null || character.IsBot) { return; }
775 
776  if (GetSoundIconParent(character)?.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon") is GUIComponent soundIcon)
777  {
778  soundIcon.Color = Color.White;
779  Pair<string, float> userdata = soundIcon.UserData as Pair<string, float>;
780  userdata.Second = 1.0f;
781  }
782  }
783 
784  private GUIComponent GetSoundIconParent(GUIComponent characterComponent)
785  {
786  return characterComponent?
787  .FindChild(c => c is GUILayoutGroup)?
788  .GetChildByUserData("extraicons")?
789  .GetChildByUserData("soundicons");
790  }
791 
792  private GUIComponent GetSoundIconParent(Character character)
793  {
794  return GetSoundIconParent(crewList?.Content.GetChildByUserData(character));
795  }
796 
797  #endregion
798 
799  #region Crew List Order Displayment
800 
805  public void SetCharacterOrder(Character character, Order order, bool isNewOrder = true)
806  {
807  if (order != null && order.TargetAllCharacters)
808  {
809  Hull hull = order.TargetHull;
810  if (order.IsReport)
811  {
812  if (order.OrderGiver?.CurrentHull == null && hull == null) { return; }
813  hull ??= order.OrderGiver.CurrentHull;
814  AddOrder(order.WithTargetEntity(hull), order.FadeOutTime);
815  }
816  if (order.IsDeconstructOrder)
817  {
818  if (order.TargetEntity is Item item)
819  {
820  if (order.Identifier == Tags.DeconstructThis)
821  {
822  foreach (var stackedItem in item.GetStackedItems())
823  {
824  Item.DeconstructItems.Add(stackedItem);
825  }
826  HintManager.OnItemMarkedForDeconstruction(order.OrderGiver);
827  }
828  else
829  {
830  foreach (var stackedItem in item.GetStackedItems())
831  {
832  Item.DeconstructItems.Remove(stackedItem);
833  }
834  }
835  }
836  }
837  else if (order.IsIgnoreOrder)
838  {
839  WallSection ws = null;
840  if (order.TargetType == Order.OrderTargetType.Entity && order.TargetEntity is IIgnorable ignorable)
841  {
842  if (ignorable is Item item)
843  {
844  foreach (var stackedItem in item.GetStackedItems())
845  {
846  stackedItem.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis;
847  AddOrder(order.Clone().WithTargetEntity(stackedItem), fadeOutTime: null);
848  }
849  }
850  else
851  {
852  ignorable.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis;
853  AddOrder(order.Clone(), fadeOutTime: null);
854  }
855  }
856  else if (order.TargetType == Order.OrderTargetType.WallSection && order.TargetEntity is Structure s)
857  {
858  var wallSectionIndex = order.WallSectionIndex ?? s.Sections.IndexOf(wallContext);
859  ws = s.GetSection(wallSectionIndex);
860  if (ws != null)
861  {
862  ws.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis;
863  AddOrder(order.WithWallSection(s, wallSectionIndex), fadeOutTime: null);
864  }
865  }
866  else
867  {
868  return;
869  }
870  if (ws != null)
871  {
872  hull = Hull.FindHull(ws.WorldPosition);
873  }
874  else if (order.TargetEntity is Item item)
875  {
876  hull = item.CurrentHull;
877  }
878  else if (order.TargetEntity is ISpatialEntity se)
879  {
880  hull = Hull.FindHull(se.WorldPosition);
881  }
882  }
883  if (IsSinglePlayer)
884  {
885  order.OrderGiver?.Speak(
886  order.GetChatMessage("", hull?.DisplayName?.Value, givingOrderToSelf: character == order.OrderGiver, isNewOrder: isNewOrder),
887  ChatMessageType.Order);
888  }
889  else
890  {
891  OrderChatMessage msg = new OrderChatMessage(order.WithTargetEntity(order.IsReport ? hull : order.TargetEntity), null, order.OrderGiver, isNewOrder: isNewOrder);
893  }
894  }
895  else
896  {
897  //can't issue an order if no characters are available
898  if (character == null) { return; }
899  var orderGiver = order?.OrderGiver;
900  if (IsSinglePlayer)
901  {
902  bool isGivingOrderToSelf = orderGiver == character;
903  character.SetOrder(order, isNewOrder, speak: !isGivingOrderToSelf);
904  string message = order?.GetChatMessage(character.Name, orderGiver?.CurrentHull?.DisplayName?.Value, isGivingOrderToSelf, orderOption: order?.Option ?? Identifier.Empty, isNewOrder: isNewOrder);
905  orderGiver?.Speak(message);
906  }
907  else if (orderGiver != null)
908  {
909  OrderChatMessage msg = new OrderChatMessage(order, character, orderGiver, isNewOrder: isNewOrder);
911  }
912  }
913  }
914 
918  public void AddCurrentOrderIcon(Character character, Order order)
919  {
920  if (character == null) { return; }
921 
922  var characterComponent = crewList.Content.GetChildByUserData(character);
923 
924  if (characterComponent == null) { return; }
925 
926  var currentOrderIconList = GetCurrentOrderIconList(characterComponent);
927  var currentOrderIcons = currentOrderIconList.Content.Children;
928  var iconsToRemove = new List<GUIComponent>();
929  var newPreviousOrders = new List<Order>();
930  bool updatedExistingIcon = false;
931 
932  foreach (var icon in currentOrderIcons)
933  {
934  var orderInfo = (Order)icon.UserData;
935  var matchingOrder = character.GetCurrentOrder(orderInfo);
936  if (matchingOrder is null)
937  {
938  iconsToRemove.Add(icon);
939  newPreviousOrders.Add(orderInfo);
940  }
941  else if (orderInfo.MatchesOrder(order))
942  {
943  icon.UserData = order.Clone();
944  if (icon is GUIImage image)
945  {
946  image.Sprite = GetOrderIconSprite(order);
947  image.ToolTip = CreateOrderTooltip(order);
948  }
949  updatedExistingIcon = true;
950  }
951  }
952  iconsToRemove.ForEach(c => currentOrderIconList.RemoveChild(c));
953 
954  // Remove a previous order icon if it matches the new order
955  // We don't want the same order as both a current order and a previous order
956  var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent);
957  var previousOrderIcons = previousOrderIconGroup.Children;
958  foreach (var icon in previousOrderIcons)
959  {
960  var orderInfo = (Order)icon.UserData;
961  if (orderInfo.MatchesOrder(order))
962  {
963  previousOrderIconGroup.RemoveChild(icon);
964  break;
965  }
966  }
967 
968  // Rearrange the icons before adding anything
969  if (updatedExistingIcon)
970  {
971  RearrangeIcons();
972  }
973 
974  for (int i = newPreviousOrders.Count - 1; i >= 0; i--)
975  {
976  AddPreviousOrderIcon(character, characterComponent, newPreviousOrders[i]);
977  }
978 
979  if (order == null || order.Identifier == dismissedOrderPrefab.Identifier || updatedExistingIcon)
980  {
981  RearrangeIcons();
982  return;
983  }
984 
985  int orderIconCount = currentOrderIconList.Content.CountChildren + previousOrderIconGroup.CountChildren;
986  if (orderIconCount >= CharacterInfo.MaxCurrentOrders)
987  {
988  RemoveLastOrderIcon(characterComponent);
989  }
990 
991  float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * currentOrderIconList.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing);
992  Point size = new Point((int)nodeWidth, currentOrderIconList.RectTransform.NonScaledSize.Y);
993  var nodeIcon = CreateNodeIcon(size, currentOrderIconList.Content.RectTransform, GetOrderIconSprite(order), order.Color, tooltip: CreateOrderTooltip(order));
994  nodeIcon.UserData = order.Clone();
995  nodeIcon.OnSecondaryClicked = (image, userData) =>
996  {
997  if (!CanIssueOrders) { return false; }
998  var orderInfo = (Order)userData;
999  var order = orderInfo.GetDismissal().WithManualPriority(character.GetCurrentOrder(orderInfo)?.ManualPriority ?? 0).WithOrderGiver(Character.Controlled);
1000  SetCharacterOrder(character, order);
1001  return true;
1002  };
1003 
1004  new GUIFrame(new RectTransform(new Point((int)(1.5f * nodeWidth)), parent: nodeIcon.RectTransform, Anchor.Center), "OuterGlowCircular")
1005  {
1006  CanBeFocused = false,
1007  Color = order.Color,
1008  UserData = "glow",
1009  Visible = false
1010  };
1011 
1012  int hierarchyIndex = Math.Clamp(CharacterInfo.HighestManualOrderPriority - order.ManualPriority, 0, Math.Max(currentOrderIconList.Content.CountChildren - 1, 0));
1013  if (hierarchyIndex != currentOrderIconList.Content.GetChildIndex(nodeIcon))
1014  {
1015  nodeIcon.RectTransform.RepositionChildInHierarchy(hierarchyIndex);
1016  }
1017 
1018  RearrangeIcons();
1019 
1020  void RearrangeIcons()
1021  {
1022  if (character.CurrentOrders != null)
1023  {
1024  // Make sure priority values are up-to-date
1025  foreach (var currentOrderInfo in character.CurrentOrders)
1026  {
1027  var component = currentOrderIconList.Content.FindChild(c => c?.UserData is Order componentOrderInfo &&
1028  componentOrderInfo.MatchesOrder(currentOrderInfo));
1029  if (component == null) { continue; }
1030  var componentOrderInfo = (Order)component.UserData;
1031  int newPriority = currentOrderInfo.ManualPriority;
1032  if (componentOrderInfo.ManualPriority != newPriority)
1033  {
1034  component.UserData = componentOrderInfo.WithManualPriority(newPriority);
1035  }
1036  }
1037 
1038  currentOrderIconList.Content.RectTransform.SortChildren((x, y) =>
1039  {
1040  var xOrder = (Order)x.GUIComponent.UserData;
1041  var yOrder = (Order)y.GUIComponent.UserData;
1042  return yOrder.ManualPriority.CompareTo(xOrder.ManualPriority);
1043  });
1044 
1045  if (currentOrderIconList.Parent is GUILayoutGroup parentGroup)
1046  {
1047  int iconCount = currentOrderIconList.Content.CountChildren;
1048  float nonScaledWidth = ((float)iconCount / CharacterInfo.MaxCurrentOrders) * parentGroup.Rect.Width + (iconCount * currentOrderIconList.Spacing);
1049  currentOrderIconList.RectTransform.NonScaledSize = new Point((int)nonScaledWidth, currentOrderIconList.RectTransform.NonScaledSize.Y);
1050  parentGroup.Recalculate();
1051  previousOrderIconGroup.Recalculate();
1052  }
1053  }
1054  }
1055  }
1056 
1057  private void AddPreviousOrderIcon(Character character, GUIComponent characterComponent, Order orderInfo)
1058  {
1059  if (orderInfo == null || orderInfo.Identifier == dismissedOrderPrefab.Identifier) { return; }
1060 
1061  var currentOrderIconList = GetCurrentOrderIconList(characterComponent);
1062  int maxPreviousOrderIcons = CharacterInfo.MaxCurrentOrders - currentOrderIconList.Content.CountChildren;
1063 
1064  if (maxPreviousOrderIcons < 1) { return; }
1065 
1066  var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent);
1067  if (previousOrderIconGroup.CountChildren >= maxPreviousOrderIcons)
1068  {
1069  RemoveLastPreviousOrderIcon(previousOrderIconGroup);
1070  }
1071 
1072  float nodeWidth = ((1.0f / CharacterInfo.MaxCurrentOrders) * previousOrderIconGroup.Parent.Rect.Width) - ((CharacterInfo.MaxCurrentOrders - 1) * currentOrderIconList.Spacing);
1073  Point size = new Point((int)nodeWidth, previousOrderIconGroup.Rect.Height);
1074  var previousOrderInfo = orderInfo.WithType(Order.OrderType.Previous);
1075  var prevOrderFrame = new GUIButton(new RectTransform(size, parent: previousOrderIconGroup.RectTransform), style: null)
1076  {
1077  UserData = previousOrderInfo,
1078  OnClicked = (button, userData) =>
1079  {
1080  if (!CanIssueOrders) { return false; }
1081  var orderInfo = (Order)userData;
1082  int priority = GetManualOrderPriority(character, orderInfo);
1083  SetCharacterOrder(character, orderInfo.WithManualPriority(priority).WithOrderGiver(Character.Controlled));
1084  return true;
1085  },
1086  OnSecondaryClicked = (button, userData) =>
1087  {
1088  if (previousOrderIconGroup == null) { return false; }
1089  previousOrderIconGroup.RemoveChild(button);
1090  previousOrderIconGroup.Recalculate();
1091  return true;
1092  }
1093  };
1094  prevOrderFrame.RectTransform.IsFixedSize = true;
1095 
1096  var prevOrderIconFrame = new GUIFrame(
1097  new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.BottomLeft),
1098  style: null);
1099 
1100  CreateNodeIcon(Vector2.One,
1101  prevOrderIconFrame.RectTransform,
1102  GetOrderIconSprite(previousOrderInfo),
1103  previousOrderInfo.Color,
1104  tooltip: CreateOrderTooltip(previousOrderInfo));
1105 
1106  foreach (GUIComponent c in prevOrderIconFrame.Children)
1107  {
1108  c.HoverColor = c.Color;
1109  c.PressedColor = c.Color;
1110  c.SelectedColor = c.Color;
1111  }
1112 
1113  new GUIImage(
1114  new RectTransform(new Vector2(0.8f), prevOrderFrame.RectTransform, anchor: Anchor.TopRight),
1115  previousOrderArrow,
1116  scaleToFit: true)
1117  {
1118  CanBeFocused = false
1119  };
1120 
1121  prevOrderFrame.SetAsFirstChild();
1122  }
1123 
1124  private void AddOldPreviousOrderIcons(Character character, GUIComponent oldCharacterComponent)
1125  {
1126  var oldPrevOrderIcons = GetPreviousOrderIconGroup(oldCharacterComponent).Children;
1127  if (oldPrevOrderIcons.None()) { return; }
1128  if (oldPrevOrderIcons.Count() > 1)
1129  {
1130  oldPrevOrderIcons = oldPrevOrderIcons.Reverse();
1131  }
1132  if (crewList.Content.Children.FirstOrDefault(c => c.UserData == character) is GUIComponent newCharacterComponent)
1133  {
1134  foreach (GUIComponent icon in oldPrevOrderIcons)
1135  {
1136  if (icon.UserData is Order orderInfo)
1137  {
1138  AddPreviousOrderIcon(character, newCharacterComponent, orderInfo);
1139  }
1140  }
1141  }
1142  }
1143 
1144  private void RemoveLastOrderIcon(GUIComponent characterComponent)
1145  {
1146  var previousOrderIconGroup = GetPreviousOrderIconGroup(characterComponent);
1147  if (RemoveLastPreviousOrderIcon(previousOrderIconGroup))
1148  {
1149  return;
1150  }
1151  var currentOrderIconList = GetCurrentOrderIconList(characterComponent);
1152  if (currentOrderIconList.Content.CountChildren > 0)
1153  {
1154  var iconToRemove = currentOrderIconList.Content.Children.Last();
1155  currentOrderIconList.RemoveChild(iconToRemove);
1156  return;
1157  }
1158  }
1159 
1160  private bool RemoveLastPreviousOrderIcon(GUILayoutGroup iconGroup)
1161  {
1162  if (iconGroup.CountChildren > 0)
1163  {
1164  var iconToRemove = iconGroup.Children.Last();
1165  iconGroup.RemoveChild(iconToRemove);
1166  return true;
1167  }
1168  return false;
1169  }
1170 
1171  private GUIListBox GetCurrentOrderIconList(GUIComponent characterComponent) =>
1172  characterComponent?.GetChild<GUILayoutGroup>().GetChild<GUILayoutGroup>().GetChild<GUIListBox>();
1173 
1174  private GUILayoutGroup GetPreviousOrderIconGroup(GUIComponent characterComponent) =>
1175  characterComponent?.GetChild<GUILayoutGroup>().GetChild<GUILayoutGroup>().GetChild<GUILayoutGroup>();
1176 
1177  private void OnOrdersRearranged(GUIListBox orderList, object userData)
1178  {
1179  var orderComponent = orderList.Content.GetChildByUserData(userData);
1180  if (orderComponent == null) { return; }
1181  var orderInfo = (Order)userData;
1182  var priority = Math.Max(CharacterInfo.HighestManualOrderPriority - orderList.Content.GetChildIndex(orderComponent), 1);
1183  if (orderInfo.ManualPriority == priority) { return; }
1184  var character = (Character)orderList.UserData;
1185  SetCharacterOrder(character, orderInfo.WithManualPriority(priority), isNewOrder: false);
1186  }
1187 
1188  private LocalizedString CreateOrderTooltip(OrderPrefab orderPrefab, Identifier option, Entity targetEntity)
1189  {
1190  if (orderPrefab == null) { return ""; }
1191  if (option != Identifier.Empty)
1192  {
1193  return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(),
1194  ("[ordername]".ToIdentifier(), orderPrefab.Name),
1195  ("[orderoption]".ToIdentifier(), orderPrefab.GetOptionName(option)));
1196  }
1197  else if (targetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null)
1198  {
1199  return TextManager.GetWithVariables("crewlistordericontooltip".ToIdentifier(),
1200  ("[ordername]".ToIdentifier(), orderPrefab.Name),
1201  ("[orderoption]".ToIdentifier(), targetItem.Name));
1202  }
1203  else
1204  {
1205  return orderPrefab.Name;
1206  }
1207  }
1208 
1209  private LocalizedString CreateOrderTooltip(Order order)
1210  {
1211  if (order.DisplayGiverInTooltip && order.OrderGiver != null)
1212  {
1213  return TextManager.GetWithVariables("crewlistordericontooltip",
1214  ("[ordername]", order.Name),
1215  ("[orderoption]", order.OrderGiver.DisplayName));
1216  }
1217  return CreateOrderTooltip(order.Prefab, order.Option, order?.TargetEntity);
1218  }
1219 
1220  private Sprite GetOrderIconSprite(Order order)
1221  {
1222  if (order == null) { return null; }
1223  Sprite sprite = null;
1224  if (order.Option != Identifier.Empty && order.Prefab.OptionSprites.Any())
1225  {
1226  order.Prefab.OptionSprites.TryGetValue(order.Option, out sprite);
1227  }
1228  if (sprite == null && order.TargetEntity is Item targetItem && targetItem.Prefab.MinimapIcon != null)
1229  {
1230  sprite = targetItem.Prefab.MinimapIcon;
1231  }
1232  return sprite ?? order.SymbolSprite;
1233  }
1234 
1235  #endregion
1236 
1237  #region Updating and drawing the UI
1238 
1239  private void DrawMiniMapOverlay(SpriteBatch spriteBatch, GUICustomComponent container)
1240  {
1241  Submarine sub = container.UserData as Submarine;
1242 
1243  if (sub?.HullVertices == null) { return; }
1244 
1245  var dockedBorders = sub.GetDockedBorders();
1246  dockedBorders.Location += sub.WorldPosition.ToPoint();
1247 
1248  float scale = Math.Min(
1249  container.Rect.Width / (float)dockedBorders.Width,
1250  container.Rect.Height / (float)dockedBorders.Height) * 0.9f;
1251 
1252  float displayScale = ConvertUnits.ToDisplayUnits(scale);
1253  Vector2 offset = (sub.WorldPosition - new Vector2(dockedBorders.Center.X, dockedBorders.Y - dockedBorders.Height / 2)) * scale;
1254  Vector2 center = container.Rect.Center.ToVector2();
1255 
1256  for (int i = 0; i < sub.HullVertices.Count; i++)
1257  {
1258  Vector2 start = (sub.HullVertices[i] * displayScale + offset);
1259  start.Y = -start.Y;
1260  Vector2 end = (sub.HullVertices[(i + 1) % sub.HullVertices.Count] * displayScale + offset);
1261  end.Y = -end.Y;
1262  GUI.DrawLine(spriteBatch, center + start, center + end, Color.DarkCyan * Rand.Range(0.3f, 0.35f), width: 10);
1263  }
1264  }
1265 
1266  public void AddToGUIUpdateList()
1267  {
1268  if (GUI.DisableHUD) { return; }
1269  if (CoroutineManager.IsCoroutineRunning("LevelTransition") || CoroutineManager.IsCoroutineRunning("SubmarineTransition")) { return; }
1270 
1271  commandFrame?.AddToGUIUpdateList(order: 1);
1272 
1273  if (GUI.DisableUpperHUD) { return; }
1274 
1275  if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y || prevUIScale != GUI.Scale)
1276  {
1277  var oldCrewList = crewList;
1278  InitProjectSpecific();
1279 
1280  foreach (GUIComponent oldCharacterComponent in oldCrewList.Content.Children)
1281  {
1282  if (!(oldCharacterComponent.UserData is Character character) || character.IsDead || character.Removed) { continue; }
1283  AddCharacter(character);
1284  AddOldPreviousOrderIcons(character, oldCharacterComponent);
1285  }
1286  }
1287 
1288  crewArea.Visible = GameMain.GameSession?.GameMode is not CampaignMode campaign || (!campaign.ForceMapUI && !campaign.ShowCampaignUI);
1289 
1290  guiFrame.AddToGUIUpdateList();
1291  }
1292 
1293  public void SelectNextCharacter()
1294  {
1295  if (!AllowCharacterSwitch || GameMain.IsMultiplayer || characters.None()) { return; }
1296  if (crewList.Content.GetChild(TryAdjustIndex(1))?.UserData is Character character)
1297  {
1298  SelectCharacter(character);
1299  }
1300  }
1301 
1303  {
1304  if (!AllowCharacterSwitch || GameMain.IsMultiplayer || characters.None()) { return; }
1305  if (crewList.Content.GetChild(TryAdjustIndex(-1))?.UserData is Character character)
1306  {
1307  SelectCharacter(character);
1308  }
1309  }
1310 
1311  private void SelectCharacter(Character character)
1312  {
1313  if (ConversationAction.IsDialogOpen) { return; }
1314  if (!AllowCharacterSwitch) { return; }
1315  //make the previously selected character wait in place for some time
1316  //(so they don't immediately start idling and walking away from their station)
1317  var aiController = Character.Controlled?.AIController;
1318  aiController?.Reset();
1319  DisableCommandUI();
1320  Character.Controlled = character;
1321  HintManager.OnChangeCharacter();
1322  if (GameSession.TabMenuInstance != null && TabMenu.SelectedTab == TabMenu.InfoFrameTab.Talents)
1323  {
1324  GameSession.TabMenuInstance.SelectInfoFrameTab(TabMenu.SelectedTab);
1325  }
1326  }
1327 
1328  private int TryAdjustIndex(int amount)
1329  {
1330  if (Character.Controlled == null) { return 0; }
1331 
1332  int currentIndex = crewList.Content.GetChildIndex(crewList.Content.GetChildByUserData(Character.Controlled));
1333  if (currentIndex == -1) { return 0; }
1334 
1335  int lastIndex = crewList.Content.CountChildren - 1;
1336 
1337  int index = currentIndex + amount;
1338  for (int i = 0; i < crewList.Content.CountChildren; i++)
1339  {
1340  if (index > lastIndex) { index = 0; }
1341  if (index < 0) { index = lastIndex; }
1342 
1343  if ((crewList.Content.GetChild(index)?.UserData as Character)?.IsOnPlayerTeam ?? false)
1344  {
1345  return index;
1346  }
1347 
1348  index += amount;
1349  }
1350 
1351  return 0;
1352  }
1353 
1354  private bool CreateOrder(OrderPrefab orderPrefab, Hull targetHull = null)
1355  {
1356  var sub = Character.Controlled?.Submarine;
1357 
1358  if (sub == null || sub.TeamID != Character.Controlled.TeamID || sub.Info.IsWreck) { return false; }
1359 
1360  var order = new Order(orderPrefab, targetHull, null, Character.Controlled)
1361  .WithManualPriority(CharacterInfo.HighestManualOrderPriority);
1362  SetCharacterOrder(null, order);
1363 
1364  if (IsSinglePlayer)
1365  {
1366  HumanAIController.ReportProblem(Character.Controlled, order);
1367  }
1368 
1369  return true;
1370  }
1371 
1372  private void UpdateOrderDrag()
1373  {
1374  if (DraggedOrderPrefab is { } orderPrefab)
1375  {
1376  if (dropOrder)
1377  {
1378  // stinky workaround
1379  if (framesToSkip > 0)
1380  {
1381  framesToSkip--;
1382  }
1383  else
1384  {
1385  Hull hull = null;
1386 
1387  if (GUI.MouseOn is GUIFrame frame)
1388  {
1389  if (frame.UserData is Hull data)
1390  {
1391  hull = data;
1392  }
1393  else if (frame.Parent?.UserData is Hull parentData)
1394  {
1395  hull = parentData;
1396  }
1397  }
1398 
1399  framesToSkip = 2;
1400  dropOrder = false;
1401  DraggedOrderPrefab = null;
1402 
1403  if (hull is null && GUI.MouseOn is { Visible: true, CanBeFocused: true }) { return; }
1404 
1405  hull ??= Hull.HullList.FirstOrDefault(h => h.WorldRect.ContainsWorld(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition)));
1406  CreateOrder(orderPrefab, hull);
1407  }
1408  }
1409  else
1410  {
1411  DragOrder = DragOrder || Vector2.DistanceSquared(dragPoint, PlayerInput.MousePosition) > dragOrderTreshold * dragOrderTreshold;
1412 
1413  if (!PlayerInput.PrimaryMouseButtonHeld())
1414  {
1415  if (DragOrder)
1416  {
1417  dropOrder = true;
1418  }
1419  else
1420  {
1421  DraggedOrderPrefab = null;
1422  }
1423  dragPoint = Vector2.Zero;
1424  DragOrder = false;
1425  }
1426  }
1427  }
1428  }
1429 
1430  partial void UpdateProjectSpecific(float deltaTime)
1431  {
1432  // Quick selection
1433  if (GameMain.IsSingleplayer && GUI.KeyboardDispatcher.Subscriber == null)
1434  {
1435  if (PlayerInput.KeyHit(InputType.SelectNextCharacter))
1436  {
1438  }
1439  if (PlayerInput.KeyHit(InputType.SelectPreviousCharacter))
1440  {
1442  }
1443  }
1444 
1445  if (GUI.DisableHUD) { return; }
1446 
1447  UpdateOrderDrag();
1448 
1449  #region Command UI
1450 
1451  WasCommandInterfaceDisabledThisUpdate = false;
1452 
1453  if (PlayerInput.KeyDown(InputType.Command) &&
1454  (GUI.KeyboardDispatcher.Subscriber == null || (GUI.KeyboardDispatcher.Subscriber is GUIComponent component && (component == crewList || component.IsChildOf(crewList)))) &&
1455  commandFrame == null && !clicklessSelectionActive && CanIssueOrders && !(GameMain.GameSession?.Campaign?.ShowCampaignUI ?? false) &&
1456  Character.Controlled?.SelectedItem?.Prefab is not { DisableCommandMenuWhenSelected: true } &&
1457  !Inventory.IsMouseOnInventory)
1458  {
1459  if (PlayerInput.KeyDown(InputType.ContextualCommand))
1460  {
1461  CreateCommandUI(FindEntityContext(), true);
1462  }
1463  else
1464  {
1465  CreateCommandUI(CharacterHUD.MouseOnCharacterPortrait() ? Character.Controlled : GUI.MouseOn?.UserData as Character);
1466  }
1467  SoundPlayer.PlayUISound(GUISoundType.PopupMenu);
1468  clicklessSelectionActive = isOpeningClick = true;
1469  }
1470 
1471  if (commandFrame != null)
1472  {
1473  void ResetNodeSelection(GUIButton newSelectedNode = null)
1474  {
1475  if (commandFrame == null) { return; }
1476  selectedNode?.Children.ForEach(c => c.Color = c.HoverColor * nodeColorMultiplier);
1477  selectedNode = newSelectedNode;
1478  timeSelected = 0;
1479  isSelectionHighlighted = false;
1480  }
1481 
1482  // When using Deselect to close the interface, make sure it's not a seconday mouse button click on a node
1483  // That should be reserved for opening manual assignment
1484  bool isMouseOnOptionNode = optionNodes.Any(n => GUI.IsMouseOn(n.Button));
1485  bool isMouseOnShortcutNode = !isMouseOnOptionNode && shortcutNodes.Any(n => GUI.IsMouseOn(n));
1486  bool hitDeselect = PlayerInput.KeyHit(InputType.Deselect) &&
1487  (!PlayerInput.SecondaryMouseButtonClicked() || (!isMouseOnOptionNode && !isMouseOnShortcutNode));
1488 
1489  bool isBoundToPrimaryMouse = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Command].MouseButton == MouseButton.PrimaryMouse;
1490  bool canToggleInterface = !isBoundToPrimaryMouse ||
1491  (!isMouseOnOptionNode && !isMouseOnShortcutNode && extraOptionNodes.None(n => GUI.IsMouseOn(n)) && !GUI.IsMouseOn(returnNode));
1492 
1493  // TODO: Consider using HUD.CloseHUD() instead of KeyHit(Escape), the former method is also used for health UI
1494  if (hitDeselect || PlayerInput.KeyHit(Keys.Escape) || !CanIssueOrders ||
1495  (canToggleInterface && PlayerInput.KeyHit(InputType.Command) && selectedNode == null && !clicklessSelectionActive))
1496  {
1497  DisableCommandUI();
1498  }
1499  else if (PlayerInput.KeyUp(InputType.Command))
1500  {
1501  // Clickless selection behavior
1502  if (canToggleInterface && !isOpeningClick && clicklessSelectionActive && timeSelected < 0.15f)
1503  {
1504  DisableCommandUI();
1505  }
1506  else
1507  {
1508  clicklessSelectionActive = isOpeningClick = false;
1509  if (selectedNode != null)
1510  {
1511  ResetNodeSelection();
1512  }
1513  }
1514  }
1515  else if (PlayerInput.KeyDown(InputType.Command) && (targetFrame == null || !targetFrame.Visible))
1516  {
1517  // Clickless selection behavior
1518  if (!GUI.IsMouseOn(centerNode))
1519  {
1520  clicklessSelectionActive = true;
1521 
1522  var mouseBearing = GetBearing(centerNode.Center, PlayerInput.MousePosition, flipY: true);
1523 
1524  GUIComponent closestNode = null;
1525  float closestBearing = 0;
1526 
1527  optionNodes.ForEach(n => CheckIfClosest(n.Button));
1528  CheckIfClosest(returnNode);
1529 
1530  void CheckIfClosest(GUIComponent comp)
1531  {
1532  if (comp == null) { return; }
1533  var offset = comp.RectTransform.AbsoluteOffset;
1534  var nodeBearing = GetBearing(centerNode.RectTransform.AbsoluteOffset.ToVector2(), offset.ToVector2(), flipY: true);
1535  if (closestNode == null)
1536  {
1537  closestNode = comp;
1538  closestBearing = Math.Abs(nodeBearing - mouseBearing);
1539  }
1540  else
1541  {
1542  var difference = Math.Abs(nodeBearing - mouseBearing);
1543  if (difference < closestBearing)
1544  {
1545  closestNode = comp;
1546  closestBearing = difference;
1547  }
1548  }
1549  }
1550 
1551  if (closestNode != null && closestNode.CanBeFocused && closestNode == selectedNode)
1552  {
1553  timeSelected += deltaTime;
1554  if (timeSelected >= selectionTime)
1555  {
1556  if (PlayerInput.IsShiftDown() && selectedNode.OnSecondaryClicked != null)
1557  {
1558  selectedNode.OnSecondaryClicked.Invoke(selectedNode, selectedNode.UserData);
1559  }
1560  else
1561  {
1562  selectedNode.OnClicked?.Invoke(selectedNode, selectedNode.UserData);
1563  }
1564  ResetNodeSelection();
1565  }
1566  else if (timeSelected >= 0.15f && !isSelectionHighlighted)
1567  {
1568  selectedNode.Children.ForEach(c => c.Color = c.HoverColor);
1569  isSelectionHighlighted = true;
1570  }
1571  }
1572  else
1573  {
1574  ResetNodeSelection(closestNode as GUIButton);
1575  }
1576  }
1577  else if (selectedNode != null)
1578  {
1579  ResetNodeSelection();
1580  }
1581  }
1582 
1583  var hotkeyHit = false;
1584  foreach (OptionNode node in optionNodes)
1585  {
1586  if (node.Keys != Keys.None && PlayerInput.KeyHit(node.Keys))
1587  {
1588  var button = node.Button;
1589  if (PlayerInput.IsShiftDown() && button?.OnSecondaryClicked != null)
1590  {
1591  button.OnSecondaryClicked.Invoke(button, button.UserData);
1592  }
1593  else
1594  {
1595  button?.OnClicked?.Invoke(button, button.UserData);
1596  }
1597  ResetNodeSelection();
1598  hotkeyHit = true;
1599  break;
1600  }
1601  }
1602 
1603  if (!hotkeyHit)
1604  {
1605  if (returnNodeHotkey != Keys.None && PlayerInput.KeyHit(returnNodeHotkey))
1606  {
1607  returnNode?.OnClicked?.Invoke(returnNode, returnNode.UserData);
1608  ResetNodeSelection();
1609  }
1610  else if (expandNodeHotkey != Keys.None && PlayerInput.KeyHit(expandNodeHotkey))
1611  {
1612  expandNode?.OnClicked?.Invoke(expandNode, expandNode.UserData);
1613  ResetNodeSelection();
1614  }
1615  }
1616  }
1617  else if (!PlayerInput.KeyDown(InputType.Command))
1618  {
1619  clicklessSelectionActive = false;
1620  }
1621 
1622  #endregion
1623 
1624  if (ChatBox != null)
1625  {
1626  ChatBox.Update(deltaTime);
1627  ChatBox.InputBox.Visible = Character.Controlled != null;
1628  if (!DebugConsole.IsOpen && ChatBox.InputBox.Visible && GUI.KeyboardDispatcher.Subscriber == null && !ChatBox.InputBox.Selected)
1629  {
1630  ChatBox.ApplySelectionInputs();
1631  }
1632  }
1633 
1634  if (!GUI.DisableUpperHUD)
1635  {
1636  crewArea.Visible = characters.Count > 0 && CharacterHealth.OpenHealthWindow == null;
1637 
1638  foreach (GUIComponent characterComponent in crewList.Content.Children)
1639  {
1640  if (characterComponent.UserData is Character character)
1641  {
1642  if (character.Removed)
1643  {
1644  characterComponent.Visible = false;
1645  continue;
1646  }
1647 
1648  characterComponent.Visible = Character.Controlled == null || Character.Controlled.TeamID == character.TeamID;
1649  if (character.TeamID == CharacterTeamType.FriendlyNPC && Character.Controlled != null &&
1650  (character.CurrentHull == Character.Controlled.CurrentHull || Vector2.DistanceSquared(Character.Controlled.WorldPosition, character.WorldPosition) < 500.0f * 500.0f))
1651  {
1652  characterComponent.Visible = true;
1653  }
1654  if (characterComponent.Visible)
1655  {
1656  if (character == Character.Controlled && crewList.SelectedComponent != characterComponent)
1657  {
1658  crewList.Select(character, GUIListBox.Force.Yes);
1659  }
1660  // Icon colors might change based on the target so we check if they need to be updated
1661  if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList)
1662  {
1663  foreach (var orderIcon in currentOrderIconList.Content.Children)
1664  {
1665  if (orderIcon.UserData is not Order order) { continue; }
1666  if (order.ColoredWhenControllingGiver && order.OrderGiver != Character.Controlled)
1667  {
1668  orderIcon.Color = AIObjective.ObjectiveIconColor;
1669  }
1670  else
1671  {
1672  orderIcon.Color = order.Color;
1673  }
1674  }
1675  }
1676  // Only update the order highlights and objective icons here in singleplayer
1677  // The server will let the clients know when they need to update in multiplayer
1678  if (GameMain.IsSingleplayer && character.IsBot && character.AIController is HumanAIController controller &&
1679  controller.ObjectiveManager is AIObjectiveManager objectiveManager)
1680  {
1681  if (objectiveManager.CurrentObjective is AIObjective currentObjective)
1682  {
1683  if (objectiveManager.IsOrder(currentObjective))
1684  {
1685  var orderInfo = objectiveManager.CurrentOrders.FirstOrDefault(o => o.Objective == currentObjective);
1686  if (orderInfo != null)
1687  {
1688  SetOrderHighlight(characterComponent, orderInfo.Identifier, orderInfo.Option);
1689  }
1690  }
1691  else
1692  {
1693  CreateObjectiveIcon(characterComponent, currentObjective);
1694  }
1695  }
1696  }
1697  // Order highlighting and objective icons are intended to communicate bot behavior so they should be disabled for player characters
1698  if (character.IsPlayer)
1699  {
1700  DisableOrderHighlight(characterComponent);
1701  RemoveObjectiveIcon(characterComponent);
1702  }
1703  if (GetSoundIconParent(characterComponent) is GUIComponent soundIconParent)
1704  {
1705  if (soundIconParent.FindChild(c => c.UserData is Pair<string, float> pair && pair.First == "soundicon") is GUIImage soundIcon)
1706  {
1707  if (character.IsPlayer)
1708  {
1709  soundIconParent.Visible = true;
1710  VoipClient.UpdateVoiceIndicator(soundIcon, 0.0f, deltaTime);
1711  }
1712  else if(soundIcon.Visible)
1713  {
1714  var userdata = soundIcon.UserData as Pair<string, float>;
1715  userdata.Second = 0.0f;
1716  soundIconParent.Visible = soundIcon.Visible = false;
1717  }
1718  }
1719  }
1720  }
1721  }
1722  }
1723 
1724  traitorButtons.ForEach(btn => btn.Visible = Character.Controlled is { IsDead: false } && btn.UserData as Character != Character.Controlled);
1725 
1726  crewArea.RectTransform.AbsoluteOffset = Vector2.SmoothStep(
1727  new Vector2(-crewArea.Rect.Width - HUDLayoutSettings.Padding, 0.0f),
1728  Vector2.Zero,
1729  crewListOpenState).ToPoint();
1730 
1731  crewListOpenState = IsCrewMenuOpen ?
1732  Math.Min(crewListOpenState + deltaTime * 2.0f, 1.0f) :
1733  Math.Max(crewListOpenState - deltaTime * 2.0f, 0.0f);
1734 
1735  if (GUI.KeyboardDispatcher.Subscriber == null && PlayerInput.KeyHit(InputType.CrewOrders))
1736  {
1737  SoundPlayer.PlayUISound(GUISoundType.PopupMenu);
1738  IsCrewMenuOpen = !IsCrewMenuOpen;
1739  }
1740  }
1741 
1742  UpdateReports();
1743  }
1744 
1745  private void SetOrderHighlight(GUIComponent characterComponent, Identifier orderIdentifier, Identifier orderOption)
1746  {
1747  if (characterComponent == null) { return; }
1748  RemoveObjectiveIcon(characterComponent);
1749  if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList)
1750  {
1751  bool foundMatch = false;
1752  foreach (var orderIcon in currentOrderIconList.Content.Children)
1753  {
1754  if (orderIcon.GetChildByUserData("glow") is not GUIComponent glowComponent) { continue; }
1755  glowComponent.Color = orderIcon.Color;
1756  if (foundMatch)
1757  {
1758  glowComponent.Visible = false;
1759  continue;
1760  }
1761  var orderInfo = (Order)orderIcon.UserData;
1762  foundMatch = orderInfo.MatchesOrder(orderIdentifier, orderOption);
1763  glowComponent.Visible = foundMatch;
1764  }
1765  }
1766  }
1767 
1768  public void SetOrderHighlight(Character character, Identifier orderIdentifier, Identifier orderOption)
1769  {
1770  if (crewList == null) { return; }
1771  var characterComponent = crewList.Content.GetChildByUserData(character);
1772  SetOrderHighlight(characterComponent, orderIdentifier, orderOption);
1773  }
1774 
1775  private void DisableOrderHighlight(GUIComponent characterComponent)
1776  {
1777  if (GetCurrentOrderIconList(characterComponent) is GUIListBox currentOrderIconList)
1778  {
1779  foreach (var orderIcon in currentOrderIconList.Content.Children)
1780  {
1781  var glowComponent = orderIcon.GetChildByUserData("glow");
1782  if (glowComponent == null) { continue; }
1783  glowComponent.Visible = false;
1784  }
1785  }
1786  }
1787 
1788  private void CreateObjectiveIcon(GUIComponent characterComponent, Sprite sprite, LocalizedString tooltip)
1789  {
1790  if (characterComponent == null || !(characterComponent.UserData is Character character) || character.IsPlayer) { return; }
1791  DisableOrderHighlight(characterComponent);
1792  if (GetObjectiveIconParent(characterComponent) is GUIFrame objectiveIconFrame)
1793  {
1794  var existingObjectiveIcon = objectiveIconFrame.GetChild<GUIImage>();
1795  if (existingObjectiveIcon == null || existingObjectiveIcon.Sprite != sprite || existingObjectiveIcon.ToolTip != tooltip)
1796  {
1797  objectiveIconFrame.ClearChildren();
1798  if (sprite != null)
1799  {
1800  var objectiveIcon = CreateNodeIcon(Vector2.One, objectiveIconFrame.RectTransform, sprite, AIObjective.ObjectiveIconColor, tooltip: tooltip);
1801  new GUIFrame(new RectTransform(new Vector2(1.5f), objectiveIcon.RectTransform, anchor: Anchor.Center), style: "OuterGlowCircular")
1802  {
1803  CanBeFocused = false,
1804  Color = AIObjective.ObjectiveIconColor
1805  };
1806  objectiveIconFrame.Visible = true;
1807  }
1808  else
1809  {
1810  objectiveIconFrame.Visible = false;
1811  }
1812  }
1813  }
1814  }
1815 
1816  public void CreateObjectiveIcon(Character character, Identifier identifier, Identifier option, Entity targetEntity)
1817  {
1818  CreateObjectiveIcon(crewList?.Content.GetChildByUserData(character),
1819  AIObjective.GetSprite(identifier, option, targetEntity),
1820  GetObjectiveIconTooltip(identifier, option, targetEntity));
1821  }
1822 
1823  private void CreateObjectiveIcon(GUIComponent characterComponent, AIObjective objective)
1824  {
1825  CreateObjectiveIcon(characterComponent,
1826  objective?.GetSprite(),
1827  GetObjectiveIconTooltip(objective));
1828  }
1829 
1830  private LocalizedString GetObjectiveIconTooltip(Identifier identifier, Identifier option, Entity targetEntity)
1831  {
1832  LocalizedString variableValue;
1833  if (OrderPrefab.Prefabs.ContainsKey(identifier))
1834  {
1835  var orderPrefab = OrderPrefab.Prefabs[identifier];
1836  variableValue = CreateOrderTooltip(orderPrefab, option, targetEntity);
1837  }
1838  else
1839  {
1840  variableValue = TextManager.Get($"objective.{identifier}");
1841  }
1842  return variableValue.IsNullOrEmpty() ? variableValue : TextManager.GetWithVariable("crewlistobjectivetooltip", "[objective]", variableValue);
1843  }
1844 
1845  private LocalizedString GetObjectiveIconTooltip(AIObjective objective)
1846  {
1847  return objective == null ? "" :
1848  GetObjectiveIconTooltip(objective.Identifier, objective.Option, (objective as AIObjectiveOperateItem)?.OperateTarget);
1849  }
1850 
1851  private GUIComponent GetObjectiveIconParent(GUIComponent characterComponent)
1852  {
1853  return characterComponent?
1854  .GetChild<GUILayoutGroup>()?
1855  .GetChildByUserData("extraicons")?
1856  .GetChildByUserData("objectiveicon");
1857  }
1858 
1859  private void RemoveObjectiveIcon(GUIComponent characterComponent)
1860  {
1861  if (GetObjectiveIconParent(characterComponent) is GUIFrame objectiveIconFrame)
1862  {
1863  objectiveIconFrame.ClearChildren();
1864  objectiveIconFrame.Visible = false;
1865  }
1866  }
1867 
1868  #endregion
1869 
1870  #region Command UI
1871 
1872  public static bool IsCommandInterfaceOpen
1873  {
1874  get
1875  {
1876  if (GameMain.GameSession?.CrewManager == null)
1877  {
1878  return false;
1879  }
1880  else
1881  {
1882  return GameMain.GameSession.CrewManager.commandFrame != null || GameMain.GameSession.CrewManager.WasCommandInterfaceDisabledThisUpdate;
1883  }
1884  }
1885  }
1886  private GUIFrame commandFrame, targetFrame;
1887  private GUIButton centerNode, returnNode, expandNode;
1888  private GUIFrame shortcutCenterNode;
1889  private class OptionNode
1890  {
1891  public readonly GUIButton Button;
1892  public readonly Keys Keys;
1893  public OptionNode(GUIButton guiComponent, Keys keys)
1894  {
1895  Button = guiComponent;
1896  Keys = keys;
1897  }
1898  }
1899  private readonly List<OptionNode> optionNodes = new List<OptionNode>();
1900  private Keys returnNodeHotkey = Keys.None, expandNodeHotkey = Keys.None;
1901  private readonly List<GUIComponent> shortcutNodes = new List<GUIComponent>();
1902  private readonly List<GUIComponent> extraOptionNodes = new List<GUIComponent>();
1903  private GUICustomComponent nodeConnectors;
1904  private GUIImage background;
1905 
1906  private GUIButton selectedNode;
1907  private readonly float selectionTime = 0.75f;
1908  private float timeSelected = 0.0f;
1909  private bool clicklessSelectionActive, isOpeningClick, isSelectionHighlighted;
1910 
1911  private Point centerNodeSize, nodeSize, shortcutCenterNodeSize, shortcutNodeSize, returnNodeSize, assignmentNodeSize;
1912  private float centerNodeMargin, optionNodeMargin, shortcutCenterNodeMargin, shortcutNodeMargin, returnNodeMargin;
1913 
1914  private List<OrderCategory> availableCategories;
1915  private Stack<GUIButton> historyNodes = new Stack<GUIButton>();
1916  private readonly List<Character> extraOptionCharacters = new List<Character>();
1917 
1921  private const float nodeColorMultiplier = 0.75f;
1922  private int nodeDistance = (int)(GUI.Scale * 250);
1923  private const float returnNodeDistanceModifier = 0.65f;
1924  private OrderPrefab dismissedOrderPrefab => OrderPrefab.Dismissal;
1925  private Character characterContext;
1926  private Item itemContext;
1927  private Hull hullContext;
1928  private WallSection wallContext;
1929  private bool isContextual;
1930  private readonly List<Order> contextualOrders = new List<Order>();
1931  private Point shorcutCenterNodeOffset;
1932  private const int maxShortcutNodeCount = 4;
1933 
1934  private bool WasCommandInterfaceDisabledThisUpdate { get; set; }
1935  public static bool CanIssueOrders
1936  {
1937  get
1938  {
1939 #if DEBUG
1940  if (Character.Controlled == null) { return true; }
1941 #endif
1942  return Character.Controlled?.Info != null && Character.Controlled.SpeechImpediment < 100.0f;
1943 
1944  }
1945  }
1946 
1947  private bool CanCharacterBeHeard()
1948  {
1949 #if DEBUG
1950  if (Character.Controlled == null) { return true; }
1951 #endif
1952  if (Character.Controlled != null)
1953  {
1954  if (characterContext == null)
1955  {
1956  return characters.Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled)) || GetOrderableFriendlyNPCs().Any(c => c != Character.Controlled && c.CanHearCharacter(Character.Controlled));
1957  }
1958  else
1959  {
1960  return characterContext.CanHearCharacter(Character.Controlled);
1961  }
1962  }
1963  return false;
1964  }
1965 
1966  private Entity FindEntityContext()
1967  {
1968  if (Character.Controlled?.FocusedCharacter is Character focusedCharacter && !focusedCharacter.IsDead &&
1969  HumanAIController.IsFriendly(Character.Controlled, focusedCharacter) && Character.Controlled.TeamID == focusedCharacter.TeamID)
1970  {
1971  if (Character.Controlled?.FocusedItem != null)
1972  {
1973  Vector2 mousePos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
1974  if (Vector2.Distance(mousePos, focusedCharacter.WorldPosition) < Vector2.Distance(mousePos, Character.Controlled.FocusedItem.WorldPosition))
1975  {
1976  return focusedCharacter;
1977  }
1978  else
1979  {
1980  return Character.Controlled.FocusedItem;
1981  }
1982  }
1983  else
1984  {
1985  return focusedCharacter;
1986  }
1987 
1988  }
1989  else if (TryGetBreachedHullAtHoveredWall(out Hull breachedHull, out wallContext))
1990  {
1991  return breachedHull;
1992  }
1993  else
1994  {
1995  return Character.Controlled?.FocusedItem;
1996  }
1997  }
1998 
1999  public void OpenCommandUI(Entity entityContext = null, bool forceContextual = false)
2000  {
2001  CreateCommandUI(entityContext, forceContextual);
2002  SoundPlayer.PlayUISound(GUISoundType.PopupMenu);
2003  clicklessSelectionActive = isOpeningClick = true;
2004  }
2005 
2006  private void CreateCommandUI(Entity entityContext = null, bool forceContextual = false)
2007  {
2008  if (commandFrame != null) { DisableCommandUI(); }
2009 
2010  // Character context works differently to others as we still use the "basic" command interface,
2011  // but the order will be automatically assigned to this character
2012  isContextual = forceContextual;
2013  if (entityContext is Character character)
2014  {
2015  characterContext = character;
2016  itemContext = null;
2017  hullContext = null;
2018  wallContext = null;
2019  isContextual = false;
2020  }
2021  else if (entityContext is Item item)
2022  {
2023  itemContext = item;
2024  characterContext = null;
2025  hullContext = null;
2026  wallContext = null;
2027  isContextual = true;
2028  }
2029  else if (entityContext is Hull hull)
2030  {
2031  hullContext = hull;
2032  characterContext = null;
2033  itemContext = null;
2034  isContextual = true;
2035  }
2036 
2037  ScaleCommandUI();
2038 
2039  commandFrame = new GUIFrame(
2040  new RectTransform(Vector2.One, GUI.Canvas, anchor: Anchor.Center),
2041  style: null,
2042  color: Color.Transparent);
2043  background = new GUIImage(
2044  new RectTransform(Vector2.One, commandFrame.RectTransform, anchor: Anchor.Center),
2045  "CommandBackground");
2046  background.Color = background.Color * 0.8f;
2047  GUIButton startNode = null;
2048  if (characterContext == null)
2049  {
2050  startNode = new GUIButton(
2051  new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center),
2052  style: null);
2053  CreateNodeIcon(startNode.RectTransform, "CommandStartNode");
2054  }
2055  else
2056  {
2057  // Button
2058  startNode = new GUIButton(
2059  new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center),
2060  style: null);
2061  // Container
2062  new GUIImage(
2063  new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center),
2064  "CommandNodeContainer",
2065  scaleToFit: true)
2066  {
2067  Color = characterContext.Info?.Job?.Prefab != null ? characterContext.Info.Job.Prefab.UIColor * nodeColorMultiplier : Color.White,
2068  HoverColor = characterContext.Info?.Job?.Prefab != null ? characterContext.Info.Job.Prefab.UIColor : Color.White,
2069  UserData = "colorsource"
2070  };
2071  // Character icon
2072  var characterIcon = new GUICustomComponent(
2073  new RectTransform(Vector2.One, startNode.RectTransform, anchor: Anchor.Center),
2074  (spriteBatch, _) =>
2075  {
2076  if (entityContext is not Character character || character?.Info == null) { return; }
2077  var node = startNode;
2078  character.Info.DrawJobIcon(spriteBatch,
2079  new Rectangle((int)(node.Rect.X + node.Rect.Width * 0.5f), (int)(node.Rect.Y + node.Rect.Height * 0.1f), (int)(node.Rect.Width * 0.6f), (int)(node.Rect.Height * 0.8f)));
2080  character.Info.DrawIcon(spriteBatch, new Vector2(node.Rect.X + node.Rect.Width * 0.35f, node.Center.Y), node.Rect.Size.ToVector2() * 0.7f);
2081  });
2082  SetCharacterTooltip(characterIcon, entityContext as Character);
2083  }
2084  SetCenterNode(startNode);
2085 
2086  availableCategories ??= GetAvailableCategories();
2087 
2088  if (isContextual)
2089  {
2090  CreateContextualOrderNodes();
2091  }
2092  else
2093  {
2094  CreateShortcutNodes();
2095  CreateOrderCategoryNodes();
2096  }
2097 
2098  CreateNodeConnectors();
2099  if (Character.Controlled != null)
2100  {
2101  Character.Controlled.dontFollowCursor = true;
2102  }
2103 
2104  HintManager.OnShowCommandInterface();
2105  }
2106 
2107  public void ToggleCommandUI()
2108  {
2109  if (commandFrame == null)
2110  {
2111  if (CanIssueOrders)
2112  {
2113  CreateCommandUI();
2114  }
2115  }
2116  else
2117  {
2118  DisableCommandUI();
2119  }
2120  }
2121 
2122  private void ScaleCommandUI()
2123  {
2124  // Node sizes
2125  nodeSize = new Point((int)(100 * GUI.Scale));
2126  centerNodeSize = nodeSize;
2127  returnNodeSize = new Point((int)(48 * GUI.Scale));
2128  assignmentNodeSize = new Point((int)(64 * GUI.Scale));
2129  shortcutCenterNodeSize = returnNodeSize;
2130  shortcutNodeSize = assignmentNodeSize;
2131 
2132  // Node margins (used in drawing the connecting lines)
2133  centerNodeMargin = centerNodeSize.X * 0.5f;
2134  optionNodeMargin = nodeSize.X * 0.5f;
2135  shortcutCenterNodeMargin = shortcutCenterNodeSize.X * 0.45f;
2136  shortcutNodeMargin = shortcutNodeSize.X * 0.5f;
2137  returnNodeMargin = returnNodeSize.X * 0.5f;
2138 
2139  nodeDistance = (int)(150 * GUI.Scale);
2140  shorcutCenterNodeOffset = new Point(0, (int)(1.35f * nodeDistance));
2141  }
2142 
2143  private List<OrderCategory> GetAvailableCategories()
2144  {
2145  availableCategories = new List<OrderCategory>();
2146  foreach (OrderCategory category in Enum.GetValues(typeof(OrderCategory)))
2147  {
2148  if (OrderPrefab.Prefabs.Any(o => o.Category == category && !o.IsReport))
2149  {
2150  availableCategories.Add(category);
2151  }
2152  }
2153  return availableCategories;
2154  }
2155 
2156  private void CreateNodeConnectors()
2157  {
2158  nodeConnectors = new GUICustomComponent(
2159  new RectTransform(Vector2.One, commandFrame.RectTransform),
2160  onDraw: DrawNodeConnectors)
2161  {
2162  CanBeFocused = false
2163  };
2164  nodeConnectors.SetAsFirstChild();
2165  background.SetAsFirstChild();
2166  }
2167 
2168  private void DrawNodeConnectors(SpriteBatch spriteBatch, GUIComponent container)
2169  {
2170  if (centerNode == null || optionNodes == null) { return; }
2171  var startNodePos = centerNode.Rect.Center.ToVector2();
2172  // Don't draw connectors for assignment nodes
2173  if (optionNodes.FirstOrDefault()?.Button.UserData is not Character)
2174  {
2175  // Regular option nodes
2176  if (targetFrame == null || !targetFrame.Visible)
2177  {
2178  optionNodes.ForEach(n => DrawNodeConnector(startNodePos, centerNodeMargin, n.Button, optionNodeMargin, spriteBatch));
2179  }
2180  // Minimap item nodes
2181  else
2182  {
2183  foreach (var node in optionNodes)
2184  {
2185  float iconRadius = 0.5f * optionNodeMargin;
2186  Vector2 itemPosition = node.Button.Parent.Rect.Center.ToVector2();
2187  if (Vector2.Distance(node.Button.Center, itemPosition) <= iconRadius) { continue; }
2188  DrawNodeConnector(itemPosition, 0.0f, node.Button, iconRadius, spriteBatch, widthMultiplier: 0.5f);
2189  GUI.DrawFilledRectangle(spriteBatch, itemPosition - Vector2.One, new Vector2(3),
2190  node.Button.GetChildByUserData("colorsource")?.Color ?? Color.White);
2191  }
2192  }
2193  }
2194  DrawNodeConnector(startNodePos, centerNodeMargin, returnNode, returnNodeMargin, spriteBatch);
2195  if (shortcutCenterNode == null || !shortcutCenterNode.Visible) { return; }
2196  DrawNodeConnector(startNodePos, centerNodeMargin, shortcutCenterNode, shortcutCenterNodeMargin, spriteBatch);
2197  startNodePos = shortcutCenterNode.Rect.Center.ToVector2();
2198  shortcutNodes.ForEach(n => DrawNodeConnector(startNodePos, shortcutCenterNodeMargin, n, shortcutNodeMargin, spriteBatch));
2199  }
2200 
2201  private void DrawNodeConnector(Vector2 startNodePos, float startNodeMargin, GUIComponent endNode, float endNodeMargin, SpriteBatch spriteBatch, float widthMultiplier = 1.0f)
2202  {
2203  if (endNode == null || !endNode.Visible) { return; }
2204  var endNodePos = endNode.Rect.Center.ToVector2();
2205  var direction = (endNodePos - startNodePos) / Vector2.Distance(startNodePos, endNodePos);
2206  var start = startNodePos + direction * startNodeMargin;
2207  var end = endNodePos - direction * endNodeMargin;
2208  var colorSource = endNode.GetChildByUserData("colorsource");
2209  if ((selectedNode == null && endNode != shortcutCenterNode && GUI.IsMouseOn(endNode)) ||
2210  (isSelectionHighlighted && (endNode == selectedNode || (endNode == shortcutCenterNode && shortcutNodes.Any(n => GUI.IsMouseOn(n))))))
2211  {
2212  GUI.DrawLine(spriteBatch, start, end, colorSource?.HoverColor ?? Color.White, width: Math.Max(widthMultiplier * 4.0f, 1.0f));
2213  }
2214  else
2215  {
2216  GUI.DrawLine(spriteBatch, start, end, colorSource?.Color ?? Color.White * nodeColorMultiplier, width: Math.Max(widthMultiplier * 2.0f, 1.0f));
2217  }
2218  }
2219 
2220  public void DisableCommandUI()
2221  {
2222  if (commandFrame == null) { return; }
2223  WasCommandInterfaceDisabledThisUpdate = true;
2224  RemoveOptionNodes();
2225  historyNodes.Clear();
2226  nodeConnectors = null;
2227  centerNode = null;
2228  returnNode = null;
2229  expandNode = null;
2230  shortcutCenterNode = null;
2231  targetFrame = null;
2232  selectedNode = null;
2233  timeSelected = 0;
2234  background = null;
2235  commandFrame = null;
2236  extraOptionCharacters.Clear();
2237  isOpeningClick = isSelectionHighlighted = false;
2238  characterContext = null;
2239  itemContext = null;
2240  isContextual = false;
2241  contextualOrders.Clear();
2242  returnNodeHotkey = expandNodeHotkey = Keys.None;
2243  if (Character.Controlled != null)
2244  {
2246  }
2247  }
2248 
2249  private bool NavigateForward(GUIButton node, object userData)
2250  {
2251  if (commandFrame == null) { return false; }
2252  if (optionNodes.Find(n => n.Button == node) is not OptionNode optionNode || !optionNodes.Remove(optionNode))
2253  {
2254  shortcutNodes.Remove(node);
2255  };
2256  RemoveOptionNodes();
2257  bool wasMinimapVisible = targetFrame != null && targetFrame.Visible;
2258  HideMinimap();
2259 
2260  if (returnNode != null)
2261  {
2262  returnNode.RemoveChild(returnNode.GetChildByUserData("hotkey"));
2263  returnNode.Children.ForEach(child => child.Visible = false);
2264  returnNode.Visible = false;
2265  historyNodes.Push(returnNode);
2266  }
2267 
2268  // When the mini map is shown, always position the return node on the bottom
2269  bool placeReturnNodeOnTheBottom = wasMinimapVisible ||
2270  (node?.UserData is Order order && order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).Count > 1);
2271  var offset = placeReturnNodeOnTheBottom ?
2272  new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)) :
2273  node.RectTransform.AbsoluteOffset.Multiply(-returnNodeDistanceModifier);
2274  SetReturnNode(centerNode, offset);
2275 
2276  SetCenterNode(node);
2277  if (shortcutCenterNode != null)
2278  {
2279  commandFrame.RemoveChild(shortcutCenterNode);
2280  shortcutCenterNode = null;
2281  }
2282 
2283  CreateNodes(userData);
2284  CreateReturnNodeHotkey();
2285  return true;
2286  }
2287 
2288  private bool NavigateBackward(GUIButton node, object userData)
2289  {
2290  if (commandFrame == null) { return false; }
2291  RemoveOptionNodes();
2292  HideMinimap();
2293  // TODO: Center node could move to option node instead of being removed
2294  commandFrame.RemoveChild(centerNode);
2295  SetCenterNode(node);
2296  if (historyNodes.Count > 0)
2297  {
2298  var historyNode = historyNodes.Pop();
2299  SetReturnNode(historyNode, historyNode.RectTransform.AbsoluteOffset);
2300  historyNode.Visible = true;
2301  historyNode.RemoveChild(historyNode.GetChildByUserData("hotkey"));
2302  historyNode.Children.ForEach(child => child.Visible = true);
2303  }
2304  else
2305  {
2306  returnNode = null;
2307  }
2308  CreateNodes(userData);
2309  CreateReturnNodeHotkey();
2310  return true;
2311  }
2312 
2313  private void HideMinimap()
2314  {
2315  if (targetFrame == null || !targetFrame.Visible) { return; }
2316  targetFrame.Visible = false;
2317  // Reset the node connectors to their original parent
2318  nodeConnectors.RectTransform.Parent = commandFrame.RectTransform;
2319  nodeConnectors.RectTransform.RepositionChildInHierarchy(1);
2320  }
2321 
2322  private void CreateReturnNodeHotkey()
2323  {
2324  if (returnNode != null && returnNode.Visible)
2325  {
2326  var hotkey = 1;
2327  if (targetFrame == null || !targetFrame.Visible)
2328  {
2329  hotkey = optionNodes.Count + 1;
2330  if (expandNode != null && expandNode.Visible) { hotkey += 1; }
2331  }
2332  CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true);
2333  returnNodeHotkey = Keys.D0 + hotkey % 10;
2334  }
2335  else
2336  {
2337  returnNodeHotkey = Keys.None;
2338  }
2339  }
2340 
2341  private void SetCenterNode(GUIButton node, bool resetAnchor = false)
2342  {
2343  node.RectTransform.Parent = commandFrame.RectTransform;
2344  if (resetAnchor)
2345  {
2346  node.RectTransform.SetPosition(Anchor.Center);
2347  }
2348  node.RectTransform.SetPosition(Anchor.Center);
2349  node.RectTransform.MoveOverTime(Point.Zero, CommandNodeAnimDuration);
2350  node.RectTransform.ScaleOverTime(centerNodeSize, CommandNodeAnimDuration);
2351  node.RemoveChild(node.GetChildByUserData("hotkey"));
2352  foreach (GUIComponent c in node.Children)
2353  {
2354  c.Color = c.HoverColor * nodeColorMultiplier;
2355  c.HoverColor = c.Color;
2356  c.PressedColor = c.Color;
2357  c.SelectedColor = c.Color;
2358  SetCharacterTooltip(c, characterContext);
2359  }
2360  node.OnClicked = null;
2361  node.OnSecondaryClicked = null;
2362  node.CanBeFocused = false;
2363  centerNode = node;
2364  }
2365 
2366  private void SetReturnNode(GUIButton node, Point offset)
2367  {
2368  node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration);
2369  node.RectTransform.ScaleOverTime(returnNodeSize, CommandNodeAnimDuration);
2370  foreach (GUIComponent c in node.Children)
2371  {
2372  c.HoverColor = c.Color * (1 / nodeColorMultiplier);
2373  c.PressedColor = c.HoverColor;
2374  c.SelectedColor = c.HoverColor;
2375  c.ToolTip = TextManager.Get("commandui.return");
2376  }
2377  node.OnClicked = NavigateBackward;
2378  node.OnSecondaryClicked = null;
2379  node.CanBeFocused = true;
2380  returnNode = node;
2381  }
2382 
2383  private bool CreateNodes(object userData)
2384  {
2385  if (userData == null)
2386  {
2387  if (isContextual)
2388  {
2389  CreateContextualOrderNodes();
2390  }
2391  else
2392  {
2393  CreateShortcutNodes();
2394  CreateOrderCategoryNodes();
2395  }
2396  }
2397  else if (userData is OrderCategory category)
2398  {
2399  CreateOrderNodes(category);
2400  }
2401  else if (userData is Order nodeOrder)
2402  {
2403  Submarine submarine = GetTargetSubmarine();
2404  List<Item> matchingItems = null;
2405  if (itemContext == null && nodeOrder.MustSetTarget)
2406  {
2407  matchingItems = nodeOrder.GetMatchingItems(submarine, true, interactableFor: characterContext ?? Character.Controlled);
2408  }
2409  //more than one target item -> create a minimap-like selection with a pic of the sub
2410  if (itemContext == null && nodeOrder.TargetEntity is not Item && matchingItems != null && matchingItems.Count > 1)
2411  {
2412  CreateMinimapNodes(nodeOrder, submarine, matchingItems);
2413  }
2414  //only one target (or an order with no particular targets), just show options
2415  else
2416  {
2417  CreateOrderOptionNodes(nodeOrder, itemContext ?? nodeOrder.TargetEntity as Item ?? matchingItems?.FirstOrDefault());
2418  }
2419  }
2420  else if (userData is MinimapNodeData {Order: { } minimapOrder} && minimapOrder.Prefab.HasOptions)
2421  {
2422  CreateOrderOptionNodes(minimapOrder, minimapOrder.TargetEntity as Item);
2423  }
2424  else
2425  {
2426  DebugConsole.ThrowError($"Unexpected node user data of type {userData.GetType()} when creating command interface nodes");
2427  return false;
2428  }
2429  return true;
2430  }
2431 
2432  private void RemoveOptionNodes()
2433  {
2434  if (commandFrame != null)
2435  {
2436  optionNodes.ForEach(node => commandFrame.RemoveChild(node.Button));
2437  shortcutNodes.ForEach(node => commandFrame.RemoveChild(node));
2438  commandFrame.RemoveChild(expandNode);
2439  }
2440  optionNodes.Clear();
2441  shortcutNodes.Clear();
2442  expandNode = null;
2443  expandNodeHotkey = Keys.None;
2444  RemoveExtraOptionNodes();
2445  }
2446 
2447  private void RemoveExtraOptionNodes()
2448  {
2449  if (commandFrame != null)
2450  {
2451  extraOptionNodes.ForEach(node => commandFrame.RemoveChild(node));
2452  }
2453  extraOptionNodes.Clear();
2454  }
2455 
2456  private void CreateOrderCategoryNodes()
2457  {
2458  // TODO: Calculate firstAngle parameter based on category count
2459  var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, availableCategories.Count, MathHelper.ToRadians(225));
2460  var offsetIndex = 0;
2461  availableCategories.ForEach(oc => CreateOrderCategoryNode(oc, offsets[offsetIndex++].ToPoint(), offsetIndex));
2462  }
2463 
2464  private void CreateOrderCategoryNode(OrderCategory category, Point offset, int hotkey)
2465  {
2466  var node = new GUIButton(
2467  new RectTransform(nodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null)
2468  {
2469  UserData = category,
2470  OnClicked = NavigateForward
2471  };
2472 
2473  node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration);
2474  var icon = OrderCategoryIcon.OrderCategoryIcons.FirstOrDefault(ic => ic.Category == category);
2475  if (icon is not null)
2476  {
2477  var tooltip = TextManager.Get($"ordercategorytitle.{category}");
2478  var categoryDescription = TextManager.Get($"ordercategorydescription.{category}");
2479  if (!categoryDescription.IsNullOrWhiteSpace()) { tooltip += "\n" + categoryDescription; }
2480  CreateNodeIcon(Vector2.One, node.RectTransform, icon.Sprite, icon.Color, tooltip: tooltip);
2481  }
2482  CreateHotkeyIcon(node.RectTransform, hotkey % 10);
2483  optionNodes.Add(new OptionNode(node, Keys.D0 + hotkey % 10));
2484  }
2485 
2489  private void CreateShortcutNodes()
2490  {
2491  if (!(GetTargetSubmarine() is { } sub)) { return; }
2492  shortcutNodes.Clear();
2493  var subItems = sub.GetItems(false);
2494  if (CanFitMoreNodes() && subItems.Find(i => i.HasTag(Tags.Reactor) && i.IsPlayerTeamInteractable)?.GetComponent<Reactor>() is Reactor reactor)
2495  {
2496  float reactorOutput = -reactor.CurrPowerConsumption;
2497  // If player is not an engineer AND the reactor is not powered up AND nobody is using the reactor
2498  // --> Create shortcut node for "Operate Reactor" order's "Power Up" option
2499  if (ShouldDelegateOrder("operatereactor") && reactorOutput < float.Epsilon && characters.None(c => c.SelectedItem == reactor.Item))
2500  {
2501  var orderPrefab = OrderPrefab.Prefabs["operatereactor"];
2502  var order = new Order(orderPrefab, orderPrefab.Options[0], reactor.Item, reactor);
2503  if (IsNonDuplicateOrder(order))
2504  {
2505  AddOrderNode(order);
2506  }
2507  }
2508  }
2509  // TODO: Reconsider the conditions as bot captain can have the nav term selected without operating it
2510  // If player is not a captain AND nobody is using the nav terminal AND the nav terminal is powered up
2511  // --> Create shortcut node for Steer order
2512  if (CanFitMoreNodes() && ShouldDelegateOrder("steer") && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["steer"]) &&
2513  subItems.Find(i => i.HasTag(Tags.NavTerminal) && i.IsPlayerTeamInteractable) is Item nav && characters.None(c => c.SelectedItem == nav) &&
2514  nav.GetComponent<Steering>() is Steering steering && steering.Voltage > steering.MinVoltage)
2515  {
2516  var order = new Order(OrderPrefab.Prefabs["steer"], steering.Item, steering);
2517  AddOrderNode(order);
2518  }
2519  // If player is not a security officer AND invaders are reported
2520  // --> Create shorcut node for Fight Intruders order
2521  if (CanFitMoreNodes() && ShouldDelegateOrder("fightintruders") &&
2522  ActiveOrders.Any(o => o.Order.Identifier == "reportintruders") &&
2523  IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fightintruders"]))
2524  {
2525  AddOrderNodeWithIdentifier("fightintruders");
2526  }
2527  // If player is not a mechanic AND a breach has been reported
2528  // --> Create shorcut node for Fix Leaks order
2529  if (CanFitMoreNodes() && ShouldDelegateOrder("fixleaks") &&
2530  IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["fixleaks"]) &&
2531  ActiveOrders.Any(o => o.Order.Identifier == "reportbreach"))
2532  {
2533  AddOrderNodeWithIdentifier("fixleaks");
2534  }
2535  // --> Create shortcut nodes for the Repair orders
2536  if (CanFitMoreNodes() && ActiveOrders.Any(o => o.Order.Identifier == "reportbrokendevices"))
2537  {
2538  var reportBrokenDevices = OrderPrefab.Prefabs["reportbrokendevices"];
2539  // TODO: Doesn't work for player issued reports, because they don't have a target.
2540  bool useSpecificRepairOrder = false;
2541  if (CanFitMoreNodes() && ShouldDelegateOrder("repairelectrical") &&
2542  ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "electrical")))
2543  {
2544  if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairelectrical"]))
2545  {
2546  AddOrderNodeWithIdentifier("repairelectrical");
2547  }
2548  useSpecificRepairOrder = true;
2549  }
2550  if (CanFitMoreNodes() && ShouldDelegateOrder("repairmechanical") &&
2551  ActiveOrders.Any(o => o.Order.Prefab == reportBrokenDevices && o.Order.TargetItemComponent is Repairable r && r.RequiredSkills.Any(s => s.Identifier == "mechanical")))
2552  {
2553  if (IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["repairmechanical"]))
2554  {
2555  AddOrderNodeWithIdentifier("repairmechanical");
2556  }
2557  useSpecificRepairOrder = true;
2558  }
2559  if (!useSpecificRepairOrder && CanFitMoreNodes() && ShouldDelegateOrder("repairsystems") && OrderPrefab.Prefabs["repairsystems"] is OrderPrefab repairOrder && IsNonDuplicateOrderPrefab(repairOrder))
2560  {
2561  AddOrderNodeWithIdentifier("repairsystems");
2562  }
2563  }
2564  // If fire is reported
2565  // --> Create shortcut node for Extinguish Fires order
2566  if (CanFitMoreNodes() && IsNonDuplicateOrderPrefab(OrderPrefab.Prefabs["extinguishfires"]) &&
2567  ActiveOrders.Any(o => o.Order.Identifier == "reportfire"))
2568  {
2569  AddOrderNodeWithIdentifier("extinguishfires");
2570  }
2571  if (CanFitMoreNodes() && characterContext?.Info?.Job?.Prefab?.AppropriateOrders != null)
2572  {
2573  foreach (Identifier orderIdentifier in characterContext.Info.Job.Prefab.AppropriateOrders)
2574  {
2575  if (OrderPrefab.Prefabs[orderIdentifier] is OrderPrefab orderPrefab && IsNonDuplicateOrderPrefab(orderPrefab) &&
2576  shortcutNodes.None(n => n.UserData is Order order && order.Identifier == orderIdentifier) &&
2577  !orderPrefab.IsReport && orderPrefab.Category != null)
2578  {
2579  if (!orderPrefab.MustSetTarget || orderPrefab.GetMatchingItems(sub, true, interactableFor: characterContext ?? Character.Controlled).Any())
2580  {
2581  var order = orderPrefab.CreateInstance(OrderPrefab.OrderTargetType.Entity);
2582  AddOrderNode(order);
2583  }
2584  if (!CanFitMoreNodes()) { break; }
2585  }
2586  }
2587  }
2588  if (CanFitMoreNodes() && characterContext != null && !characterContext.IsDismissed)
2589  {
2590  var order = OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity);
2591  AddOrderNode(order);
2592  }
2593  shortcutNodes.RemoveAll(n => n.UserData is Order o && !IsOrderAvailable(o));
2594  if (shortcutNodes.Count < 1) { return; }
2595  shortcutCenterNode = new GUIFrame(new RectTransform(shortcutCenterNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center), style: null)
2596  {
2597  CanBeFocused = false
2598  };
2599  CreateNodeIcon(shortcutCenterNode.RectTransform, "CommandShortcutNode");
2600  foreach (GUIComponent c in shortcutCenterNode.Children)
2601  {
2602  c.HoverColor = c.Color;
2603  c.PressedColor = c.Color;
2604  c.SelectedColor = c.Color;
2605  }
2606  shortcutCenterNode.RectTransform.MoveOverTime(shorcutCenterNodeOffset, CommandNodeAnimDuration);
2607  int nodeCountForCalculations = shortcutNodes.Count * 2 + 2;
2608  Vector2[] offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 0.75f * nodeDistance, nodeCountForCalculations);
2609  int firstOffsetIndex = nodeCountForCalculations / 2 - 1;
2610  for (int i = 0; i < shortcutNodes.Count; i++)
2611  {
2612  shortcutNodes[i].RectTransform.Parent = commandFrame.RectTransform;
2613  shortcutNodes[i].RectTransform.MoveOverTime(shorcutCenterNodeOffset + offsets[firstOffsetIndex - i].ToPoint(), CommandNodeAnimDuration);
2614  }
2615 
2616  bool CanFitMoreNodes()
2617  {
2618  return shortcutNodes.Count < maxShortcutNodeCount;
2619  }
2620  static bool ShouldDelegateOrder(string orderIdentifier) => ShouldDelegateOrderId(orderIdentifier.ToIdentifier());
2621  static bool ShouldDelegateOrderId(Identifier orderIdentifier)
2622  {
2623  return Character.Controlled is not Character c || !(c?.Info?.Job != null && c.Info.Job.Prefab.AppropriateOrders.Contains(orderIdentifier));
2624  }
2625  bool IsNonDuplicateOrder(Order order) => IsNonDuplicateOrderPrefab(order.Prefab, order.Option);
2626  bool IsNonDuplicateOrderPrefab(OrderPrefab orderPrefab, Identifier option = default)
2627  {
2628  return characterContext == null || (option.IsEmpty ?
2629  characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier) :
2630  characterContext.CurrentOrders.None(oi => oi?.Identifier == orderPrefab?.Identifier && oi.Option == option));
2631  }
2632  void AddOrderNodeWithIdentifier(string identifier)
2633  {
2634  var order = OrderPrefab.Prefabs[identifier].CreateInstance(OrderPrefab.OrderTargetType.Entity);
2635  AddOrderNode(order);
2636  }
2637  void AddOrderNode(Order order)
2638  {
2639  var node = order.Option.IsEmpty ?
2640  CreateOrderNode(shortcutNodeSize, null, Point.Zero, order, -1) :
2641  CreateOrderOptionNode(shortcutNodeSize, null, Point.Zero, order, -1);
2642  shortcutNodes.Add(node);
2643  }
2644  }
2645 
2646  private void CreateOrderNodes(OrderCategory orderCategory)
2647  {
2648  var orderPrefabs = OrderPrefab.Prefabs.Where(o => o.Category == orderCategory && !o.IsReport && IsOrderAvailable(o)).OrderBy(o => o.Identifier).ToArray();
2649  Order order;
2650  bool disableNode;
2651  var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance,
2652  GetCircumferencePointCount(orderPrefabs.Length), GetFirstNodeAngle(orderPrefabs.Length));
2653  for (int i = 0; i < orderPrefabs.Length; i++)
2654  {
2655  order = orderPrefabs[i].CreateInstance(OrderPrefab.OrderTargetType.Entity);
2656  disableNode = !CanCharacterBeHeard() ||
2657  (order.MustSetTarget && (order.ItemComponentType != null || order.GetTargetItems().Any() || order.RequireItems.Any()) &&
2658  order.GetMatchingItems(true, interactableFor: characterContext ?? Character.Controlled).None());
2659  optionNodes.Add(new OptionNode(
2660  CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, (i + 1) % 10, disableNode: disableNode, checkIfOrderCanBeHeard: false),
2661  !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None));
2662  }
2663  }
2664 
2671  private void CreateContextualOrderNodes()
2672  {
2673  if (contextualOrders.None())
2674  {
2675  // Check if targeting an item or a hull
2676  if (itemContext != null && itemContext.IsPlayerTeamInteractable)
2677  {
2678  ItemComponent targetComponent;
2679  foreach (OrderPrefab p in OrderPrefab.Prefabs)
2680  {
2681  targetComponent = null;
2682  if (p.UseController && itemContext.Components.None(c => c is Controller)) { continue; }
2683  if (p.HasOptionSpecificTargetItems)
2684  {
2685  foreach (Identifier option in p.Options)
2686  {
2687  if (p.TargetItemsMatchItem(itemContext, option))
2688  {
2689  contextualOrders.Add(new Order(p, option, itemContext, targetComponent));
2690  }
2691  }
2692  }
2693  else if (p.TargetItemsMatchItem(itemContext) || p.TryGetTargetItemComponent(itemContext, out targetComponent))
2694  {
2695  contextualOrders.Add(p.HasOptions ?
2696  p.CreateInstance(OrderPrefab.OrderTargetType.Entity) :
2697  new Order(p, itemContext, targetComponent));
2698  }
2699  }
2700 
2701  // If targeting a periscope connected to a turret, show the 'operateweapons' order
2702  var operateWeaponsPrefab = OrderPrefab.Prefabs["operateweapons"];
2703  if (contextualOrders.None(o => o.Identifier == "operateweapons") && itemContext.Components.Any(c => c is Controller))
2704  {
2705  var turret = itemContext.GetConnectedComponents<Turret>().FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item)) ??
2706  itemContext.GetConnectedComponents<Turret>(recursive: true).FirstOrDefault(c => operateWeaponsPrefab.TargetItemsMatchItem(c.Item));
2707  if (turret != null)
2708  {
2709  contextualOrders.Add(new Order(operateWeaponsPrefab, turret.Item, turret));
2710  }
2711  }
2712  // If targeting a repairable item with condition below the repair threshold, show the 'repairsystems' order
2713  if (contextualOrders.None(order => order.Identifier == "repairsystems") && itemContext.Repairables.Any(r => r.IsBelowRepairThreshold))
2714  {
2715  if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("electrical"))))
2716  {
2717  contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairelectrical"], itemContext, targetItem: null));
2718  }
2719  else if (itemContext.Repairables.Any(r => r != null && r.RequiredSkills.Any(s => s != null && s.Identifier.Equals("mechanical"))))
2720  {
2721  contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairmechanical"], itemContext, targetItem: null));
2722  }
2723  else
2724  {
2725  contextualOrders.Add(new Order(OrderPrefab.Prefabs["repairsystems"], itemContext, targetItem: null));
2726  }
2727  }
2728  // Remove the 'pumpwater' order if the target pump is auto-controlled (as it will immediately overwrite the work done by the bot)
2729  if (contextualOrders.FirstOrDefault(order => order.Identifier.Equals("pumpwater")) is Order pumpOrder &&
2730  itemContext.Components.FirstOrDefault(c => c.GetType() == pumpOrder.ItemComponentType) is Pump pump && pump.IsAutoControlled)
2731  {
2732  contextualOrders.Remove(pumpOrder);
2733  }
2734  if (contextualOrders.None(info => info.Identifier.Equals("cleanupitems")))
2735  {
2736  if (AIObjectiveCleanupItems.IsValidTarget(itemContext, Character.Controlled, checkInventory: false) || AIObjectiveCleanupItems.IsValidContainer(itemContext, Character.Controlled))
2737  {
2738  contextualOrders.Add(new Order(OrderPrefab.Prefabs["cleanupitems"], itemContext, targetItem: null));
2739  }
2740  }
2741  AddIgnoreOrder(itemContext);
2742  }
2743  else if (hullContext != null)
2744  {
2745  contextualOrders.Add(new Order(OrderPrefab.Prefabs["fixleaks"], hullContext, targetItem: null));
2746  if (wallContext != null)
2747  {
2748  AddIgnoreOrder(wallContext);
2749  }
2750  }
2751  void AddIgnoreOrder(IIgnorable target)
2752  {
2753  var orderIdentifier = Tags.IgnoreThis;
2754  if (!target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier))
2755  {
2756  AddOrder();
2757  }
2758  else
2759  {
2760  orderIdentifier = Tags.UnignoreThis;
2761  if (target.OrderedToBeIgnored && contextualOrders.None(order => order.Identifier == orderIdentifier))
2762  {
2763  AddOrder();
2764  }
2765  }
2766 
2767  void AddOrder()
2768  {
2769  if (target is WallSection ws)
2770  {
2771  contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], ws.Wall, ws.Wall.Sections.IndexOf(ws), orderGiver: Character.Controlled));
2772  }
2773  else
2774  {
2775  contextualOrders.Add(new Order(OrderPrefab.Prefabs[orderIdentifier], target as Entity, null, Character.Controlled));
2776  }
2777  }
2778  }
2779  if (contextualOrders.None(order => order.Identifier.Equals("wait")))
2780  {
2781  Vector2 position = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
2782  Hull hull = Hull.FindHull(position, guess: Character.Controlled?.CurrentHull);
2783  contextualOrders.Add(new Order(OrderPrefab.Prefabs["wait"], new OrderTarget(position, hull)));
2784  }
2785  if (contextualOrders.None(order => order.Category != OrderCategory.Movement) && characters.Any(c => c != Character.Controlled))
2786  {
2787  if (contextualOrders.None(order => order.Identifier.Equals("follow")))
2788  {
2789  contextualOrders.Add(OrderPrefab.Prefabs["follow"].CreateInstance(OrderPrefab.OrderTargetType.Entity));
2790  }
2791  }
2792  // Show 'dismiss' order only when there are crew members with active orders
2793  if (contextualOrders.None(order => order.IsDismissal) && characters.Any(c => !c.IsDismissed))
2794  {
2795  contextualOrders.Add(OrderPrefab.Dismissal.CreateInstance(OrderPrefab.OrderTargetType.Entity));
2796  }
2797  }
2798  contextualOrders.RemoveAll(o => !IsOrderAvailable(o));
2799  var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count));
2800  bool disableNode = !CanCharacterBeHeard();
2801  for (int i = 0; i < contextualOrders.Count; i++)
2802  {
2803  var order = contextualOrders[i];
2804  int hotkey = (i + 1) % 10;
2805  var component = order.Option.IsEmpty ?
2806  CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) :
2807  CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey);
2808  optionNodes.Add(new OptionNode(component, !disableNode ? Keys.D0 + (i + 1) % 10 : Keys.None));
2809  }
2810  }
2811 
2813  private GUIButton CreateOrderNode(Point size, RectTransform parent, Point offset, Order order, int hotkey, bool disableNode = false, bool checkIfOrderCanBeHeard = true)
2814  {
2815  var node = new GUIButton(
2816  new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null)
2817  {
2818  UserData = order
2819  };
2820 
2821  node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration);
2822 
2823  if (checkIfOrderCanBeHeard && !disableNode)
2824  {
2825  disableNode = !CanCharacterBeHeard();
2826  }
2827 
2828  bool mustSetOptionOrTarget = order.Prefab.HasOptions;
2829  Item orderTargetEntity = null;
2830 
2831  // If the order doesn't have options, but must set a target,
2832  // we have to check if there's only one possible target available
2833  // so we know to directly target that with the order
2834  if (!mustSetOptionOrTarget && order.MustSetTarget && itemContext == null)
2835  {
2836  var matchingItems = order.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: characterContext ?? Character.Controlled);
2837  if (matchingItems.Count > 1)
2838  {
2839  mustSetOptionOrTarget = true;
2840  }
2841  else
2842  {
2843  orderTargetEntity = matchingItems.FirstOrDefault();
2844  }
2845  }
2846 
2847  node.OnClicked = (button, userData) =>
2848  {
2849  if (disableNode || !CanIssueOrders) { return false; }
2850  var o = userData as Order;
2851  if (mustSetOptionOrTarget)
2852  {
2853  NavigateForward(button, userData);
2854  }
2855  else if (o.MustManuallyAssign && characterContext == null)
2856  {
2857  CreateAssignmentNodes(node);
2858  }
2859  else
2860  {
2861  if (orderTargetEntity != null)
2862  {
2863  o = new Order(o.Prefab, orderTargetEntity, orderTargetEntity.Components.FirstOrDefault(ic => ic.GetType() == order.ItemComponentType), orderGiver: order.OrderGiver);
2864  }
2865  var character = !o.TargetAllCharacters ? characterContext ?? GetCharacterForQuickAssignment(o) : null;
2866  int priority = GetManualOrderPriority(character, o);
2867  SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled));
2868  DisableCommandUI();
2869  }
2870  return true;
2871  };
2872 
2873  if (CanOpenManualAssignment(node))
2874  {
2875  node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button);
2876  }
2877  var showAssignmentTooltip = !mustSetOptionOrTarget && characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters;
2878  var orderName = GetOrderNameBasedOnContextuality(order);
2879  var icon = CreateNodeIcon(Vector2.One, node.RectTransform, order.SymbolSprite, order.Color,
2880  tooltip: !showAssignmentTooltip ? orderName : orderName +
2881  "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") +
2882  "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip"));
2883 
2884  if (disableNode)
2885  {
2886  node.CanBeFocused = icon.CanBeFocused = false;
2887  CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear"));
2888  }
2889  else if (hotkey >= 0)
2890  {
2891  CreateHotkeyIcon(node.RectTransform, hotkey);
2892  }
2893  return node;
2894  }
2895 
2896  public struct MinimapNodeData
2897  {
2898  public Order Order;
2899  }
2900 
2901  private void CreateMinimapNodes(Order order, Submarine submarine, List<Item> matchingItems)
2902  {
2903  // TODO: Further adjustments to frameSize calculations
2904  // I just divided the existing sizes by 2 to get it working quickly without it overlapping too much
2905  Point frameSize;
2906  Rectangle subBorders = submarine.GetDockedBorders();
2907  if (subBorders.Width > subBorders.Height)
2908  {
2909  frameSize.X = Math.Min(GameMain.GraphicsWidth / 2, GameMain.GraphicsWidth - 50) / 2;
2910  //height depends on the dimensions of the sub
2911  frameSize.Y = (int)(frameSize.X * (subBorders.Height / (float)subBorders.Width));
2912  }
2913  else
2914  {
2915  frameSize.Y = Math.Min((int)(GameMain.GraphicsHeight * 0.6f), GameMain.GraphicsHeight - 50) / 2;
2916  //width depends on the dimensions of the sub
2917  frameSize.X = (int)(frameSize.Y * (subBorders.Width / (float)subBorders.Height));
2918  }
2919 
2920  // TODO: Use the old targetFrame if possible
2921  targetFrame = new GUIFrame(
2922  new RectTransform(frameSize, parent: commandFrame.RectTransform, anchor: Anchor.Center)
2923  {
2924  AbsoluteOffset = new Point(0, -150),
2925  Pivot = Pivot.BottomCenter
2926  },
2927  style: "InnerFrame");
2928 
2929  submarine.CreateMiniMap(targetFrame, pointsOfInterest: matchingItems);
2930 
2931  new GUICustomComponent(new RectTransform(Vector2.One, targetFrame.RectTransform), onDraw: DrawMiniMapOverlay)
2932  {
2933  CanBeFocused = false,
2934  UserData = submarine
2935  };
2936 
2937  List<GUIComponent> optionElements = new List<GUIComponent>();
2938  foreach (Item item in matchingItems)
2939  {
2940  var itemTargetFrame = targetFrame.Children.First().FindChild(item);
2941  if (itemTargetFrame == null) { continue; }
2942 
2943  var anchor = Anchor.TopLeft;
2944  if (itemTargetFrame.RectTransform.RelativeOffset.X < 0.5f)
2945  {
2946  if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f)
2947  {
2948  anchor = Anchor.BottomRight;
2949  }
2950  else
2951  {
2952  anchor = Anchor.TopRight;
2953  }
2954  }
2955  else if (itemTargetFrame.RectTransform.RelativeOffset.Y < 0.5f)
2956  {
2957  anchor = Anchor.BottomLeft;
2958  }
2959 
2960  var userData = new MinimapNodeData
2961  {
2962  Order = item == null ? order : order.WithItemComponent(item, order.GetTargetItemComponent(item))
2963  };
2964  var optionElement = new GUIButton(
2965  new RectTransform(
2966  new Point((int)(50 * GUI.Scale)),
2967  parent: itemTargetFrame.RectTransform,
2968  anchor: anchor),
2969  style: null)
2970  {
2971  UserData = userData,
2972  Font = GUIStyle.SmallFont,
2973  OnClicked = (button, obj) =>
2974  {
2975  if (!CanIssueOrders) { return false; }
2976  var o = (MinimapNodeData)obj;
2977  if (o.Order.Prefab.HasOptions)
2978  {
2979  NavigateForward(button, o);
2980  }
2981  else if (o.Order.MustManuallyAssign && characterContext == null)
2982  {
2983  CreateAssignmentNodes(button);
2984  }
2985  else
2986  {
2987  var character = characterContext ?? GetCharacterForQuickAssignment(o.Order);
2988  int priority = GetManualOrderPriority(character, o.Order);
2989  SetCharacterOrder(
2990  character,
2991  o.Order
2992  .WithManualPriority(priority)
2993  .WithOrderGiver(Character.Controlled));
2994  DisableCommandUI();
2995  }
2996  return true;
2997  }
2998  };
2999  if (CanOpenManualAssignmentMinimapOrder(optionElement))
3000  {
3001  optionElement.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button);
3002  }
3003  var colorMultiplier = characters.Any(c => c.CurrentOrders.Any(o => o != null &&
3004  o.Identifier == userData.Order.Identifier &&
3005  o.TargetEntity == userData.Order.TargetEntity)) ? 0.5f : 1f;
3006  CreateNodeIcon(Vector2.One, optionElement.RectTransform, item.Prefab.MinimapIcon ?? order.SymbolSprite, order.Color * colorMultiplier, tooltip: item.Name);
3007  optionNodes.Add(new OptionNode(optionElement, Keys.None));
3008  optionElements.Add(optionElement);
3009  }
3010 
3011  Rectangle clampArea = new Rectangle(10, 10, GameMain.GraphicsWidth - 20, GameMain.GraphicsHeight - 20);
3012  Rectangle disallowedArea = targetFrame.GetChild<GUIFrame>().Rect;
3013  Point originalSize = disallowedArea.Size;
3014  disallowedArea.Size = disallowedArea.MultiplySize(0.9f);
3015  disallowedArea.X += (originalSize.X - disallowedArea.Size.X) / 2;
3016  disallowedArea.Y += (originalSize.Y - disallowedArea.Size.Y) / 2;
3017  GUI.PreventElementOverlap(optionElements, new List<Rectangle>() { disallowedArea }, clampArea);
3018  nodeConnectors.RectTransform.Parent = targetFrame.RectTransform;
3019  nodeConnectors.RectTransform.SetAsFirstChild();
3020 
3021  var shadow = new GUIFrame(
3022  new RectTransform(targetFrame.Rect.Size + new Point((int)(200 * GUI.Scale)), targetFrame.RectTransform, anchor: Anchor.Center),
3023  style: "OuterGlow",
3024  color: matchingItems.Count > 1 ? Color.Black * 0.9f : Color.Black * 0.7f);
3025  shadow.SetAsFirstChild();
3026  }
3027 
3028  private void CreateOrderOptionNodes(Order order, Item targetItem)
3029  {
3030  if (itemContext != null)
3031  {
3032  targetItem = !order.UseController ? itemContext :
3033  itemContext.GetConnectedComponents<Turret>().FirstOrDefault()?.Item ?? itemContext.GetConnectedComponents<Turret>(recursive: true).FirstOrDefault()?.Item;
3034  }
3035  var o = targetItem == null ? order : order.WithItemComponent(targetItem, order.GetTargetItemComponent(targetItem));
3036  var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance,
3037  GetCircumferencePointCount(order.Options.Length),
3038  GetFirstNodeAngle(order.Options.Length));
3039  var offsetIndex = 0;
3040  for (int i = 0; i < order.Options.Length; i++)
3041  {
3042  optionNodes.Add(new OptionNode(
3043  CreateOrderOptionNode(nodeSize, commandFrame.RectTransform, offsets[offsetIndex++].ToPoint(), o.WithOption(order.Options[i]), (i + 1) % 10),
3044  Keys.D0 + (i + 1) % 10));
3045  }
3046  }
3047 
3048  private GUIButton CreateOrderOptionNode(Point size, RectTransform parent, Point offset, Order order, int hotkey)
3049  {
3050  var node = new GUIButton(new RectTransform(size, parent: parent, anchor: Anchor.Center), style: null)
3051  {
3052  UserData = order,
3053  OnClicked = (button, userData) =>
3054  {
3055  if (!CanIssueOrders) { return false; }
3056  var o = userData as Order;
3057  if (o.MustManuallyAssign && characterContext == null)
3058  {
3059  CreateAssignmentNodes(button);
3060  }
3061  else
3062  {
3063  var character = characterContext ?? GetCharacterForQuickAssignment(o);
3064  int priority = GetManualOrderPriority(character, o);
3065  SetCharacterOrder(character, o.WithManualPriority(priority).WithOrderGiver(Character.Controlled));
3066  DisableCommandUI();
3067  }
3068  return true;
3069  }
3070  };
3071  if (CanOpenManualAssignment(node))
3072  {
3073  node.OnSecondaryClicked = (button, _) => CreateAssignmentNodes(button);
3074  }
3075  node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration);
3076 
3077  GUIImage icon = null;
3078  if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite))
3079  {
3080  var optionName = order.Prefab.GetOptionName(order.Option);
3081  var showAssignmentTooltip = characterContext == null && !order.MustManuallyAssign && !order.TargetAllCharacters;
3082  icon = CreateNodeIcon(Vector2.One, node.RectTransform, sprite, order.Color,
3083  tooltip: characterContext != null ? optionName : optionName +
3084  "\n" + PlayerInput.PrimaryMouseLabel + ": " + TextManager.Get("commandui.quickassigntooltip") +
3085  "\n" + PlayerInput.SecondaryMouseLabel + ": " + TextManager.Get("commandui.manualassigntooltip"));
3086  }
3087  if (!CanCharacterBeHeard())
3088  {
3089  node.CanBeFocused = false;
3090  if (icon != null) { icon.CanBeFocused = false; }
3091  CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get(characterContext == null ? "nocharactercanhear" : "thischaractercanthear"));
3092  }
3093  else if (hotkey >= 0)
3094  {
3095  CreateHotkeyIcon(node.RectTransform, hotkey);
3096  }
3097  return node;
3098  }
3099 
3100  private bool CreateAssignmentNodes(GUIComponent node)
3101  {
3102  if (centerNode == null)
3103  {
3104  DisableCommandUI();
3105  return false;
3106  }
3107 
3108  var order = node.UserData is MinimapNodeData minimapNodeData ? minimapNodeData.Order : node.UserData as Order;
3109  var characters = GetCharactersForManualAssignment(order);
3110  if (characters.None()) { return false; }
3111 
3112  if (!(optionNodes.Find(n => n.Button == node) is OptionNode optionNode) || !optionNodes.Remove(optionNode))
3113  {
3114  shortcutNodes.Remove(node);
3115  };
3116  RemoveOptionNodes();
3117 
3118  if (returnNode != null)
3119  {
3120  returnNode.Children.ForEach(child => child.Visible = false);
3121  returnNode.Visible = false;
3122  historyNodes.Push(returnNode);
3123  }
3124  SetReturnNode(centerNode, new Point(0, (int)(returnNodeDistanceModifier * nodeDistance)));
3125 
3126  if (targetFrame == null || !targetFrame.Visible)
3127  {
3128  SetCenterNode(node as GUIButton);
3129  }
3130  else
3131  {
3132  if (order.Option.IsEmpty)
3133  {
3134  SetCenterNode(node as GUIButton, resetAnchor: true);
3135  }
3136  else
3137  {
3138  var clickedOptionNode = new GUIButton(
3139  new RectTransform(centerNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center),
3140  style: null)
3141  {
3142  UserData = node.UserData
3143  };
3144  if (order.Prefab.OptionSprites.TryGetValue(order.Option, out Sprite sprite))
3145  {
3146  CreateNodeIcon(Vector2.One, clickedOptionNode.RectTransform, sprite, order.Color, tooltip: order.GetOptionName(order.Option)); //TODO: revise tooltip
3147  }
3148  SetCenterNode(clickedOptionNode);
3149  node = null;
3150  }
3151  HideMinimap();
3152  }
3153  if (shortcutCenterNode != null)
3154  {
3155  commandFrame.RemoveChild(shortcutCenterNode);
3156  shortcutCenterNode = null;
3157  }
3158 
3159  var characterCount = characters.Count;
3160  int hotkey = 1;
3161  Vector2[] offsets;
3162  var needToExpand = characterCount > 10;
3163  if (characterCount > 5)
3164  {
3165  // First ring
3166  var charactersOnFirstRing = needToExpand ? 5 : (int)Math.Floor(characterCount / 2f);
3167  offsets = GetAssignmentNodeOffsets(charactersOnFirstRing);
3168  for (int i = 0; i < charactersOnFirstRing; i++)
3169  {
3170  CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10);
3171  }
3172  // Second ring
3173  var charactersOnSecondRing = needToExpand ? 4 : characterCount - charactersOnFirstRing;
3174  offsets = GetAssignmentNodeOffsets(needToExpand ? 5 : charactersOnSecondRing, false);
3175  for (int i = 0; i < charactersOnSecondRing; i++)
3176  {
3177  CreateAssignmentNode(order, characters[charactersOnFirstRing + i], offsets[i].ToPoint(), hotkey++ % 10);
3178  }
3179  }
3180  else
3181  {
3182  offsets = GetAssignmentNodeOffsets(characterCount);
3183  for (int i = 0; i < characterCount; i++)
3184  {
3185  CreateAssignmentNode(order, characters[i], offsets[i].ToPoint(), hotkey++ % 10);
3186  }
3187  }
3188 
3189  if (!needToExpand)
3190  {
3191  hotkey = optionNodes.Count + 1;
3192  CreateHotkeyIcon(returnNode.RectTransform, hotkey % 10, true);
3193  returnNodeHotkey = Keys.D0 + hotkey % 10;
3194  expandNodeHotkey = Keys.None;
3195  return true;
3196  }
3197 
3198  extraOptionCharacters.Clear();
3199  // Sort expanded assignment nodes by characters' jobs and then by their names
3200  extraOptionCharacters.AddRange(characters.GetRange(hotkey - 1, characterCount - (hotkey - 1))
3201  .OrderBy(c => c?.Info?.Job?.Name).ThenBy(c => c?.Info?.DisplayName));
3202 
3203  expandNode = new GUIButton(
3204  new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center)
3205  {
3206  AbsoluteOffset = offsets.Last().ToPoint()
3207  },
3208  style: null)
3209  {
3210  UserData = order,
3211  OnClicked = ExpandAssignmentNodes
3212  };
3213  CreateNodeIcon(expandNode.RectTransform, "CommandExpandNode", order.Color, tooltip: TextManager.Get("commandui.expand"));
3214 
3215  hotkey = optionNodes.Count + 1;
3216  CreateHotkeyIcon(expandNode.RectTransform, hotkey % 10);
3217  expandNodeHotkey = Keys.D0 + hotkey % 10;
3218  CreateHotkeyIcon(returnNode.RectTransform, ++hotkey % 10, true);
3219  returnNodeHotkey = Keys.D0 + hotkey % 10;
3220  return true;
3221  }
3222 
3223  private Vector2[] GetAssignmentNodeOffsets(int characters, bool firstRing = true)
3224  {
3225  var nodeDistance = 1.8f * this.nodeDistance;
3226  var nodePositionsOnEachSide = characters % 2 > 0 ? 7 : 6;
3227  var nodeCountForCalculation = 2 * nodePositionsOnEachSide + 2;
3228  var offsets = MathUtils.GetPointsOnCircumference(firstRing ? new Vector2(0f, 0.5f * nodeDistance) : Vector2.Zero,
3229  nodeDistance, nodeCountForCalculation, MathHelper.ToRadians(180f + 360f / nodeCountForCalculation));
3230  var emptySpacesPerSide = (nodePositionsOnEachSide - characters) / 2;
3231  var offsetsInUse = new Vector2[nodePositionsOnEachSide - 2 * emptySpacesPerSide];
3232  for (int i = 0; i < offsetsInUse.Length; i++)
3233  {
3234  offsetsInUse[i] = offsets[i + emptySpacesPerSide];
3235  }
3236  return offsetsInUse;
3237  }
3238 
3239  private bool ExpandAssignmentNodes(GUIButton node, object userData)
3240  {
3241  node.OnClicked = (button, _) =>
3242  {
3243  RemoveExtraOptionNodes();
3244  button.OnClicked = ExpandAssignmentNodes;
3245  return true;
3246  };
3247 
3248  var availableNodePositions = 20;
3249  var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, 2.7f * this.nodeDistance, availableNodePositions,
3250  firstAngle: MathHelper.ToRadians(-90f - ((extraOptionCharacters.Count - 1) * 0.5f * (360f / availableNodePositions))));
3251  for (int i = 0; i < extraOptionCharacters.Count && i < availableNodePositions; i++)
3252  {
3253  CreateAssignmentNode(userData as Order, extraOptionCharacters[i], offsets[i].ToPoint(), -1, nameLabelScale: 1.15f);
3254  }
3255  return true;
3256  }
3257 
3258  private void CreateAssignmentNode(Order order, Character character, Point offset, int hotkey, float nameLabelScale = 1f)
3259  {
3260  // Button
3261  var node = new GUIButton(
3262  new RectTransform(assignmentNodeSize, parent: commandFrame.RectTransform, anchor: Anchor.Center),
3263  style: null)
3264  {
3265  UserData = character,
3266  OnClicked = (_, userData) =>
3267  {
3268  if (!CanIssueOrders) { return false; }
3269  var character = userData as Character;
3270  int priority = GetManualOrderPriority(character, order);
3271  SetCharacterOrder(character, order.WithManualPriority(priority).WithOrderGiver(Character.Controlled));
3272  DisableCommandUI();
3273  return true;
3274  }
3275  };
3276  node.RectTransform.MoveOverTime(offset, CommandNodeAnimDuration);
3277 
3278  var jobColor = character.Info?.Job?.Prefab?.UIColor ?? Color.White;
3279 
3280  // Order icon
3281  var topOrderInfo = character.GetCurrentOrderWithTopPriority();
3282  GUIImage orderIcon;
3283  if (topOrderInfo != null)
3284  {
3285  orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), topOrderInfo.SymbolSprite, scaleToFit: true);
3286  var tooltip = topOrderInfo.Name;
3287  if (topOrderInfo.Option != Identifier.Empty) { tooltip += " (" + topOrderInfo.GetOptionName(topOrderInfo.Option) + ")"; };
3288  orderIcon.ToolTip = tooltip;
3289  }
3290  else
3291  {
3292  orderIcon = new GUIImage(new RectTransform(new Vector2(1.2f), node.RectTransform, anchor: Anchor.Center), "CommandIdleNode", scaleToFit: true);
3293  }
3294  orderIcon.Color = jobColor * nodeColorMultiplier;
3295  orderIcon.HoverColor = jobColor;
3296  orderIcon.PressedColor = jobColor;
3297  orderIcon.SelectedColor = jobColor;
3298  orderIcon.UserData = "colorsource";
3299 
3300  // Name label
3301  var width = (int)(nameLabelScale * nodeSize.X);
3302  var font = GUIStyle.SmallFont;
3303  var nameLabel = new GUITextBlock(
3304  new RectTransform(new Point(width, 0), parent: node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.BottomCenter)
3305  {
3306  RelativeOffset = new Vector2(0f, -0.25f)
3307  },
3308  ToolBox.LimitString(character.Info?.DisplayName, font, width), textColor: jobColor * nodeColorMultiplier, font: font, textAlignment: Alignment.Center, style: null)
3309  {
3310  CanBeFocused = false,
3312  HoverTextColor = jobColor
3313  };
3314 
3315  if (character.Info?.Job?.Prefab?.IconSmall is Sprite smallJobIcon)
3316  {
3317  // Job icon
3318  new GUIImage(
3319  new RectTransform(new Vector2(0.4f), node.RectTransform, anchor: Anchor.TopCenter, pivot: Pivot.Center)
3320  {
3321  RelativeOffset = new Vector2(0.0f, -((orderIcon.RectTransform.RelativeSize.Y - 1) / 2))
3322  },
3323  smallJobIcon, scaleToFit: true)
3324  {
3325  CanBeFocused = false,
3326  Color = jobColor,
3327  HoverColor = jobColor
3328  };
3329  }
3330 
3331  bool canHear = character.CanHearCharacter(Character.Controlled);
3332 #if DEBUG
3333  if (Character.Controlled == null) { canHear = true; }
3334 #endif
3335 
3336  if (!canHear)
3337  {
3338  node.CanBeFocused = orderIcon.CanBeFocused = false;
3339  CreateBlockIcon(node.RectTransform, tooltip: TextManager.Get("thischaractercanthear"));
3340  }
3341  if (hotkey >= 0)
3342  {
3343  if (canHear) { CreateHotkeyIcon(node.RectTransform, hotkey); }
3344  optionNodes.Add(new OptionNode(node, canHear ? Keys.D0 + hotkey : Keys.None));
3345  }
3346  else
3347  {
3348  extraOptionNodes.Add(node);
3349  }
3350  }
3351 
3352  private GUIImage CreateNodeIcon(Vector2 relativeSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null)
3353  {
3354  // Icon
3355  return new GUIImage(
3356  new RectTransform(relativeSize, parent),
3357  sprite,
3358  scaleToFit: true)
3359  {
3360  Color = color * nodeColorMultiplier,
3361  HoverColor = color,
3362  PressedColor = color,
3363  SelectedColor = color,
3364  ToolTip = tooltip,
3365  UserData = "colorsource"
3366  };
3367  }
3368 
3372  private GUIImage CreateNodeIcon(Point absoluteSize, RectTransform parent, Sprite sprite, Color color, LocalizedString tooltip = null)
3373  {
3374  // Icon
3375  return new GUIImage(
3376  new RectTransform(absoluteSize, parent: parent) { IsFixedSize = true },
3377  sprite,
3378  scaleToFit: true)
3379  {
3380  Color = color * nodeColorMultiplier,
3381  HoverColor = color,
3382  PressedColor = color,
3383  SelectedColor = color,
3384  ToolTip = tooltip,
3385  UserData = "colorsource"
3386  };
3387  }
3388 
3389  private void CreateNodeIcon(RectTransform parent, string style, Color? color = null, LocalizedString tooltip = null)
3390  {
3391  // Icon
3392  var icon = new GUIImage(
3393  new RectTransform(Vector2.One, parent),
3394  style,
3395  scaleToFit: true)
3396  {
3397  ToolTip = tooltip,
3398  UserData = "colorsource"
3399  };
3400  if (color.HasValue)
3401  {
3402  icon.Color = color.Value * nodeColorMultiplier;
3403  icon.HoverColor = color.Value;
3404  }
3405  else
3406  {
3407  icon.Color = icon.HoverColor * nodeColorMultiplier;
3408  }
3409  }
3410 
3411  private void CreateHotkeyIcon(RectTransform parent, int hotkey, bool enlargeIcon = false)
3412  {
3413  var bg = new GUIImage(
3414  new RectTransform(new Vector2(enlargeIcon ? 0.4f : 0.25f), parent, anchor: Anchor.BottomCenter, pivot: Pivot.Center),
3415  "CommandHotkeyContainer",
3416  scaleToFit: true)
3417  {
3418  CanBeFocused = false,
3419  UserData = "hotkey"
3420  };
3421  new GUITextBlock(
3422  new RectTransform(Vector2.One, bg.RectTransform, anchor: Anchor.Center),
3423  hotkey.ToString(),
3424  textColor: Color.Black,
3425  textAlignment: Alignment.Center)
3426  {
3427  CanBeFocused = false
3428  };
3429  }
3430 
3431  private void CreateBlockIcon(RectTransform parent, LocalizedString tooltip = null)
3432  {
3433  var icon = new GUIImage(new RectTransform(new Vector2(0.9f), parent, anchor: Anchor.Center), cancelIcon, scaleToFit: true)
3434  {
3435  CanBeFocused = false,
3436  Color = GUIStyle.Red * nodeColorMultiplier,
3437  HoverColor = GUIStyle.Red
3438  };
3439  if (!tooltip.IsNullOrEmpty())
3440  {
3441  string color = XMLExtensions.ColorToString(GUIStyle.Red);
3442  tooltip = $"‖color:{color}‖{tooltip}‖color:end‖";
3443  icon.ToolTip = RichString.Rich(tooltip);
3444  icon.CanBeFocused = true;
3445  }
3446  }
3447 
3448  private int GetCircumferencePointCount(int nodes)
3449  {
3450  return nodes % 2 > 0 ? nodes : nodes + 1;
3451  }
3452 
3453  private float GetFirstNodeAngle(int nodeCount)
3454  {
3455  var bearing = 90.0f;
3456  if (returnNode != null)
3457  {
3458  bearing = GetBearing(
3459  centerNode.RectTransform.AnimTargetPos.ToVector2(),
3460  returnNode.RectTransform.AnimTargetPos.ToVector2());
3461  }
3462  else if (shortcutCenterNode != null)
3463  {
3464  bearing = GetBearing(
3465  centerNode.RectTransform.AnimTargetPos.ToVector2(),
3466  shorcutCenterNodeOffset.ToVector2());
3467  }
3468  return nodeCount % 2 > 0 ?
3469  MathHelper.ToRadians(bearing + 360.0f / nodeCount / 2) :
3470  MathHelper.ToRadians(bearing + 360.0f / (nodeCount + 1));
3471  }
3472 
3473  private float GetBearing(Vector2 startPoint, Vector2 endPoint, bool flipY = false, bool flipX = false)
3474  {
3475  var radians = Math.Atan2(
3476  !flipY ? endPoint.Y - startPoint.Y : startPoint.Y - endPoint.Y,
3477  !flipX ? endPoint.X - startPoint.X : startPoint.X - endPoint.X);
3478  var degrees = MathHelper.ToDegrees((float)radians);
3479  return (degrees < 0) ? (degrees + 360) : degrees;
3480  }
3481 
3482  private bool TryGetBreachedHullAtHoveredWall(out Hull breachedHull, out WallSection hoveredWall)
3483  {
3484  breachedHull = null;
3485  hoveredWall = null;
3486  // Based on the IsValidTarget() method of AIObjectiveFixLeaks class
3487  List<Gap> leaks = Gap.GapList.FindAll(g =>
3488  g != null && g.ConnectedWall != null && g.ConnectedDoor == null && g.Open > 0 && g.linkedTo.Any(l => l != null) &&
3489  g.Submarine != null && (Character.Controlled != null && g.Submarine.TeamID == Character.Controlled.TeamID && g.Submarine.Info.IsPlayer));
3490  if (leaks.None()) { return false; }
3491  Vector2 mouseWorldPosition = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
3492  foreach (Gap leak in leaks)
3493  {
3494  if (Submarine.RectContains(leak.ConnectedWall.WorldRect, mouseWorldPosition))
3495  {
3496  breachedHull = leak.FlowTargetHull;
3497  foreach (var section in leak.ConnectedWall.Sections)
3498  {
3499  if (Submarine.RectContains(section.WorldRect, mouseWorldPosition))
3500  {
3501  hoveredWall = section;
3502  break;
3503  }
3504 
3505  }
3506  return true;
3507  }
3508  }
3509  return false;
3510  }
3511 
3512  private Submarine GetTargetSubmarine()
3513  {
3514  var sub = Submarine.MainSub;
3515  if (Character.Controlled != null)
3516  {
3517  // Pick the second main sub when we have two teams (in combat mission)
3518  if (Character.Controlled.TeamID == CharacterTeamType.Team2 && Submarine.MainSubs.Length > 1)
3519  {
3520  sub = Submarine.MainSubs[1];
3521  }
3522  // Target current submarine (likely a shuttle) when undocked from the main sub
3523  if (Character.Controlled.Submarine is Submarine currentSub && currentSub != sub && currentSub.TeamID == Character.Controlled.TeamID && !currentSub.IsConnectedTo(sub))
3524  {
3525  sub = currentSub;
3526  }
3527  }
3528  return sub;
3529  }
3530 
3531  private void SetCharacterTooltip(GUIComponent component, Character character)
3532  {
3533  if (component == null) { return; }
3534  LocalizedString tooltip = character?.Info != null ? characterContext.Info.DisplayName : null;
3535  if (tooltip.IsNullOrWhiteSpace()) { component.ToolTip = tooltip; return; }
3536  if (character.Info?.Job != null && !characterContext.Info.Job.Name.IsNullOrWhiteSpace()) { tooltip += " (" + characterContext.Info.Job.Name + ")"; }
3537  component.ToolTip = tooltip;
3538  }
3539 
3540  private LocalizedString GetOrderNameBasedOnContextuality(Order order)
3541  {
3542  if (order == null) { return ""; }
3543  if (isContextual) { return order.ContextualName; }
3544  return order.Name;
3545  }
3546 
3547  private int GetManualOrderPriority(Character character, Order order)
3548  {
3549  return character?.Info?.GetManualOrderPriority(order) ?? CharacterInfo.HighestManualOrderPriority;
3550  }
3551 
3552  private bool IsOrderAvailable(Order order)
3553  => IsOrderAvailable(order.Prefab);
3554 
3555  private bool IsOrderAvailable(OrderPrefab order)
3556  {
3557  if (order == null) { return false; }
3558  switch (order.Identifier.Value.ToLowerInvariant())
3559  {
3560  case "assaultenemy":
3561  Character character = characterContext ?? Character.Controlled;
3562  if (character?.Submarine == null) { return false; }
3563  return character.Submarine.GetConnectedSubs().Any(s => s.TeamID != character.TeamID);
3564  default:
3565  return true;
3566  }
3567  }
3568 
3569  #region Crew Member Assignment Logic
3570  private bool CanOpenManualAssignmentMinimapOrder(GUIComponent node)
3571  {
3572  if (node == null || characterContext != null) { return false; }
3573  if (node.UserData is MinimapNodeData {Order: { } minimapOrder})
3574  {
3575  return !minimapOrder.TargetAllCharacters && (!minimapOrder.Prefab.HasOptions || !minimapOrder.Option.IsEmpty);
3576  }
3577  if (node.UserData is Order nodeOrder)
3578  {
3579  return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions &&
3580  (!nodeOrder.MustSetTarget || itemContext != null ||
3581  nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2);
3582  }
3583  return false;
3584  }
3585 
3586  private bool CanOpenManualAssignment(GUIComponent node)
3587  {
3588  if (node == null || characterContext != null) { return false; }
3589  if (node.UserData is Order nodeOrder)
3590  {
3591  return !nodeOrder.TargetAllCharacters &&
3592  (!nodeOrder.Prefab.HasOptions || !nodeOrder.Option.IsEmpty) &&
3593  (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2);
3594  }
3595  return false;
3596  }
3597 
3598  private Character GetCharacterForQuickAssignment(Order order)
3599  {
3600  return GetCharacterForQuickAssignment(order, Character.Controlled, characters);
3601  }
3602 
3603  private List<Character> GetCharactersForManualAssignment(Order order)
3604  {
3605 #if !DEBUG
3606  if (Character.Controlled == null) { return new List<Character>(); }
3607 #endif
3608  if (order.Identifier == dismissedOrderPrefab.Identifier)
3609  {
3610  return characters.Union(GetOrderableFriendlyNPCs()).Where(c => !c.IsDismissed).OrderBy(c => c.Info.DisplayName).ToList();
3611  }
3612  return GetCharactersSortedForOrder(order, characters, Character.Controlled, order.Identifier != "follow", extraCharacters: GetOrderableFriendlyNPCs()).ToList();
3613  }
3614 
3615  private IEnumerable<Character> GetOrderableFriendlyNPCs()
3616  {
3617  // TODO: change this so that we can get the data without having to rely on ui elements.
3618  return crewList.Content.Children.Where(c => c.UserData is Character character && character.TeamID == CharacterTeamType.FriendlyNPC).Select(c => (Character)c.UserData);
3619  }
3620 
3621  #endregion
3622 
3623  #endregion
3624 
3625  #region Reports
3626 
3630  public void UpdateReports()
3631  {
3632  bool canIssueOrders = false;
3634  {
3635  canIssueOrders =
3639  }
3640 
3641  if (canIssueOrders)
3642  {
3643  ReportButtonFrame.Visible = !Character.Controlled.ShouldLockHud();
3644  if (!ReportButtonFrame.Visible) { return; }
3645 
3646  var reportButtonParent = ChatBox ?? GameMain.Client?.ChatBox;
3647  if (reportButtonParent == null) { return; }
3648 
3649  ReportButtonFrame.RectTransform.AbsoluteOffset = new Point(reportButtonParent.GUIFrame.Rect.Right + (int)(10 * GUI.Scale), reportButtonParent.GUIFrame.Rect.Y);
3650 
3651  bool hasFires = Character.Controlled.CurrentHull.FireSources.Count > 0;
3652  ToggleReportButton("reportfire", hasFires);
3653 
3654  bool hasLeaks = Character.Controlled.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.Open > 0.0f);
3655  ToggleReportButton("reportbreach", hasLeaks);
3656 
3657  bool hasIntruders = Character.CharacterList.Any(c => c.CurrentHull == Character.Controlled.CurrentHull && AIObjectiveFightIntruders.IsValidTarget(c, Character.Controlled, false));
3658  ToggleReportButton("reportintruders", hasIntruders);
3659 
3660  foreach (GUIComponent reportButton in ReportButtonFrame.Children)
3661  {
3662  var highlight = reportButton.GetChildByUserData("highlighted");
3663  if (highlight.Visible)
3664  {
3665  highlight.RectTransform.LocalScale = new Vector2(1.25f + (float)Math.Sin(Timing.TotalTime * 5.0f) * 0.25f);
3666  }
3667  }
3668  }
3669  else
3670  {
3671  ReportButtonFrame.Visible = false;
3672  }
3673  }
3674 
3675  private void ToggleReportButton(string orderIdentifier, bool enabled)
3676  {
3677  ToggleReportButton(orderIdentifier.ToIdentifier(), enabled);
3678  }
3679 
3680  private void ToggleReportButton(Identifier orderIdentifier, bool enabled)
3681  {
3682  var reportButton = ReportButtonFrame.FindChild(c => c.UserData is OrderPrefab orderPrefab && orderPrefab.Identifier == orderIdentifier);
3683  if (reportButton != null)
3684  {
3685  reportButton.GetChildByUserData("highlighted").Visible = enabled;
3686  }
3687  }
3688 
3689  #endregion
3690 
3692  {
3693  crewList.ClearChildren();
3694  InitRound();
3695  }
3696 
3697  public void Reset()
3698  {
3699  characters.Clear();
3700  characterInfos.Clear();
3701  crewList.ClearChildren();
3702  }
3703 
3707  public XElement Save(XElement parentElement)
3708  {
3709  var element = new XElement("crew");
3710  for (int i = 0; i < characterInfos.Count; i++)
3711  {
3712  var ci = characterInfos[i];
3713  var infoElement = ci.Save(element);
3714  if (ci.InventoryData != null) { infoElement.Add(ci.InventoryData); }
3715  if (ci.HealthData != null) { infoElement.Add(ci.HealthData); }
3716  if (ci.OrderData != null) { infoElement.Add(ci.OrderData); }
3717  infoElement.Add(new XAttribute("crewlistindex", ci.CrewListIndex));
3718  if (ci.LastControlled) { infoElement.Add(new XAttribute("lastcontrolled", true)); }
3719  }
3720  parentElement?.Add(element);
3721  return element;
3722  }
3723 
3724  public static void ClientReadActiveOrders(IReadMessage inc)
3725  {
3726  ushort count = inc.ReadUInt16();
3727  if (count < 1) { return; }
3728  var activeOrders = new List<(Order, float?)>();
3729  for (ushort i = 0; i < count; i++)
3730  {
3731  var orderMessageInfo = OrderChatMessage.ReadOrder(inc);
3732  Character orderGiver = null;
3733  if (inc.ReadBoolean())
3734  {
3735  ushort orderGiverId = inc.ReadUInt16();
3736  orderGiver = orderGiverId != Entity.NullEntityID ? Entity.FindEntityByID(orderGiverId) as Character : null;
3737  }
3738  if (orderMessageInfo.OrderIdentifier == Identifier.Empty)
3739  {
3740  DebugConsole.ThrowError("Invalid active order - order identifier empty.");
3741  continue;
3742  }
3743  OrderPrefab orderPrefab = orderMessageInfo.OrderPrefab ?? OrderPrefab.Prefabs[orderMessageInfo.OrderIdentifier];
3744  Order order = orderMessageInfo.TargetType switch
3745  {
3746  Order.OrderTargetType.Entity =>
3747  new Order(orderPrefab, orderMessageInfo.TargetEntity, orderPrefab.GetTargetItemComponent(orderMessageInfo.TargetEntity as Item), orderGiver: orderGiver),
3748  Order.OrderTargetType.Position =>
3749  new Order(orderPrefab, orderMessageInfo.TargetPosition, orderGiver: orderGiver),
3750  Order.OrderTargetType.WallSection =>
3751  new Order(orderPrefab, orderMessageInfo.TargetEntity as Structure, orderMessageInfo.WallSectionIndex, orderGiver: orderGiver),
3752  _ => throw new NotImplementedException()
3753  };
3754  if (order != null && order.TargetAllCharacters)
3755  {
3756  var fadeOutTime = !orderPrefab.IsIgnoreOrder ? (float?)orderPrefab.FadeOutTime : null;
3757  activeOrders.Add((order, fadeOutTime));
3758  }
3759  }
3760  foreach (var (order, fadeOutTime) in activeOrders)
3761  {
3762  if (order.IsIgnoreOrder)
3763  {
3764  switch (order.TargetType)
3765  {
3766  case Order.OrderTargetType.Entity:
3767  if (order.TargetEntity is not IIgnorable ignorableEntity) { break; }
3768  ignorableEntity.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis;
3769  break;
3770  case Order.OrderTargetType.Position:
3771  throw new NotImplementedException();
3772  case Order.OrderTargetType.WallSection:
3773  if (!order.WallSectionIndex.HasValue) { break; }
3774  if (order.TargetEntity is not Structure s) { break; }
3775  if (s.GetSection(order.WallSectionIndex.Value) is not IIgnorable ignorableWall) { break; }
3776  ignorableWall.OrderedToBeIgnored = order.Identifier == Tags.IgnoreThis;
3777  break;
3778  }
3779  }
3780  GameMain.GameSession?.CrewManager?.AddOrder(order, fadeOutTime);
3781  }
3782  }
3783  }
3784 }
override bool IsValidTarget(Character target)
static Sprite GetSprite(Identifier identifier, Identifier option, Entity targetEntity)
HRManagerUI HRManagerUI
Definition: CampaignUI.cs:42
void Speak(string message, ChatMessageType? messageType=null, float delay=0.0f, Identifier identifier=default, float minDurationBetweenSimilar=0.0f)
void SetOrder(Order order, bool isNewOrder, bool speak=true, bool force=false)
Force an order to be set for the character, bypassing hearing checks
Stores information about the Character that is needed between rounds in the menu etc....
GUITextBox InputBox
Definition: ChatBox.cs:61
bool CloseAfterMessageSent
Definition: ChatBox.cs:36
readonly ChatManager ChatManager
Definition: ChatBox.cs:18
ChatBox(GUIComponent parent, bool isSinglePlayer)
Definition: ChatBox.cs:87
void AddMessage(ChatMessage message)
Definition: ChatBox.cs:389
bool TypingChatMessage(GUITextBox textBox, string text)
Definition: ChatBox.cs:342
void Store(string message)
Definition: ChatManager.cs:84
Triggers a "conversation popup" with text and support for different branching options.
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
ChatBox ChatBox
Present only in single player games. In multiplayer. The chatbox is found from GameSession....
bool CharacterClicked(GUIComponent component, object selection)
Sets which character is selected in the crew UI (highlight effect etc)
void CreateObjectiveIcon(Character character, Identifier identifier, Identifier option, Entity targetEntity)
void SetOrderHighlight(Character character, Identifier orderIdentifier, Identifier orderOption)
void OpenCommandUI(Entity entityContext=null, bool forceContextual=false)
void SetCharacterOrder(Character character, Order order, bool isNewOrder=true)
Sets the character's current order (if it's close enough to receive messages from orderGiver) and dis...
void KillCharacter(Character killedCharacter, bool resetCrewListIndex=true)
void AddSinglePlayerChatMessage(string senderName, string text, ChatMessageType messageType, Entity sender)
void AddCurrentOrderIcon(Character character, Order order)
Displays the specified order in the crew UI next to the character.
CrewManager(XElement element, bool isSinglePlayer)
GUIComponent AddCharacterToCrewList(Character character)
Add character to the list without actually adding it to the crew
void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally)
XElement Save(XElement parentElement)
Saves the current crew. Note that this is client-only code (only used in the single player campaign) ...
void UpdateReports()
Enables/disables report buttons when needed
bool IsCrewMenuOpen
This property stores the preference in settings. Don't use for automatic logic. Use AutoShowCrewList(...
void AddSinglePlayerChatMessage(LocalizedString senderName, LocalizedString text, ChatMessageType messageType, Entity sender)
Adds the message to the single player chatbox.
static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList< OrderPrefab > reports, bool isHorizontal)
Submarine Submarine
Definition: Entity.cs:53
const ushort NullEntityID
Definition: Entity.cs:14
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
GUIComponent GetChild(int index)
Definition: GUIComponent.cs:54
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
GUIComponent GetChildByUserData(object obj)
Definition: GUIComponent.cs:66
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual RichString ToolTip
virtual Rectangle Rect
IEnumerable< GUIComponent > GetAllChildren()
Returns all child elements in the hierarchy.
Definition: GUIComponent.cs:49
bool IsChildOf(GUIComponent component, bool recursive=true)
Definition: GUIComponent.cs:81
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1249
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:33
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static int GraphicsHeight
Definition: GameMain.cs:168
static bool IsMultiplayer
Definition: GameMain.cs:35
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static GameClient Client
Definition: GameMain.cs:188
static Hull FindHull(Vector2 position, Hull guess=null, bool useWorldCoordinates=true, bool inclusive=true)
Returns the hull which contains the point (or null if it isn't inside any)
static void ReportProblem(Character reporter, Order order, Hull targetHull=null)
static HashSet< Item > DeconstructItems
Items that have been marked for deconstruction
The base class for components holding the different functionalities of the item
JobPrefab Prefab
Definition: Job.cs:18
static string GetChatMessageCommand(string message, out string messageWithoutCommand)
static ChatMessage Create(string senderName, string text, ChatMessageType type, Entity sender, Client client=null, PlayerConnectionChangeType changeType=PlayerConnectionChangeType.None, Color? textColor=null)
static bool CanUseRadio(Character sender, bool ignoreJamming=false)
void Vote(VoteType voteType, object data)
Definition: GameClient.cs:2684
override IReadOnlyList< Client > ConnectedClients
Definition: GameClient.cs:133
void SendChatMessage(ChatMessage msg)
Definition: GameClient.cs:2372
static void UpdateVoiceIndicator(GUIImage soundIcon, float voipAmplitude, float deltaTime)
Definition: VoipClient.cs:172
readonly Entity TargetEntity
Definition: Order.cs:495
bool IsDeconstructOrder
Definition: Order.cs:559
ItemComponent GetTargetItemComponent(Item item)
Get the target item component based on the target item type
Definition: Order.cs:798
string GetChatMessage(string targetCharacterName, string targetRoomName, bool givingOrderToSelf, Identifier orderOption=default, bool isNewOrder=true)
LocalizedString Name
Definition: Order.cs:538
readonly Character OrderGiver
Definition: Order.cs:499
Order Clone()
Definition: Order.cs:774
Order WithType(OrderType type)
Definition: Order.cs:764
float FadeOutTime
Definition: Order.cs:553
bool IsReport
Definition: Order.cs:563
bool IsIgnoreOrder
Definition: Order.cs:558
readonly? int WallSectionIndex
Definition: Order.cs:536
readonly Identifier Option
Definition: Order.cs:482
Order WithTargetEntity(Entity entity)
Definition: Order.cs:724
Color Color
Definition: Order.cs:550
bool TargetAllCharacters
Definition: Order.cs:551
Order WithItemComponent(Item item, ItemComponent component=null)
Definition: Order.cs:749
Hull TargetHull
Definition: Order.cs:527
Identifier Identifier
Definition: Order.cs:540
Order WithWallSection(Structure wall, int? sectionIndex)
Definition: Order.cs:759
bool MatchesOrder(Identifier orderIdentifier, Identifier orderOption)
readonly OrderTargetType TargetType
Definition: Order.cs:535
Sprite SymbolSprite
Definition: Order.cs:549
OrderPrefab(ContentXElement orderElement, OrdersFile file)
Definition: Order.cs:163
readonly Sprite SymbolSprite
Definition: Order.cs:55
readonly float FadeOutTime
Definition: Order.cs:96
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
Order CreateInstance(OrderTargetType targetType, Character orderGiver=null, bool isAutonomous=false)
Create an Order instance with a null target
Definition: Order.cs:458
ItemComponent GetTargetItemComponent(Item item)
Get the target item component based on the target item type
Definition: Order.cs:341
bool IsVisibleAsReportButton
Definition: Order.cs:88
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
RectTransform?? Parent
void MoveOverTime(Point targetPos, float duration, Action onDoneMoving=null)
RectTransform(Vector2 relativeSize, RectTransform parent, Anchor anchor=Anchor.TopLeft, Pivot? pivot=null, Point? minSize=null, Point? maxSize=null, ScaleBasis scaleBasis=ScaleBasis.Normal)
Point NonScaledSize
Size before scale multiplications.
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
void CreateMiniMap(GUIComponent parent, IEnumerable< Entity > pointsOfInterest=null, bool ignoreOutpost=false)
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
OrderCategory
Definition: Order.cs:12
GUISoundType
Definition: GUI.cs:21