Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Items/Components/Machines/MiniMap.cs
1 #nullable enable
3 using Microsoft.Xna.Framework;
4 using Microsoft.Xna.Framework.Graphics;
5 using System;
6 using System.Collections.Generic;
7 using System.Collections.Immutable;
8 using System.Linq;
9 using Microsoft.Xna.Framework.Input;
10 
12 {
13  internal readonly struct MiniMapGUIComponent
14  {
15  public readonly GUIComponent RectComponent;
16  public readonly GUIComponent BorderComponent;
17 
18  public MiniMapGUIComponent(GUIComponent rectComponent)
19  {
20  RectComponent = rectComponent;
21  BorderComponent = rectComponent;
22  }
23 
24  public MiniMapGUIComponent(GUIComponent frame, GUIComponent linkedHullComponent)
25  {
26  RectComponent = frame;
27  BorderComponent = linkedHullComponent;
28  }
29 
30  public void Deconstruct(out GUIComponent component, out GUIComponent borderComponent)
31  {
32  component = RectComponent;
33  borderComponent = BorderComponent;
34  }
35  }
36 
37  internal readonly struct MiniMapSprite
38  {
39  public readonly Sprite? Sprite;
40  public readonly Color Color;
41 
42  public MiniMapSprite(JobPrefab prefab)
43  {
44  Sprite = prefab.IconSmall;
45  Color = prefab.UIColor;
46  }
47 
48  public MiniMapSprite(Order order)
49  {
50  Sprite = order.SymbolSprite;
51  Color = order.Color;
52  }
53  }
54 
55  internal readonly struct MiniMapHullData
56  {
57  public readonly List<List<Vector2>> Polygon;
58  public readonly (RectangleF Rect, Hull Hull)[] RectDatas;
59  public readonly RectangleF Bounds;
60  public readonly Point ParentSize;
61 
62  public MiniMapHullData(List<List<Vector2>> polygon, RectangleF bounds, Point parentSize, ImmutableArray<RectangleF> rects, ImmutableArray<Hull> hulls)
63  {
64  ParentSize = parentSize;
65  Bounds = bounds;
66  Polygon = polygon;
67  int count = Math.Min(rects.Length, hulls.Length);
68  RectDatas = new (RectangleF Rect, Hull Hull)[count];
69  for (int i = 0; i < count; i++)
70  {
71  RectDatas[i] = (rects[i], hulls[i]);
72  }
73  }
74  }
75 
76  internal enum MiniMapMode
77  {
78  None,
79  HullStatus,
80  ElectricalView,
81  ItemFinder
82  }
83 
84  internal readonly struct RelativeEntityRect
85  {
86  public readonly Vector2 RelativePosition;
87  public readonly Vector2 RelativeSize;
88 
89  public RelativeEntityRect(RectangleF worldBorders, RectangleF entityRect)
90  {
91  RelativePosition = new Vector2((entityRect.X - worldBorders.X) / worldBorders.Width, (worldBorders.Y - entityRect.Y) / worldBorders.Height);
92  RelativeSize = new Vector2(entityRect.Width / worldBorders.Width, entityRect.Height / worldBorders.Height);
93  }
94 
95  public Vector2 PositionRelativeTo(RectangleF frame, bool skipOffset = false)
96  {
97  if (skipOffset)
98  {
99  return RelativePosition * frame.Size;
100  }
101 
102  return frame.Location + RelativePosition * frame.Size;
103  }
104 
105  public Vector2 SizeRelativeTo(RectangleF frame)
106  {
107  return RelativeSize * frame.Size;
108  }
109 
110  public RectangleF RectangleRelativeTo(RectangleF frame, bool skipOffset = false)
111  {
112  return new RectangleF(PositionRelativeTo(frame, skipOffset), SizeRelativeTo(frame));
113  }
114 
115  public void Deconstruct(out float posX, out float posY, out float sizeX, out float sizeY)
116  {
117  posX = RelativePosition.X;
118  posY = RelativePosition.Y;
119  sizeX = RelativeSize.X;
120  sizeY = RelativeSize.Y;
121  }
122  }
123 
124  internal readonly struct MiniMapSettings
125  {
126  public static MiniMapSettings Default = new MiniMapSettings
127  (
128  createHullElements: true,
129  elementColor: MiniMap.MiniMapBaseColor
130  );
131 
132  public readonly bool CreateHullElements;
133  public readonly Color ElementColor;
134 
135  public MiniMapSettings(bool createHullElements = false, Color? elementColor = null)
136  {
137  CreateHullElements = createHullElements;
138  ElementColor = elementColor ?? MiniMap.MiniMapBaseColor;
139  }
140  }
141 
142  partial class MiniMap : Powered
143  {
144  private Dictionary<Hull, HullData> hullDatas;
145  private DateTime resetDataTime;
146 
147  private GUIFrame submarineContainer;
148 
149  private GUIFrame? hullInfoFrame;
150  private GUIScissorComponent? scissorComponent;
151  private GUIComponent? miniMapContainer;
152  private GUIComponent miniMapFrame;
153  private GUIComponent electricalFrame;
154  private GUILayoutGroup reportFrame;
155  private GUILayoutGroup searchBarFrame;
156  private GUITextBox searchBar;
157  private GUIComponent? searchAutoComplete;
158 
159  private ItemPrefab? searchedPrefab;
160 
161  private GUITextBlock tooltipHeader, tooltipFirstLine, tooltipSecondLine, tooltipThirdLine;
162 
163  private LocalizedString noPowerTip = string.Empty;
164 
165  private readonly List<Submarine> displayedSubs = new List<Submarine>();
166 
167  private Point prevResolution;
168  private float cardRefreshTimer;
169  private const float cardRefreshDelay = 3f;
170 
171  private readonly HashSet<MiniMapSprite> cardsToDraw = new HashSet<MiniMapSprite>();
172 
173  private List<MapEntity> subEntities = new List<MapEntity>();
174 
175  private Texture2D? submarinePreview;
176 
177  private MiniMapMode currentMode;
178  private ImmutableArray<GUIButton> modeSwitchButtons;
179 
180  private Point elementSize;
181 
182  private ImmutableDictionary<MapEntity, MiniMapGUIComponent> hullStatusComponents;
183  private ImmutableDictionary<MapEntity, MiniMapGUIComponent> electricalMapComponents;
184  private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> electricalChildren;
185  private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> doorChildren;
186  private ImmutableDictionary<MiniMapGUIComponent, GUIComponent> weaponChildren;
187 
188  private ImmutableHashSet<ItemPrefab>? itemsFoundOnSub;
189 
190  private ImmutableHashSet<Vector2>? MiniMapBlips;
191  private float blipState;
192  private const float maxBlipState = 1f;
193 
194  private const float maxZoom = 10f,
195  minZoom = 0.5f,
196  defaultZoom = 1f;
197 
198  private float zoom = defaultZoom;
199 
200  private float Zoom
201  {
202  get => zoom;
203  set => zoom = Math.Clamp(value, minZoom, maxZoom);
204  }
205 
206  private Vector2 mapOffset = Vector2.Zero;
207  private bool dragMap;
208  private Vector2? dragMapStart;
209  private const int dragTreshold = 8;
210 
211  private bool recalculate;
212 
213  public static readonly Color MiniMapBaseColor = new Color(15, 178, 107);
214 
215  private static readonly Color WetHullColor = new Color(11, 122, 205),
216  DoorIndicatorColor = GUIStyle.Green,
217  NoPowerDoorColor = DoorIndicatorColor * 0.1f,
218  DefaultNeutralColor = MiniMapBaseColor * 0.8f,
219  HoverColor = Color.White,
220  BlueprintBlue = new Color(23, 38, 33),
221  HullWaterColor = new Color(17, 173, 179) * 0.5f,
222  HullWaterLineColor = Color.LightBlue * 0.5f,
223  NoPowerColor = MiniMapBaseColor * 0.1f,
224  ElectricalBaseColor = GUIStyle.Orange,
225  NoPowerElectricalColor = ElectricalBaseColor * 0.1f;
226 
227  partial void InitProjSpecific()
228  {
229  hullDatas = new Dictionary<Hull, HullData>();
230 
231  SetDefaultMode();
232 
233  noPowerTip = TextManager.Get("SteeringNoPowerTip");
234  CreateGUI();
235  }
236 
237  private void SetDefaultMode()
238  {
239  currentMode = true switch
240  {
241  true when EnableHullStatus => MiniMapMode.HullStatus,
242  true when EnableElectricalView => MiniMapMode.ElectricalView,
243  true when EnableItemFinder => MiniMapMode.ItemFinder,
244  _ => MiniMapMode.None
245  };
246  }
247 
248  protected override void CreateGUI()
249  {
252 
253  GuiFrame.RectTransform.RelativeOffset = new Vector2(0.05f, 0.0f);
254  GuiFrame.CanBeFocused = true;
255  var submarineBack = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDBack, null);
256  GUIFrame paddedContainer = new GUIFrame(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center), style: null);
257  submarineContainer = new GUIFrame(new RectTransform(Vector2.One, paddedContainer.RectTransform, Anchor.Center), style: null);
258  var submarineFront = new GUICustomComponent(new RectTransform(GuiFrame.Rect.Size - GUIStyle.ItemFrameMargin, GuiFrame.RectTransform, Anchor.Center) { AbsoluteOffset = GUIStyle.ItemFrameOffset }, DrawHUDFront, null)
259  {
260  CanBeFocused = false
261  };
262 
263  GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, isHorizontal: true) { CanBeFocused = true };
264 
265  modeSwitchButtons = ImmutableArray.Create
266  (
267  new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.HullStatus") { UserData = MiniMapMode.HullStatus, Enabled = EnableHullStatus, ToolTip = TextManager.Get("StatusMonitorButton.HullStatus.Tooltip") },
268  new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ElectricalView") { UserData = MiniMapMode.ElectricalView, Enabled = EnableElectricalView, ToolTip = TextManager.Get("StatusMonitorButton.ElectricalView.Tooltip") },
269  new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonLayout.RectTransform), string.Empty, style: "StatusMonitorButton.ItemFinder") { UserData = MiniMapMode.ItemFinder, Enabled = EnableItemFinder, ToolTip = TextManager.Get("StatusMonitorButton.ItemFinder.Tooltip") }
270  );
271 
272  foreach (GUIButton button in modeSwitchButtons)
273  {
274  button.OnClicked = (btn, o) =>
275  {
276  if (!(o is MiniMapMode m)) { return false; }
277 
278  currentMode = m;
279  Zoom = defaultZoom;
280  mapOffset = Vector2.Zero;
281  recalculate = true;
282 
283  foreach (GUIButton otherButton in modeSwitchButtons)
284  {
285  otherButton.Selected = false;
286  }
287 
288  btn.Selected = true;
289  return true;
290  };
291 
292  if (button.UserData is MiniMapMode buttonMode)
293  {
294  button.Selected = currentMode == buttonMode;
295  }
296  }
297 
298  OrderPrefab[] reports = OrderPrefab.Prefabs.Where(o => o.IsVisibleAsReportButton).OrderBy(o => o.Identifier).ToArray();
299 
300  GUIFrame bottomFrame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.15f), paddedContainer.RectTransform, Anchor.BottomCenter) { MaxSize = new Point(int.MaxValue, GUI.IntScale(40)) }, style: null)
301  {
302  CanBeFocused = false
303  };
304 
305  reportFrame = new GUILayoutGroup(new RectTransform(new Vector2(1), bottomFrame.RectTransform), isHorizontal: true)
306  {
307  Stretch = true,
308  AbsoluteSpacing = GUI.IntScale(5)
309  };
310 
311  if (reports.Any())
312  {
313  CrewManager.CreateReportButtons(GameMain.GameSession?.CrewManager, reportFrame, reports, true);
314  }
315 
316  searchBarFrame = new GUILayoutGroup(new RectTransform(new Vector2(1.5f, 1.0f), bottomFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.Center)
317  {
318  Visible = false
319  };
320  searchBar = new GUITextBox(new RectTransform(new Vector2(1), searchBarFrame.RectTransform), string.Empty, createClearButton: true, createPenIcon: true)
321  {
322  OnEnterPressed = (box, text) =>
323  {
324  SearchItems(text);
325  return true;
326  }
327  };
328 
329  searchAutoComplete = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas), style: "GUIToolTip")
330  {
331  Visible = false,
332  CanBeFocused = false
333  };
334 
335  SetAutoCompletePosition(searchAutoComplete, searchBar);
336 
337  GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, searchAutoComplete.RectTransform))
338  {
339  PlaySoundOnSelect = true,
340  OnSelected = (component, o) =>
341  {
342  if (o is ItemPrefab prefab)
343  {
344  searchedPrefab = prefab;
345  searchBar.TextBlock.Text = prefab.Name;
346  searchBar.Deselect();
347  SearchItems(searchBar.Text);
348  }
349  return true;
350  }
351  };
352 
353  List<ItemPrefab> shownItemPrefabs = new List<ItemPrefab>();
354  foreach (ItemPrefab prefab in ItemPrefab.Prefabs.OrderBy(prefab => prefab.Name))
355  {
356  if (prefab.HideInMenus) { continue; }
357  if (shownItemPrefabs.Any(ip => DisplayAsSameItem(ip, prefab)))
358  {
359  continue;
360  }
361  CreateItemFrame(prefab, listBox.Content.RectTransform);
362  shownItemPrefabs.Add(prefab);
363  }
364 
365  searchBar.OnDeselected += (sender, key) =>
366  {
367  searchAutoComplete.Visible = false;
368  };
369 
370  searchBar.OnSelected += (sender, key) =>
371  {
372  itemsFoundOnSub = Item.ItemList.Where(it => VisibleOnItemFinder(it)).Select(it => it.Prefab).ToImmutableHashSet();
373  };
374 
375  searchBar.OnKeyHit += ControlSearchTooltip;
376  searchBar.OnTextChanged += UpdateSearchTooltip;
377 
378  hullInfoFrame = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.13f), GUI.Canvas, minSize: new Point(250, 150)), style: "GUIToolTip")
379  {
380  CanBeFocused = false,
381  Visible = false
382  };
383 
384  var hullInfoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), hullInfoFrame.RectTransform, Anchor.Center))
385  {
386  Stretch = true,
387  RelativeSpacing = 0.05f
388  };
389 
390  tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true };
391  tooltipFirstLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true };
392  tooltipSecondLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true };
393  tooltipThirdLine = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), hullInfoContainer.RectTransform), string.Empty) { Wrap = true };
394 
395  hullInfoFrame.Children.ForEach(c =>
396  {
397  c.CanBeFocused = false;
398  c.Children.ForEach(c2 => c2.CanBeFocused = false);
399  });
400 
401  submarineBack.RectTransform.MaxSize =
402  submarineFront.RectTransform.MaxSize =
403  submarineContainer.RectTransform.MaxSize =
404  new Point(int.MaxValue, paddedContainer.Rect.Height - bottomFrame.Rect.Height - buttonLayout.Rect.Height);
405  }
406 
407  private static Sprite GetPreviewSprite(ItemPrefab prefab)
408  {
409  return prefab.InventoryIcon ?? prefab.Sprite;
410  }
411 
416  private static bool DisplayAsSameItem(ItemPrefab prefab1, ItemPrefab prefab2)
417  {
418  if (prefab1 == prefab2) { return true; }
419  if (prefab1.Name == prefab2.Name)
420  {
421  var sprite1 = GetPreviewSprite(prefab1);
422  var sprite2 = GetPreviewSprite(prefab2);
423  return sprite1?.FullPath == sprite2?.FullPath && sprite1?.SourceRect == sprite2?.SourceRect;
424  }
425  return false;
426  }
427 
428  private bool VisibleOnItemFinder(Item it)
429  {
430  if (it?.Submarine == null) { return false; }
431  if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
432  if (it.NonInteractable || it.IsHidden) { return false; }
433  if (it.GetComponent<Pickable>() == null) { return false; }
434 
435  var holdable = it.GetComponent<Holdable>();
436  if (holdable != null && holdable.Attached) { return false; }
437 
438  var wire = it.GetComponent<Wire>();
439  if (wire != null && wire.Connections.Any(c => c != null)) { return false; }
440 
441  if (it.Container?.GetComponent<ItemContainer>() is { DrawInventory: false } or { AllowAccess: false }) { return false; }
442 
443  if (it.HasTag(Tags.TraitorMissionItem)) { return false; }
444 
445  return true;
446  }
447 
448  public override void AddToGUIUpdateList(int order = 0)
449  {
450  base.AddToGUIUpdateList(order);
451  hullInfoFrame?.AddToGUIUpdateList(order: order + 1);
452  if (currentMode == MiniMapMode.ItemFinder && searchBar.Selected)
453  {
454  searchAutoComplete?.AddToGUIUpdateList(order: order + 1);
455  }
456  }
457 
458  private void CreateHUD()
459  {
460  subEntities.Clear();
461  prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
462  submarineContainer.ClearChildren();
463 
464  if (item.Submarine is null)
465  {
466  displayedSubs.Clear();
467  return;
468  }
469 
470  scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
471  miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
472 
473  ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent<Door>() != null || it.GetComponent<Turret>() != null)).ToImmutableHashSet();
474  miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents);
475 
476  IEnumerable<Item> electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.IsHidden && !it.NonInteractable && it.GetComponent<Repairable>() != null);
477  electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
478 
479  Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
480 
481  foreach (var (entity, component) in electricalMapComponents)
482  {
483  GUIComponent parent = component.RectComponent;
484  if (entity is not Item it ) { continue; }
485  Sprite? sprite = it.Prefab.UpgradePreviewSprite;
486  if (sprite is null) { continue; }
487 
488  GUIImage child = new GUIImage(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), sprite)
489  {
490  OutlineColor = ElectricalBaseColor,
491  Color = ElectricalBaseColor,
492  HoverCursor = CursorState.Hand,
493  SpriteEffects = item.Rotation > 90.0f && item.Rotation < 270.0f ? SpriteEffects.FlipVertically : SpriteEffects.None
494  };
495 
496  electricChildren.Add(component, child);
497  }
498 
499  electricalChildren = electricChildren.ToImmutableDictionary();
500 
501  Dictionary<MiniMapGUIComponent, GUIComponent> doorChilds = new Dictionary<MiniMapGUIComponent, GUIComponent>();
502  Dictionary<MiniMapGUIComponent, GUIComponent> weaponChilds = new Dictionary<MiniMapGUIComponent, GUIComponent>();
503 
504  foreach (var (entity, component) in hullStatusComponents)
505  {
506  if (!hullPointsOfInterest.Contains(entity)) { continue; }
507 
508  if (entity is not Item it) { continue; }
509  const int borderMaxSize = 2;
510 
511  if (it.GetComponent<Door>() is { })
512  {
513  const int minSize = 8;
514 
515  Point size = component.BorderComponent.Rect.Size;
516 
517  size.X = Math.Max(size.X, minSize);
518  size.Y = Math.Max(size.Y, minSize);
519  float width = Math.Min(borderMaxSize, Math.Min(size.X, size.Y) / 8f);
520 
521  GUIFrame frame = new GUIFrame(new RectTransform(size, component.RectComponent.RectTransform, anchor: Anchor.Center), style: "ScanLines", color: DoorIndicatorColor)
522  {
523  OutlineColor = DoorIndicatorColor,
524  OutlineThickness = width
525  };
526  doorChilds.Add(component, frame);
527  }
528  else if (it.GetComponent<Turret>() is { } turret)
529  {
530  int parentWidth = (int) (submarineContainer.Rect.Width / 16f);
531  GUICustomComponent frame = new GUICustomComponent(new RectTransform(new Point(parentWidth, parentWidth), component.RectComponent.RectTransform, anchor: Anchor.Center), (batch, customComponent) =>
532  {
533  Vector2 center = customComponent.Center;
534  float rotation = turret.Rotation;
535 
536  if (!hasPower)
537  {
538  float minRotation = MathHelper.ToRadians(Math.Min(turret.RotationLimits.X, turret.RotationLimits.Y)),
539  maxRotation = MathHelper.ToRadians(Math.Max(turret.RotationLimits.X, turret.RotationLimits.Y));
540 
541  rotation = (minRotation + maxRotation) / 2;
542  }
543 
544  if (turret.WeaponIndicatorSprite is { } weaponSprite)
545  {
546  Vector2 origin = weaponSprite.Origin;
547  float scale = parentWidth / Math.Max(weaponSprite.size.X, weaponSprite.size.Y);
548  Color color = !hasPower ? NoPowerColor : turret.ActiveUser is null ? Color.DimGray : GUIStyle.Green;
549  weaponSprite.Draw(batch, center, color, origin, rotation, scale, SpriteEffects.None);
550  }
551  })
552  {
553  CanBeFocused = false
554  };
555 
556  weaponChilds.Add(component, frame);
557  }
558  }
559 
560  doorChildren = doorChilds.ToImmutableDictionary();
561  weaponChildren = weaponChilds.ToImmutableDictionary();
562 
563  Rectangle parentRect = miniMapFrame.Rect;
564 
565  displayedSubs.Clear();
566  displayedSubs.Add(item.Submarine);
567  displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID));
568 
569  subEntities = MapEntity.MapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.IsHidden).OrderByDescending(w => w.SpriteDepth).ToList();
570 
571  BakeSubmarine(item.Submarine, parentRect);
572  elementSize = GuiFrame.Rect.Size;
573  }
574 
575  public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
576  {
577  //recreate HUD if the subs we should display have changed
578  if (item.Submarine == null && displayedSubs.Count > 0 || // item not inside a sub anymore, but display is still showing subs
579  item.Submarine is { } itemSub &&
580  (
581  !displayedSubs.Contains(itemSub) || // current sub not displayed
582  itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
583  displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed
584  ) ||
585  prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed
586  !submarineContainer.Children.Any()) // We lack a GUI
587  {
588  CreateHUD();
589  }
590 
591  //reset data if we haven't received anything in a while
592  //(so that outdated hull info won't be shown if detectors stop sending signals)
593  if (DateTime.Now > resetDataTime)
594  {
595  foreach (HullData hullData in hullDatas.Values)
596  {
597  if (!hullData.Distort)
598  {
599  if (Timing.TotalTime > hullData.LastOxygenDataTime + 1.0) { hullData.ReceivedOxygenAmount = null; }
600  if (Timing.TotalTime > hullData.LastWaterDataTime + 1.0) { hullData.ReceivedWaterAmount = null; }
601  }
602  }
603  resetDataTime = DateTime.Now + new TimeSpan(0, 0, 1);
604  }
605 
606  if (cardRefreshTimer > cardRefreshDelay)
607  {
608  if (item.Submarine is { } sub)
609  {
610  UpdateIDCards(sub);
611  }
612  cardRefreshTimer = 0;
613  }
614  else
615  {
616  cardRefreshTimer += deltaTime;
617  }
618 
619  if (scissorComponent != null)
620  {
621  if (PlayerInput.PrimaryMouseButtonDown() && currentMode != MiniMapMode.HullStatus)
622  {
623  if (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn))
624  {
625  dragMapStart = PlayerInput.MousePosition;
626  }
627  }
628 
629  if (currentMode != MiniMapMode.HullStatus && Math.Abs(PlayerInput.ScrollWheelSpeed) > 0 && (GUI.MouseOn == scissorComponent || scissorComponent.IsParentOf(GUI.MouseOn)))
630  {
631  float newZoom = Math.Clamp(Zoom + PlayerInput.ScrollWheelSpeed / 1000.0f * Zoom, minZoom, maxZoom);
632  float distanceScale = newZoom / Zoom;
633  mapOffset *= distanceScale;
634  recalculate |= !MathUtils.NearlyEqual(Zoom, newZoom);
635  Zoom = newZoom;
636  }
637  }
638 
639  if (dragMapStart is { } dragStart)
640  {
641  if (dragMap || Vector2.DistanceSquared(dragStart, PlayerInput.MousePosition) > GUI.IntScale(dragTreshold * dragTreshold))
642  {
643  mapOffset.X += PlayerInput.MouseSpeed.X;
644  mapOffset.Y += PlayerInput.MouseSpeed.Y;
645 
646  recalculate = true;
647  dragMap = true;
648  }
649  }
650 
652  {
653  dragMapStart = null;
654  dragMap = false;
655  }
656 
657  if (recalculate)
658  {
659  if (miniMapContainer != null)
660  {
661  miniMapContainer.RectTransform.LocalScale = new Vector2(Zoom);
662  miniMapContainer.RectTransform.RecalculateChildren(true, true);
663  miniMapContainer.RectTransform.AbsoluteOffset = mapOffset.ToPoint();
664  }
665  recalculate = false;
666  }
667 
668  // is there a better way to do this?
669  if (GuiFrame.Rect.Size != elementSize)
670  {
671  CreateGUI();
672  elementSize = GuiFrame.Rect.Size;
673  }
674 
675  float distort = item.Repairables.Any(r => r.IsBelowRepairThreshold) ? 1.0f - item.Condition / item.MaxCondition : 0.0f;
676  foreach (HullData hullData in hullDatas.Values)
677  {
678  hullData.DistortionTimer -= deltaTime;
679  if (hullData.DistortionTimer <= 0.0f)
680  {
681  hullData.Distort = Rand.Range(0.0f, 1.0f) < distort * distort;
682  if (hullData.Distort)
683  {
684  hullData.ReceivedOxygenAmount = Rand.Range(0.0f, 100.0f);
685  hullData.ReceivedWaterAmount = Rand.Range(0.0f, 100.0f);
686  }
687  hullData.DistortionTimer = Rand.Range(1.0f, 10.0f);
688  }
689  }
690 
691  UpdateHUDBack();
692 
693  if (blipState > maxBlipState)
694  {
695  blipState = 0;
696  }
697 
698  blipState += deltaTime;
699 
700  if (currentMode == MiniMapMode.HullStatus && !EnableHullStatus ||
701  currentMode == MiniMapMode.ElectricalView && !EnableElectricalView ||
702  currentMode == MiniMapMode.ItemFinder && !EnableItemFinder)
703  {
704  SetDefaultMode();
705  }
706 
707  modeSwitchButtons[0].Enabled = EnableHullStatus;
708  modeSwitchButtons[1].Enabled = EnableElectricalView;
709  modeSwitchButtons[2].Enabled = EnableItemFinder;
710  }
711 
712  private void UpdateIDCards(Submarine sub)
713  {
714  if (hullDatas is null) { return; }
715 
716  foreach (HullData data in hullDatas.Values)
717  {
718  data.Cards.Clear();
719  }
720 
721  foreach (Item it in sub.GetItems(true))
722  {
723  if (it is { CurrentHull: { } hull } && it.GetComponent<IdCard>() is { } idCard && idCard.TeamID == sub.TeamID)
724  {
725  if (!hullDatas.ContainsKey(hull)) { continue; }
726 
727  hullDatas[hull].Cards.Add(idCard);
728  }
729  }
730  }
731 
732  private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container)
733  {
734  if (miniMapFrame == null)
735  {
736  //frame not created yet, could happen if the item hasn't been inside any sub this round?
737  return;
738  }
739 
740  if (Voltage < MinVoltage)
741  {
742  Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip);
743  Vector2 textPos = GuiFrame.Rect.Center.ToVector2();
744  Color noPowerColor = GUIStyle.Orange * (float)Math.Abs(Math.Sin(Timing.TotalTime));
745 
746  GUI.DrawString(spriteBatch, textPos - textSize / 2, noPowerTip, noPowerColor, Color.Black * 0.8f, font: GUIStyle.SubHeadingFont);
747  return;
748  }
749 
750  if (currentMode == MiniMapMode.HullStatus && item.Submarine != null)
751  {
752  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
753  spriteBatch.End();
754  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
755  spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect;
756 
757  var sprite = GUIStyle.UIGlowSolidCircular.Value?.Sprite;
758  float alpha = (MathF.Sin(blipState / maxBlipState * MathHelper.TwoPi) + 1.5f) * 0.5f;
759  if (sprite != null && ShowHullIntegrity)
760  {
761  Vector2 spriteSize = sprite.size;
762  Rectangle worldBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
763  worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
764  foreach (Gap gap in Gap.GapList)
765  {
766  if (gap.IsRoomToRoom || gap.linkedTo.Count == 0 || gap.Submarine != item.Submarine || gap.ConnectedDoor != null || gap.IsHidden) { continue; }
767  RectangleF entityRect = ScaleRectToUI(gap, miniMapFrame.Rect, worldBorders);
768 
769  Vector2 scale = new Vector2(entityRect.Size.X / spriteSize.X, entityRect.Size.Y / spriteSize.Y) * 2.0f;
770 
771  Color color = ToolBox.GradientLerp(gap.Open, GUIStyle.HealthBarColorMedium, GUIStyle.HealthBarColorLow) * alpha;
772  sprite.Draw(spriteBatch,
773  miniMapFrame.Rect.Location.ToVector2() + entityRect.Center,
774  color, origin: sprite.Origin, rotate: 0.0f, scale: scale);
775  }
776  }
777 
778  if (currentMode == MiniMapMode.HullStatus && hullStatusComponents != null)
779  {
780  foreach (var (entity, component) in hullStatusComponents)
781  {
782  if (!(entity is Hull hull)) { continue; }
783  if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; }
784  DrawHullCards(spriteBatch, hull, hullData, component.RectComponent);
785 
786  if (item.CurrentHull is { } currentHull && currentHull == hull)
787  {
788  Sprite? pingCircle = GUIStyle.YouAreHereCircle.Value?.Sprite;
789  if (pingCircle is null) { continue; }
790 
791  Vector2 charPos = item.WorldPosition;
792  Vector2 hullPos = hull.WorldRect.Location.ToVector2(),
793  hullSize = hull.WorldRect.Size.ToVector2();
794  Vector2 relativePos = (charPos - hullPos) / hullSize * component.RectComponent.Rect.Size.ToVector2();
795  relativePos.Y = -relativePos.Y;
796 
797  float parentWidth = submarineContainer.Rect.Width / 64f;
798  float spriteSize = pingCircle.size.X * (parentWidth / pingCircle.size.X);
799 
800  Vector2 drawPos = component.RectComponent.Rect.Location.ToVector2() + relativePos;
801  drawPos -= new Vector2(spriteSize, spriteSize) / 2f;
802 
803  pingCircle.Draw(spriteBatch, drawPos, GUIStyle.Red * 0.8f, Vector2.Zero, 0f, parentWidth / pingCircle.size.X);
804  }
805  }
806  }
807 
808  spriteBatch.End();
809  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
810  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
811  }
812  }
813 
814  private void ControlSearchTooltip(GUITextBox sender, Keys key)
815  {
816  if (searchAutoComplete is null || !searchAutoComplete.Visible) { return; }
817  GUIListBox listBox = searchAutoComplete.GetChild<GUIListBox>();
818  if (listBox is null) { return; }
819 
820  if (key == Keys.Down)
821  {
822  listBox.SelectNext(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes);
823  }
824  else if (key == Keys.Up)
825  {
826  listBox.SelectPrevious(force: GUIListBox.Force.Yes, playSelectSound: GUIListBox.PlaySelectSound.Yes);
827  }
828  else if (key == Keys.Enter)
829  {
830  listBox.OnSelected?.Invoke(listBox, listBox.SelectedData);
831  searchBar.Deselect();
832  }
833  }
834 
835  private bool UpdateSearchTooltip(GUITextBox box, string? text)
836  {
837  if (text is null || itemsFoundOnSub is null || searchAutoComplete is null) { return false; }
838 
839  MiniMapBlips = null;
840  searchedPrefab = null;
841  searchAutoComplete.Visible = true;
842  SetAutoCompletePosition(searchAutoComplete, box);
843 
844  GUIListBox? listBox = searchAutoComplete.GetChild<GUIListBox>();
845  if (listBox?.Content is null) { return false; }
846 
847  bool first = true;
848 
849  int i = 0;
850 
851  foreach (GUIComponent component in listBox.Content.Children)
852  {
853  component.Visible = false;
854  if (component.UserData is ItemPrefab { Name: { } prefabName} prefab &&
855  (itemsFoundOnSub.Contains(prefab) || itemsFoundOnSub.Any(ip => DisplayAsSameItem(ip, prefab))))
856  {
857  component.Visible = prefabName.ToLower().Contains(text.ToLower());
858 
859  if (component.Visible && first)
860  {
861  listBox.Select(i, GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled);
862  first = false;
863  }
864  }
865 
866  i++;
867  }
868 
869  listBox.BarScroll = 0f;
870  listBox.RecalculateChildren();
871 
872  return true;
873  }
874 
875  private void SetAutoCompletePosition(GUIComponent tooltip, GUITextBox box)
876  {
877  int height = GuiFrame.Rect.Height / 2;
878  tooltip.RectTransform.NonScaledSize = new Point(box.Rect.Width, height);
879  tooltip.RectTransform.ScreenSpaceOffset = new Point(box.Rect.X, box.Rect.Y - height);
880  }
881 
882  private static void CreateItemFrame(ItemPrefab prefab, RectTransform parent)
883  {
884  Sprite sprite = GetPreviewSprite(prefab);
885  if (sprite is null) { return; }
886  GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), parent), style: "ListBoxElement")
887  {
888  UserData = prefab
889  };
890 
891  GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), isHorizontal: true)
892  {
893  Stretch = true
894  };
895  new GUIImage(new RectTransform(Vector2.One, layout.RectTransform, scaleBasis: ScaleBasis.BothHeight), sprite)
896  {
897  Color = prefab.InventoryIconColor,
898  UserData = prefab
899  };
900 
901  var nameText = new GUITextBlock(new RectTransform(Vector2.One, layout.RectTransform), prefab.Name);
902  nameText.RectTransform.SizeChanged += () =>
903  {
904  nameText.Text = ToolBox.LimitString(prefab.Name, nameText.Font, nameText.Rect.Width);
905  };
906  }
907 
908  private void SearchItems(string text)
909  {
910  if (searchedPrefab is null)
911  {
912  ItemPrefab? first = ItemPrefab.Prefabs.FirstOrDefault(p => p.Name.ToLower().Equals(text.ToLower()));
913 
914  if (first is null)
915  {
916  searchBar.Flash(GUIStyle.Red);
917  return;
918  }
919  searchedPrefab = first;
920  }
921 
922  if (item.Submarine is null) { return; }
923 
924  HashSet<Item> foundItems = new HashSet<Item>();
925 
926  foreach (Item it in Item.ItemList)
927  {
928  if (!VisibleOnItemFinder(it)) { continue; }
929 
930  if (DisplayAsSameItem(it.Prefab, searchedPrefab))
931  {
932  // ignore items on players and hidden inventories
933  if (it.FindParentInventory(inv => inv is CharacterInventory || inv is ItemInventory { Owner: Item { IsHidden: true }}) is { }) { continue; }
934 
935  if (it.FindParentInventory(inventory => inventory is ItemInventory { Owner: Item { ParentInventory: null } }) is ItemInventory parent)
936  {
937  foundItems.Add((Item)parent.Owner);
938  }
939  else
940  {
941  foundItems.Add(it);
942  }
943  }
944  }
945 
946 
947  RectangleF dockedBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
948  dockedBorders.Location += item.Submarine.WorldPosition;
949  RectangleF parentRect = miniMapFrame.Rect;
950 
951  HashSet<Vector2> positions = new HashSet<Vector2>();
952  foreach (Item foundItem in foundItems)
953  {
954  RelativeEntityRect scaledRect = new RelativeEntityRect(dockedBorders, foundItem.WorldRect);
955  Vector2 pos = scaledRect.PositionRelativeTo(parentRect, skipOffset: true) + scaledRect.SizeRelativeTo(parentRect) / 2f;
956  positions.Add(pos);
957  }
958 
959  MiniMapBlips = positions.ToImmutableHashSet();
960 
961  if (searchAutoComplete is null) { return; }
962  searchAutoComplete.Visible = false;
963  }
964 
965  private void UpdateHUDBack()
966  {
967  if (item.Submarine == null) { return; }
968 
969  if (hullInfoFrame != null) { hullInfoFrame.Visible = false; }
970  reportFrame.Visible = false;
971  searchBarFrame.Visible = false;
972  electricalFrame.Visible = false;
973  miniMapFrame.Visible = false;
974 
975  switch (currentMode)
976  {
977  case MiniMapMode.HullStatus:
978  UpdateHullStatus();
979  miniMapFrame.Visible = true;
980  reportFrame.Visible = true;
981  break;
982  case MiniMapMode.ElectricalView:
983  UpdateElectricalView();
984  electricalFrame.Visible = true;
985  break;
986  case MiniMapMode.ItemFinder:
987  searchBarFrame.Visible = true;
988  break;
989  }
990  }
991 
992  private void UpdateHullStatus()
993  {
994  bool canHoverOverHull = true;
995 
996  foreach (var (entity, component) in hullStatusComponents)
997  {
998  // we are only interested in non-hull components
999  if (entity is Hull) { continue; }
1000 
1001  GUIComponent rectComponent = component.RectComponent;
1002 
1003  if (doorChildren.TryGetValue(component, out GUIComponent? child) && child != null)
1004  {
1005  if (item.Submarine == null || !hasPower)
1006  {
1007  child.Color = child.OutlineColor = NoPowerDoorColor;
1008  }
1009 
1010  if (Voltage < MinVoltage) { continue; }
1011 
1012  child.Color = child.OutlineColor = DoorIndicatorColor;
1013  if (GUI.MouseOn == child)
1014  {
1015  SetTooltip(rectComponent.Rect.Center, entity.Name, string.Empty, string.Empty, string.Empty);
1016  canHoverOverHull = false;
1017  child.Color = child.OutlineColor = HoverColor;
1018  }
1019  }
1020  }
1021 
1022  foreach (var (entity, (component, borderComponent)) in hullStatusComponents)
1023  {
1024  if (item.Submarine == null || !hasPower)
1025  {
1026  component.Color = borderComponent.OutlineColor = NoPowerColor;
1027  }
1028 
1029  if (!component.Visible) { continue; }
1030  if (entity is not Hull hull) { continue; }
1031  if (!submarineContainer.Rect.Contains(component.Rect))
1032  {
1033  if (hull.Submarine.Info.Type != SubmarineType.Player)
1034  {
1035  component.Visible = borderComponent.Visible = false;
1036  continue;
1037  }
1038  }
1039 
1040  if (Voltage < MinVoltage) { continue; }
1041 
1042  hullDatas.TryGetValue(hull, out HullData? hullData);
1043  if (hullData is null)
1044  {
1045  hullData = new HullData();
1046  GetLinkedHulls(hull, hullData.LinkedHulls);
1047  hullDatas.Add(hull, hullData);
1048  }
1049 
1050  Color neutralColor = DefaultNeutralColor;
1051  Color borderColor = neutralColor;
1052  Color componentColor;
1053 
1054  if (hull.IsWetRoom)
1055  {
1056  neutralColor = WetHullColor;
1057  }
1058 
1059  if (hullData.Distort)
1060  {
1061  borderComponent.OutlineColor = neutralColor * 0.5f;
1062  component.Color = Color.Lerp(Color.Black, Color.DarkGray * 0.5f, Rand.Range(0.0f, 1.0f));
1063  continue;
1064  }
1065 
1066  if (RequireOxygenDetectors)
1067  {
1068  hullData.HullOxygenAmount = hullData.ReceivedOxygenAmount;
1069  }
1070  else if (hullData.LinkedHulls.Any())
1071  {
1072  hullData.HullOxygenAmount = 0.0f;
1073  foreach (Hull linkedHull in hullData.LinkedHulls)
1074  {
1075  hullData.HullOxygenAmount += linkedHull.OxygenPercentage;
1076  }
1077  hullData.HullOxygenAmount /= hullData.LinkedHulls.Count;
1078  }
1079  else
1080  {
1081  hullData.HullOxygenAmount = hull.OxygenPercentage;
1082  }
1083  if (RequireWaterDetectors)
1084  {
1085  hullData.HullWaterAmount = hullData.ReceivedWaterAmount;
1086  }
1087  else if (hullData.LinkedHulls.Any())
1088  {
1089  float waterVolume = 0.0f;
1090  float totalVolume = 0.0f;
1091  foreach (Hull linkedHull in hullData.LinkedHulls)
1092  {
1093  //water detector ignores very small amounts of water,
1094  //do it here too so the nav terminal doesn't display the water
1095  if (WaterDetector.GetWaterPercentage(linkedHull) > 0.0f)
1096  {
1097  waterVolume += linkedHull.WaterVolume;
1098  }
1099  totalVolume += linkedHull.Volume;
1100  }
1101  hullData.HullWaterAmount =
1102  waterVolume > 1.0f ?
1103  MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f;
1104  }
1105  else
1106  {
1107  hullData.HullWaterAmount = WaterDetector.GetWaterPercentage(hull);
1108  }
1109 
1110  float gapOpenSum = 0.0f;
1111 
1112  if (ShowHullIntegrity)
1113  {
1114  float amount = 1f + hullData.LinkedHulls.Count;
1115  gapOpenSum = hull.ConnectedGaps.Concat(hullData.LinkedHulls.SelectMany(h => h.ConnectedGaps)).Where(g => g.linkedTo.Count == 1 && !g.IsHidden).Sum(g => g.Open) / amount;
1116  borderColor = Color.Lerp(neutralColor, GUIStyle.Red, Math.Min(gapOpenSum, 1.0f));
1117  }
1118 
1119  bool isHoveringOver = canHoverOverHull && GUI.MouseOn == component;
1120 
1121  // When drawing tooltip we are only interested in the component we are hovering over
1122  if (isHoveringOver)
1123  {
1124  LocalizedString header = hull.DisplayName;
1125 
1126  float? oxygenAmount = hullData.HullOxygenAmount,
1127  waterAmount = hullData.HullWaterAmount;
1128 
1129  LocalizedString line1 = gapOpenSum > 0.1f ? TextManager.Get("MiniMapHullBreach") : string.Empty;
1130  Color line1Color = GUIStyle.Red;
1131 
1132  LocalizedString line2 = oxygenAmount == null ?
1133  TextManager.Get("MiniMapAirQualityUnavailable") :
1134  TextManager.AddPunctuation(':', TextManager.Get("MiniMapAirQuality"), (int)Math.Round(oxygenAmount.Value) + "%");
1135  Color line2Color = oxygenAmount == null ? GUIStyle.Red : Color.Lerp(GUIStyle.Red, Color.LightGreen, (float)oxygenAmount / 100.0f);
1136 
1137  LocalizedString line3 = waterAmount == null ?
1138  TextManager.Get("MiniMapWaterLevelUnavailable") :
1139  TextManager.AddPunctuation(':', TextManager.Get("MiniMapWaterLevel"), (int)Math.Round(waterAmount.Value) + "%");
1140  Color line3Color = waterAmount == null ? GUIStyle.Red : Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)waterAmount / 100.0f);
1141 
1142  SetTooltip(borderComponent.Rect.Center, header, line1, line2, line3, line1Color, line2Color, line3Color);
1143  }
1144 
1145  bool draggingReport = GameMain.GameSession?.CrewManager?.DraggedOrderPrefab != null;
1146  // When setting the colors we want to know the linked hulls too or else the linked hull will not realize its being hovered over and reset the border color
1147  foreach (Hull linkedHull in hullData.LinkedHulls)
1148  {
1149  if (!hullStatusComponents.ContainsKey(linkedHull)) { continue; }
1150 
1151  isHoveringOver |=
1152  canHoverOverHull &&
1153  (hullStatusComponents[linkedHull].RectComponent == GUI.MouseOn || (draggingReport && hullStatusComponents[linkedHull].RectComponent.MouseRect.Contains(PlayerInput.MousePosition)));
1154  if (isHoveringOver) { break; }
1155  }
1156 
1157  if (isHoveringOver || (draggingReport && component.MouseRect.Contains(PlayerInput.MousePosition)))
1158  {
1159  borderColor = Color.Lerp(borderColor, Color.White, 0.5f);
1160  componentColor = HoverColor;
1161  }
1162  else
1163  {
1164  componentColor = neutralColor * 0.8f;
1165  }
1166 
1167  borderComponent.OutlineColor = borderColor;
1168  component.Color = componentColor;
1169  }
1170  }
1171 
1172  private void UpdateElectricalView()
1173  {
1174  foreach (var (entity, miniMapGuiComponent) in electricalMapComponents)
1175  {
1176  if (entity is not Item it) { continue; }
1177  if (!electricalChildren.TryGetValue(miniMapGuiComponent, out GUIComponent? component)) { continue; }
1178 
1179  if (entity.Removed)
1180  {
1181  component.Visible = false;
1182  continue;
1183  }
1184 
1185  if (item.Submarine == null || !hasPower)
1186  {
1187  component.Color = component.OutlineColor = NoPowerElectricalColor;
1188  }
1189 
1190  if (Voltage < MinVoltage || !miniMapGuiComponent.RectComponent.Visible) { continue; }
1191 
1192  int durability = (int)(it.Condition / (it.MaxCondition / it.MaxRepairConditionMultiplier) * 100f);
1193  Color color = ToolBox.GradientLerp(durability / 100f, GUIStyle.Red, GUIStyle.Orange, GUIStyle.Green, GUIStyle.Green);
1194 
1195  if (GUI.MouseOn == component)
1196  {
1197  LocalizedString line1 = string.Empty;
1198  LocalizedString line2 = string.Empty;
1199 
1200  if (it.GetComponent<PowerContainer>() is { } battery)
1201  {
1202  int batteryCapacity = (int)(battery.Charge / battery.GetCapacity() * 100f);
1203  line2 = TextManager.GetWithVariable("statusmonitor.battery.tooltip", "[amount]", batteryCapacity.ToString());
1204  }
1205  else if (it.GetComponent<PowerTransfer>() is { } powerTransfer)
1206  {
1207  int current = 0, load = 0;
1208  if (powerTransfer.PowerConnections.Count > 0 && powerTransfer.PowerConnections[0].Grid != null)
1209  {
1210  current = (int)powerTransfer.PowerConnections[0].Grid.Power;
1211  load = (int)powerTransfer.PowerConnections[0].Grid.Load;
1212  }
1213 
1214  line1 = TextManager.GetWithVariable("statusmonitor.junctionpower.tooltip", "[amount]", current.ToString())
1215  .Fallback(TextManager.GetWithVariable("statusmonitor.junctioncurrent.tooltip", "[amount]", current.ToString()));
1216  line2 = TextManager.GetWithVariables("statusmonitor.junctionload.tooltip",
1217  ("[amount]", load.ToString()),
1218  ("[load]", load.ToString()));
1219  }
1220 
1221  LocalizedString line3 = TextManager.GetWithVariable("statusmonitor.durability.tooltip", "[amount]", durability.ToString());
1222  SetTooltip(component.Rect.Center, it.Prefab.Name, line1, line2, line3, line3Color: color);
1223  color = HoverColor;
1224  }
1225 
1226  component.Color = component.OutlineColor = color;
1227  }
1228  }
1229 
1230  private void DrawHUDBack(SpriteBatch spriteBatch, GUICustomComponent container)
1231  {
1232  if (item.Submarine == null) { return; }
1233 
1234  DrawSubmarine(spriteBatch);
1235 
1236  if (Voltage < MinVoltage) { return; }
1237  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
1238  spriteBatch.End();
1239  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
1240  spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect;
1241 
1242  if (currentMode == MiniMapMode.ItemFinder)
1243  {
1244  if (MiniMapBlips != null)
1245  {
1246  foreach (Vector2 blip in MiniMapBlips)
1247  {
1248  Vector2 parentSize = miniMapFrame.Rect.Size.ToVector2();
1249  Sprite? pingCircle = GUIStyle.PingCircle.Value?.Sprite;
1250  if (pingCircle is null) { continue; }
1251  Vector2 targetSize = new Vector2(parentSize.X / 4f);
1252  Vector2 spriteScale = targetSize / pingCircle.size;
1253  float scale = Math.Min(blipState, maxBlipState / 2f);
1254  float alpha = 1.0f - Math.Clamp((blipState - maxBlipState * 0.25f) * 2f, 0f, 1f);
1255  pingCircle.Draw(spriteBatch, electricalFrame.Rect.Location.ToVector2() + blip * Zoom, GUIStyle.Red * alpha, pingCircle.Origin, 0f, spriteScale * scale, SpriteEffects.None);
1256  }
1257  }
1258  }
1259  else
1260  {
1261  bool hullsVisible = currentMode == MiniMapMode.HullStatus && item.Submarine != null;
1262 
1263  if (hullStatusComponents != null)
1264  {
1265  foreach (var (entity, component) in hullStatusComponents)
1266  {
1267  if (entity is not Hull hull) { continue; }
1268  if (!hullDatas.TryGetValue(hull, out HullData? hullData) || hullData is null) { continue; }
1269 
1270  if (hullData.Distort) { continue; }
1271 
1272  GUIComponent hullFrame = component.RectComponent;
1273 
1274  if (hullsVisible && hullData.HullWaterAmount is { } waterAmount)
1275  {
1276  if (!RequireWaterDetectors) { waterAmount = WaterDetector.GetWaterPercentage(hull); }
1277  waterAmount /= 100.0f;
1278  if (hullFrame.Rect.Height * waterAmount > 1.0f)
1279  {
1280  RectangleF waterRect = new RectangleF(hullFrame.Rect.X, hullFrame.Rect.Y + hullFrame.Rect.Height * (1.0f - waterAmount), hullFrame.Rect.Width, hullFrame.Rect.Height * waterAmount);
1281 
1282  const float width = 1f;
1283 
1284  GUI.DrawFilledRectangle(spriteBatch, waterRect, HullWaterColor);
1285 
1286  if (!MathUtils.NearlyEqual(waterAmount, 1.0f))
1287  {
1288  Vector2 offset = new Vector2(0, width);
1289  GUI.DrawLine(spriteBatch, waterRect.Location + offset, new Vector2(waterRect.Right, waterRect.Y) + offset, HullWaterLineColor, width: width);
1290  }
1291  }
1292  }
1293 
1294  if (hullsVisible && hullData.HullOxygenAmount is { } oxygenAmount)
1295  {
1296  GUI.DrawRectangle(spriteBatch, hullFrame.Rect, Color.Lerp(GUIStyle.Red * 0.5f, GUIStyle.Green * 0.3f, oxygenAmount / 100.0f), true);
1297  }
1298  }
1299  }
1300  }
1301 
1302  spriteBatch.End();
1303  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
1304  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
1305  }
1306 
1307  private void SetTooltip(Point pos, LocalizedString header, LocalizedString line1, LocalizedString line2, LocalizedString line3, Color? line1Color = null, Color? line2Color = null, Color? line3Color = null)
1308  {
1309  if (hullInfoFrame == null) { return; }
1310  hullInfoFrame.RectTransform.ScreenSpaceOffset = pos;
1311 
1312  if (hullInfoFrame.Rect.Left > submarineContainer.Rect.Right) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(submarineContainer.Rect.Right, hullInfoFrame.RectTransform.ScreenSpaceOffset.Y); }
1313  if (hullInfoFrame.Rect.Top > submarineContainer.Rect.Bottom) { hullInfoFrame.RectTransform.ScreenSpaceOffset = new Point(hullInfoFrame.RectTransform.ScreenSpaceOffset.X, submarineContainer.Rect.Bottom); }
1314 
1315  if (hullInfoFrame.Rect.Right > GameMain.GraphicsWidth) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(hullInfoFrame.Rect.Width, 0); }
1316  if (hullInfoFrame.Rect.Bottom > GameMain.GraphicsHeight) { hullInfoFrame.RectTransform.ScreenSpaceOffset -= new Point(0, hullInfoFrame.Rect.Height); }
1317 
1318  hullInfoFrame.Visible = true;
1319  tooltipHeader.Text = header;
1320 
1321  tooltipFirstLine.Text = line1;
1322  tooltipFirstLine.TextColor = line1Color ?? GUIStyle.TextColorNormal;
1323 
1324  tooltipSecondLine.Text = line2;
1325  tooltipSecondLine.TextColor = line2Color ?? GUIStyle.TextColorNormal;
1326 
1327  tooltipThirdLine.Text = line3;
1328  tooltipThirdLine.TextColor = line3Color ?? GUIStyle.TextColorNormal;
1329  }
1330 
1331  private void BakeSubmarine(Submarine sub, Rectangle container)
1332  {
1333  submarinePreview?.Dispose();
1334  Rectangle parentRect = new Rectangle(container.X, container.Y, container.Width, container.Height);
1335  const int inflate = 128;
1336  parentRect.Inflate(inflate, inflate);
1337  RenderTarget2D rt = new RenderTarget2D(GameMain.Instance.GraphicsDevice, parentRect.Width, parentRect.Height, false, SurfaceFormat.Color, DepthFormat.None);
1338 
1339  using SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice);
1340  GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
1341  GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
1342  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
1343  Rectangle worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
1344  worldBorders.Location += sub.WorldPosition.ToPoint();
1345 
1346  parentRect.Inflate(-inflate, -inflate);
1347 
1348  foreach (MapEntity entity in subEntities)
1349  {
1350  if (entity is Structure wall)
1351  {
1352  if (wall.IsPlatform) { continue; }
1353  DrawStructure(spriteBatch, wall, parentRect, worldBorders, inflate);
1354  }
1355 
1356  if (entity is Item it)
1357  {
1358  if (it.GetComponent<Pickable>() != null || it.ParentInventory != null) { continue; }
1359  DrawItem(spriteBatch, it, parentRect, worldBorders, inflate);
1360  }
1361  }
1362 
1363  spriteBatch.End();
1364  GameMain.Instance.GraphicsDevice.SetRenderTarget(null);
1365  submarinePreview = rt;
1366  }
1367 
1368  private void DrawSubmarine(SpriteBatch spriteBatch)
1369  {
1370  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
1371  spriteBatch.End();
1372  if (submarinePreview is { } texture && miniMapContainer is { } mapContainer)
1373  {
1374  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, blendState: BlendState.NonPremultiplied, effect: GameMain.GameScreen.BlueprintEffect, rasterizerState: GameMain.ScissorTestEnable);
1375  spriteBatch.GraphicsDevice.ScissorRectangle = submarineContainer.Rect;
1376 
1377  GameMain.GameScreen.BlueprintEffect.Parameters["width"].SetValue((float)texture.Width);
1378  GameMain.GameScreen.BlueprintEffect.Parameters["height"].SetValue((float)texture.Height);
1379 
1380  Color blueprintBlue = BlueprintBlue * currentMode switch { MiniMapMode.HullStatus => 0.1f, MiniMapMode.ElectricalView => 0.1f, _ => 0.5f };
1381 
1382  Vector2 origin = new Vector2(texture.Width / 2f, texture.Height / 2f);
1383  float scale = currentMode == MiniMapMode.HullStatus ? 1.0f : Zoom;
1384  spriteBatch.Draw(texture, mapContainer.Center, null, blueprintBlue, 0f, origin, scale, SpriteEffects.None, 0f);
1385 
1386  spriteBatch.End();
1387  }
1388  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
1389  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
1390  }
1391 
1392  private static void DrawItem(ISpriteBatch spriteBatch, Item item, Rectangle parent, Rectangle border, int inflate)
1393  {
1394  Sprite sprite = item.Sprite;
1395  if (sprite is null) { return; }
1396 
1397  RectangleF entityRect = ScaleRectToUI(item, parent, border);
1398 
1399  Vector2 spriteScale = new Vector2(entityRect.Size.X / sprite.size.X, entityRect.Size.Y / sprite.size.Y);
1400  Vector2 origin = new Vector2(sprite.Origin.X * spriteScale.X, sprite.Origin.Y * spriteScale.Y);
1401 
1402  if (!item.Prefab.ShowInStatusMonitor && item.GetComponent<Turret>() is { } turret)
1403  {
1404  Vector2 drawPos = turret.GetDrawPos();
1405  drawPos.Y = -drawPos.Y;
1406  if (turret.BarrelSprite is { } barrelSprite)
1407  {
1408  DrawAdditionalSprite(drawPos, barrelSprite, turret.Rotation + MathHelper.PiOver2);
1409  }
1410  }
1411 
1412  Vector2 pos = entityRect.Location + origin;
1413  pos.X += inflate;
1414  pos.Y += inflate;
1415 
1416  sprite.Draw(spriteBatch, pos, item.SpriteColor, sprite.Origin, item.RotationRad, spriteScale, item.SpriteEffects);
1417 
1418  void DrawAdditionalSprite(Vector2 basePos, Sprite addSprite, float rotation)
1419  {
1420  RectangleF addRect = ScaleRectToUI(new RectangleF(basePos, addSprite.size * item.Scale), parent, border);
1421  Vector2 addScale = new Vector2(addRect.Size.X / addSprite.size.X, addRect.Size.Y / addSprite.size.Y);
1422  addSprite.Draw(spriteBatch, new Vector2(addRect.Location.X + inflate, addRect.Location.Y + inflate), item.SpriteColor, addSprite.Origin, rotation, addScale, item.SpriteEffects);
1423  }
1424  }
1425 
1426  private static void DrawStructure(ISpriteBatch spriteBatch, Structure structure, Rectangle parent, Rectangle border, int inflate)
1427  {
1428  Sprite sprite = structure.Sprite;
1429  if (sprite is null) { return; }
1430 
1431  Vector2 textureOffset = structure.TextureOffset;
1432  textureOffset = new Vector2(
1433  MathUtils.PositiveModulo(-textureOffset.X, sprite.SourceRect.Width * structure.TextureScale.X * structure.Scale),
1434  MathUtils.PositiveModulo(-textureOffset.Y, sprite.SourceRect.Height * structure.TextureScale.Y * structure.Scale));
1435 
1436  RectangleF entityRect = ScaleRectToUI(structure, parent, border);
1437  Vector2 spriteScale = new Vector2(entityRect.Size.X / structure.Rect.Width, entityRect.Size.Y / structure.Rect.Height);
1438  float rotation = MathHelper.ToRadians(structure.Rotation);
1439 
1440  sprite.DrawTiled(
1441  spriteBatch: spriteBatch,
1442  position: entityRect.Location + entityRect.Size * 0.5f + (inflate, inflate),
1443  targetSize: entityRect.Size,
1444  rotation: rotation,
1445  origin: entityRect.Size * 0.5f,
1446  color: structure.SpriteColor,
1447  startOffset: textureOffset * spriteScale,
1448  textureScale: structure.TextureScale * structure.Scale * spriteScale,
1449  depth: structure.SpriteDepth,
1450  spriteEffects: sprite.effects ^ structure.SpriteEffects);
1451  }
1452 
1453  private static RectangleF ScaleRectToUI(MapEntity entity, RectangleF parentRect, RectangleF worldBorders)
1454  {
1455  return ScaleRectToUI(entity.WorldRect, parentRect, worldBorders);
1456  }
1457 
1458  private static RectangleF ScaleRectToUI(RectangleF rect, RectangleF parentRect, RectangleF worldBorders)
1459  {
1460  RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, rect);
1461  return relativeRect.RectangleRelativeTo(parentRect, skipOffset: true);
1462  }
1463 
1464  private void DrawHullCards(SpriteBatch spriteBatch, Hull hull, HullData data, GUIComponent frame)
1465  {
1466  cardsToDraw.Clear();
1467 
1468  if (GameMain.GameSession?.CrewManager is { ActiveOrders: { } orders })
1469  {
1470  foreach (var activeOrder in orders)
1471  {
1472  Order order = activeOrder.Order;
1473  if (order is { SymbolSprite: { }, TargetEntity: Hull _ } && order.TargetEntity == hull)
1474  {
1475  cardsToDraw.Add(new MiniMapSprite(order));
1476  }
1477  }
1478  }
1479 
1480  foreach (IdCard card in data.Cards)
1481  {
1482  if (card.OwnerJob is { Icon: { }} job)
1483  {
1484  cardsToDraw.Add(new MiniMapSprite(job));
1485  }
1486  }
1487 
1488  if (!cardsToDraw.Any()) { return; }
1489 
1490  var (centerX, centerY) = frame.Center;
1491 
1492  const float padding = 8f;
1493  float totalWidth = 0f;
1494 
1495  float parentWidth = submarineContainer.Rect.Width / 24f;
1496 
1497  int i = 0;
1498  foreach (MiniMapSprite info in cardsToDraw)
1499  {
1500  if (info.Sprite is null) { continue; }
1501 
1502  float spriteSize = info.Sprite.size.X * (parentWidth / info.Sprite.size.X) + padding;
1503  if (totalWidth + spriteSize > frame.Rect.Width) { break; }
1504 
1505  totalWidth += spriteSize;
1506  i++;
1507  }
1508 
1509  if (i > 0) { totalWidth -= padding; }
1510 
1511  float adjustedCenterX = centerX - totalWidth / 2f;
1512 
1513  float offset = 0;
1514  int amount = 0;
1515 
1516  foreach (MiniMapSprite info in cardsToDraw)
1517  {
1518  Sprite? sprite = info.Sprite;
1519  if (sprite is null) { continue; }
1520  float scale = parentWidth / sprite.size.X;
1521  float spriteSize = sprite.size.X * scale;
1522  float posX = adjustedCenterX + offset;
1523 
1524  if (posX + spriteSize > frame.Rect.X + frame.Rect.Width && amount > 0)
1525  {
1526  int amountLeft = cardsToDraw.Count - amount;
1527  if (amountLeft > 0)
1528  {
1529  string text = $"+{amountLeft}"; // TODO localization
1530  var (sizeX, sizeY) = GUIStyle.SubHeadingFont.MeasureString(text); // TODO expensive, move to a global variable
1531  float maxWidth = Math.Max(sizeX, sizeY);
1532  Vector2 drawPos = new Vector2(frame.Rect.Right - sizeX, frame.Rect.Y - sizeY / 2f);
1533 
1534  UISprite? icon = GUIStyle.IconOverflowIndicator;
1535  if (icon != null)
1536  {
1537  const int iconPadding = 4;
1538  icon.Draw(spriteBatch, new Rectangle((int)drawPos.X - iconPadding, (int)drawPos.Y - iconPadding, (int)maxWidth + iconPadding * 2, (int)maxWidth + iconPadding * 2), Color.White, SpriteEffects.None);
1539  }
1540  GUI.DrawString(spriteBatch, drawPos, text, GUIStyle.TextColorNormal, font: GUIStyle.SubHeadingFont);
1541  }
1542  break;
1543  }
1544 
1545  float halfSize = spriteSize / 2f;
1546  if (i > 0) { offset += halfSize; }
1547  Vector2 pos = new Vector2(adjustedCenterX + offset, centerY);
1548  sprite.Draw(spriteBatch, pos, info.Color * 0.8f, scale: scale, origin: sprite.size / 2f);
1549  offset += halfSize + padding;
1550  amount++;
1551  }
1552  }
1553 
1554  public static void GetLinkedHulls(Hull hull, List<Hull> linkedHulls)
1555  {
1556  foreach (var linkedEntity in hull.linkedTo)
1557  {
1558  if (linkedEntity is Hull linkedHull)
1559  {
1560  if (linkedHulls.Contains(linkedHull) || linkedHull.IsHidden) { continue; }
1561  linkedHulls.Add(linkedHull);
1562  GetLinkedHulls(linkedHull, linkedHulls);
1563  }
1564  }
1565  }
1566 
1567  public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings)
1568  {
1569  return CreateMiniMap(sub, parent, settings, null, out _);
1570  }
1571 
1572  public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings, IEnumerable<MapEntity>? pointsOfInterest, out ImmutableDictionary<MapEntity, MiniMapGUIComponent> elements)
1573  {
1574  if (settings.Equals(default(MiniMapSettings)))
1575  {
1576  throw new ArgumentException($"Provided {nameof(MiniMapSettings)} is not valid, did you mean {nameof(MiniMapSettings)}.{nameof(MiniMapSettings.Default)}?", nameof(settings));
1577  }
1578 
1579  Dictionary<MapEntity, MiniMapGUIComponent> pointsOfInterestCollection = new Dictionary<MapEntity, MiniMapGUIComponent>();
1580 
1581  RectangleF worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
1582  worldBorders.Location += sub.WorldPosition;
1583 
1584  // create a container that has the same "aspect ratio" as the sub
1585  float aspectRatio = worldBorders.Width / worldBorders.Height;
1586  float parentAspectRatio = parent.Rect.Width / (float)parent.Rect.Height;
1587 
1588  const float elementPadding = 0.9f;
1589 
1590  Vector2 containerScale = parentAspectRatio > aspectRatio ? new Vector2(aspectRatio / parentAspectRatio, 1.0f) : new Vector2(1.0f, parentAspectRatio / aspectRatio);
1591 
1592  GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null);
1593 
1594  ImmutableHashSet<Submarine> connectedSubs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID).ToImmutableHashSet();
1595  ImmutableArray<Hull> hullList = ImmutableArray<Hull>.Empty;
1596  ImmutableDictionary<Hull, ImmutableArray<Hull>> combinedHulls = ImmutableDictionary<Hull, ImmutableArray<Hull>>.Empty;
1597 
1598  if (settings.CreateHullElements)
1599  {
1600  hullList = Hull.HullList.Where(IsPartofSub).ToImmutableArray();
1601  combinedHulls = CombinedHulls(hullList);
1602  }
1603 
1604  // Make components for non-linked hulls
1605  foreach (Hull hull in hullList.Where(IsStandaloneHull))
1606  {
1607  RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, hull.WorldRect);
1608 
1609  GUIFrame hullFrame = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: "ScanLines", color: settings.ElementColor)
1610  {
1611  OutlineColor = settings.ElementColor,
1612  OutlineThickness = 2,
1613  UserData = hull
1614  };
1615 
1616  pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(hullFrame));
1617  }
1618 
1619  // Make components for linked hulls
1620  foreach (var (mainHull, linkedHulls) in combinedHulls)
1621  {
1622  MiniMapHullData data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders);
1623 
1624  RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, data.Bounds);
1625 
1626  float highestY = 0f,
1627  highestX = 0f;
1628 
1629  foreach (var (r, _) in data.RectDatas)
1630  {
1631  float y = r.Y - -r.Height,
1632  x = r.X;
1633 
1634  if (y > highestY) { highestY = y; }
1635  if (x > highestX) { highestX = x; }
1636  }
1637 
1638  Dictionary<Hull, GUIFrame> hullsAndFrames = new Dictionary<Hull, GUIFrame>();
1639 
1640  foreach (var (snappredRect, hull) in data.RectDatas)
1641  {
1642  RectangleF rect = snappredRect;
1643  rect.Height = -rect.Height;
1644  rect.Y -= rect.Height;
1645 
1646  var (parentW, parentH) = hullContainer.Rect.Size.ToVector2();
1647  Vector2 size = new Vector2(rect.Width / parentW, rect.Height / parentH);
1648  Vector2 pos = new Vector2(rect.X / parentW, rect.Y / parentH);
1649 
1650  GUIFrame hullFrame = new GUIFrame(new RectTransform(size, hullContainer.RectTransform) { RelativeOffset = pos }, style: "ScanLinesSeamless", color: settings.ElementColor)
1651  {
1652  UserData = hull,
1653  UVOffset = new Vector2(highestX - rect.X, highestY - rect.Y)
1654  };
1655 
1656  hullsAndFrames.Add(hull, hullFrame);
1657  }
1658 
1659  /*
1660  * This exists because the rectangle of GUIComponents still uses Rectangle instead of RectangleF
1661  * and because of rounding sometimes it creates 1px gaps between which looks nasty so we snap
1662  * the rectangles together if they are 2 pixels apart or less.
1663  */
1664  foreach (var (hull1, frame1) in hullsAndFrames)
1665  {
1666  Rectangle rect1 = frame1.Rect;
1667  foreach (var (hull2, frame2) in hullsAndFrames)
1668  {
1669  if (hull2 == hull1) { continue; }
1670 
1671  Rectangle rect2 = frame2.Rect;
1672  Point size = frame1.RectTransform.NonScaledSize;
1673 
1674  const int treshold = 2;
1675 
1676  int diffY = rect2.Top - rect1.Bottom;
1677  int diffX = rect2.Left - rect1.Right;
1678 
1679  if (diffY <= treshold && diffY > 0)
1680  {
1681  size.Y += diffY;
1682  }
1683 
1684  if (diffX <= treshold && diffX > 0)
1685  {
1686  size.X += diffX;
1687  }
1688 
1689  frame1.RectTransform.NonScaledSize = size;
1690  }
1691  }
1692 
1693  GUICustomComponent linkedHullFrame = new GUICustomComponent(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, (spriteBatch, component) =>
1694  {
1695  foreach (List<Vector2> list in data.Polygon)
1696  {
1697  spriteBatch.DrawPolygonInner(hullContainer.Rect.Location.ToVector2(), list, component.OutlineColor, 2f);
1698  }
1699  }, (deltaTime, component) =>
1700  {
1701  if (component.Parent.Rect.Size != data.ParentSize)
1702  {
1703  data = ConstructHullPolygon(mainHull, linkedHulls, hullContainer, worldBorders);
1704  }
1705  })
1706  {
1707  UserData = hullsAndFrames.Values.ToHashSet(),
1708  OutlineColor = settings.ElementColor,
1709  CanBeFocused = false
1710  };
1711 
1712  foreach (var (hull, component) in hullsAndFrames)
1713  {
1714  pointsOfInterestCollection.Add(hull, new MiniMapGUIComponent(component, linkedHullFrame));
1715  }
1716  }
1717 
1718  if (pointsOfInterest != null)
1719  {
1720  foreach (MapEntity entity in pointsOfInterest)
1721  {
1722  RelativeEntityRect relativeRect = new RelativeEntityRect(worldBorders, entity.WorldRect);
1723 
1724  GUIFrame poiComponent = new GUIFrame(new RectTransform(relativeRect.RelativeSize, hullContainer.RectTransform) { RelativeOffset = relativeRect.RelativePosition }, style: null)
1725  {
1726  CanBeFocused = false,
1727  UserData = entity
1728  };
1729 
1730  pointsOfInterestCollection.Add(entity, new MiniMapGUIComponent(poiComponent));
1731  }
1732  }
1733 
1734  elements = pointsOfInterestCollection.ToImmutableDictionary();
1735 
1736  return hullContainer;
1737 
1738  bool IsPartofSub(MapEntity entity)
1739  {
1740  if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.IsHidden) { return false; }
1741  return sub.IsEntityFoundOnThisSub(entity, true);
1742  }
1743 
1744  bool IsStandaloneHull(Hull hull)
1745  {
1746  return !combinedHulls.ContainsKey(hull) && !combinedHulls.Values.Any(hh => hh.Contains(hull));
1747  }
1748  }
1749 
1750  private static ImmutableDictionary<Hull, ImmutableArray<Hull>> CombinedHulls(ImmutableArray<Hull> hulls)
1751  {
1752  Dictionary<Hull, HashSet<Hull>> combinedHulls = new Dictionary<Hull, HashSet<Hull>>();
1753 
1754  foreach (Hull hull in hulls)
1755  {
1756  if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; }
1757 
1758  List<Hull> linkedHulls = new List<Hull>();
1759  GetLinkedHulls(hull, linkedHulls);
1760 
1761  linkedHulls.Remove(hull);
1762 
1763  foreach (Hull linkedHull in linkedHulls)
1764  {
1765  if (!combinedHulls.ContainsKey(hull))
1766  {
1767  combinedHulls.Add(hull, new HashSet<Hull>());
1768  }
1769 
1770  combinedHulls[hull].Add(linkedHull);
1771  }
1772  }
1773 
1774  return combinedHulls.ToImmutableDictionary(pair => pair.Key, pair => pair.Value.ToImmutableArray());
1775  }
1776 
1777  private static MiniMapHullData ConstructHullPolygon(Hull mainHull, ImmutableArray<Hull> linkedHulls, GUIComponent parent, RectangleF worldBorders)
1778  {
1779  Rectangle parentRect = parent.Rect;
1780 
1781  Dictionary<Hull, Rectangle> rects = new Dictionary<Hull, Rectangle>();
1782  Rectangle worldRect = mainHull.WorldRect;
1783  worldRect.Y = -worldRect.Y;
1784 
1785  rects.Add(mainHull, worldRect);
1786 
1787  foreach (Hull hull in linkedHulls)
1788  {
1789  Rectangle rect = hull.WorldRect;
1790  rect.Y = -rect.Y;
1791 
1792  worldRect = Rectangle.Union(worldRect, rect);
1793  rects.Add(hull, rect);
1794  }
1795 
1796  worldRect.Y = -worldRect.Y;
1797 
1798  List<RectangleF> normalizedRects = new List<RectangleF>();
1799  List<Hull> hullRefs = new List<Hull>();
1800 
1801  foreach (var (hull, rect) in rects)
1802  {
1803  Rectangle wRect = rect;
1804  wRect.Y = -wRect.Y;
1805 
1806  var (posX, posY, sizeX, sizeY) = new RelativeEntityRect(worldBorders, wRect);
1807 
1808  RectangleF newRect = new RectangleF(posX * parentRect.Width, posY * parentRect.Height, sizeX * parentRect.Width, sizeY * parentRect.Height);
1809 
1810  normalizedRects.Add(newRect);
1811  hullRefs.Add(hull);
1812  }
1813 
1814  hullRefs.Reverse(); // I have no idea why this is required
1815 
1816  ImmutableArray<RectangleF> snappedRectangles = ToolBox.SnapRectangles(normalizedRects, treshold: 1);
1817 
1818  List<List<Vector2>> polygon = ToolBox.CombineRectanglesIntoShape(snappedRectangles);
1819 
1820  List<List<Vector2>> scaledPolygon = new List<List<Vector2>>();
1821 
1822  foreach (List<Vector2> list in polygon)
1823  {
1824  // scale down the polygon just a tiny bit
1825  var (polySizeX, polySizeY) = ToolBox.GetPolygonBoundingBoxSize(list);
1826  float sizeX = polySizeX - 1f,
1827  sizeY = polySizeY - 1f;
1828 
1829  scaledPolygon.Add(ToolBox.ScalePolygon(list, new Vector2(sizeX / polySizeX, sizeY / polySizeY)));
1830  }
1831 
1832  return new MiniMapHullData(scaledPolygon, worldRect, parentRect.Size, snappedRectangles, hullRefs.ToImmutableArray());
1833  }
1834 
1835  public override void ReceiveSignal(Signal signal, Connection connection)
1836  {
1837  Item source = signal.source;
1838  if (source == null || source.CurrentHull == null) { return; }
1839 
1840  Hull sourceHull = source.CurrentHull;
1841  if (!hullDatas.TryGetValue(sourceHull, out HullData? hullData))
1842  {
1843  hullData = new HullData();
1844  hullDatas.Add(sourceHull, hullData);
1845  }
1846 
1847  if (hullData.Distort) { return; }
1848 
1849  switch (connection.Name)
1850  {
1851  case "water_data_in":
1852  //cheating a bit because water detectors don't actually send the water level
1853  bool fromWaterDetector = source.GetComponent<WaterDetector>() != null;
1854  hullData.ReceivedWaterAmount = null;
1855  hullData.LastWaterDataTime = Timing.TotalTime;
1856  if (fromWaterDetector)
1857  {
1858  hullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(sourceHull);
1859  }
1860  foreach (var linked in sourceHull.linkedTo)
1861  {
1862  if (linked is not Hull linkedHull) { continue; }
1863  if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData))
1864  {
1865  linkedHullData = new HullData();
1866  hullDatas.Add(linkedHull, linkedHullData);
1867  }
1868  linkedHullData.ReceivedWaterAmount = null;
1869  if (fromWaterDetector)
1870  {
1871  linkedHullData.ReceivedWaterAmount = WaterDetector.GetWaterPercentage(linkedHull);
1872  }
1873  }
1874  break;
1875  case "oxygen_data_in":
1876  if (!float.TryParse(signal.value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float oxy))
1877  {
1878  oxy = Rand.Range(0.0f, 100.0f);
1879  }
1880  hullData.ReceivedOxygenAmount = oxy;
1881  hullData.LastOxygenDataTime = Timing.TotalTime;
1882  foreach (var linked in sourceHull.linkedTo)
1883  {
1884  if (linked is not Hull linkedHull) { continue; }
1885  if (!hullDatas.TryGetValue(linkedHull, out HullData? linkedHullData))
1886  {
1887  linkedHullData = new HullData();
1888  hullDatas.Add(linkedHull, linkedHullData);
1889  }
1890  linkedHullData.ReceivedOxygenAmount = oxy;
1891  }
1892  break;
1893  }
1894  }
1895 
1896  protected override void RemoveComponentSpecific()
1897  {
1898  base.RemoveComponentSpecific();
1899  if (searchAutoComplete != null)
1900  {
1901  searchAutoComplete.RectTransform.Parent = null;
1902  searchAutoComplete = null;
1903  }
1904  if (hullInfoFrame != null)
1905  {
1906  hullInfoFrame.RectTransform.Parent = null;
1907  hullInfoFrame = null;
1908  }
1909  }
1910  }
1911 }
Responsible for keeping track of the characters in the player crew, saving and loading their orders,...
static void CreateReportButtons(CrewManager crewManager, GUIComponent parent, IReadOnlyList< OrderPrefab > reports, bool isHorizontal)
Submarine Submarine
Definition: Entity.cs:53
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
virtual void ClearChildren()
virtual Rectangle Rect
RectTransform RectTransform
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
GUIComponent that can be used to render custom content on the UI
static int GraphicsWidth
Definition: GameMain.cs:162
static GameSession?? GameSession
Definition: GameMain.cs:88
static int GraphicsHeight
Definition: GameMain.cs:168
static readonly List< Hull > HullList
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
override void CreateGUI()
Overload this method and implement. The method is automatically called when the resolution changes.
static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings)
override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings, IEnumerable< MapEntity >? pointsOfInterest, out ImmutableDictionary< MapEntity, MiniMapGUIComponent > elements)
static readonly PrefabCollection< OrderPrefab > Prefabs
Definition: Order.cs:41
Vector2 RelativeOffset
Defined as portions of the parent size. Also the direction of the offset is relative,...
List< Item > GetItems(bool alsoFromConnectedSubs)
IEnumerable< Submarine > GetConnectedSubs()
Returns a list of all submarines that are connected to this one via docking ports,...
Rectangle GetDockedBorders(bool allowDifferentTeam=true)
Returns a rect that contains the borders of this sub and all subs docked to it, excluding outposts
bool IsEntityFoundOnThisSub(MapEntity entity, bool includingConnectedSubs, bool allowDifferentTeam=false, bool allowDifferentType=false)
CursorState
Definition: GUI.cs:40