Client LuaCsForBarotrauma
SubEditorScreen.cs
2 using Barotrauma.IO;
4 using Barotrauma.Steam;
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.Collections.Immutable;
11 using System.Linq;
12 using System.Threading;
13 using System.Xml.Linq;
14 using Barotrauma.Sounds;
15 
16 namespace Barotrauma
17 {
19  {
20  public const int MaxStructures = 2000;
21  public const int MaxWalls = 500;
22  public const int MaxItems = 5000;
23  public const int MaxLights = 600;
24  public const int MaxShadowCastingLights = 100;
25 
26  private static Submarine MainSub
27  {
28  get => Submarine.MainSub;
29  set => Submarine.MainSub = value;
30  }
31 
32  private readonly record struct LayerData(bool IsVisible = true, bool IsGrouped = false);
33 
34  public enum Mode
35  {
36  Default,
37  Wiring
38  }
39 
40  public enum WarningType
41  {
42  NoWaypoints,
43  NoHulls,
44  DisconnectedVents,
45  NoHumanSpawnpoints,
46  NoCargoSpawnpoints,
47  NoBallastTag,
48  NonLinkedGaps,
49  NoHiddenContainers,
50  InsufficientFreeConnectionsWarning,
51  StructureCount,
52  WallCount,
53  ItemCount,
54  LightCount,
55  ShadowCastingLightCount,
56  WaterInHulls,
57  LowOxygenOutputWarning,
58  TooLargeForEndGame,
59  NotEnoughContainers
60  }
61 
62  public static Vector2 MouseDragStart = Vector2.Zero;
63 
64  private readonly Point defaultPreviewImageSize = new Point(640, 368);
65 
66  private readonly Camera cam;
67  private Vector2 camTargetFocus = Vector2.Zero;
68 
69  private SubmarineInfo backedUpSubInfo;
70 
71  private readonly HashSet<ulong> publishedWorkshopItemIds = new HashSet<ulong>();
72 
73  private Point screenResolution;
74 
75  private bool lightingEnabled;
76 
77  private bool wasSelectedBefore;
78 
80  public GUIComponent showEntitiesPanel, entityCountPanel;
81  private readonly List<GUITickBox> showEntitiesTickBoxes = new List<GUITickBox>();
82  private readonly Dictionary<string, bool> hiddenSubCategories = new Dictionary<string, bool>();
83 
84  private GUITextBlock subNameLabel;
85 
86  public bool ShowThalamus { get; private set; } = true;
87 
88  private bool entityMenuOpen = true;
89  private float entityMenuOpenState = 1.0f;
90  private string lastFilter;
92  private GUITextBox entityFilterBox;
93  private GUIListBox categorizedEntityList, allEntityList;
94  private GUIButton toggleEntityMenuButton;
95 
96  public GUIButton ToggleEntityMenuButton => toggleEntityMenuButton;
97 
98  private GUITickBox defaultModeTickBox, wiringModeTickBox;
99 
100  private GUIComponent loadFrame, saveFrame;
101 
102  private GUITextBox nameBox, descriptionBox;
103 
104  private GUIButton selectedCategoryButton;
105  private GUITextBlock selectedCategoryText;
106  private readonly List<GUIButton> entityCategoryButtons = new List<GUIButton>();
107  private MapEntityCategory? selectedCategory;
108 
109  private GUIFrame hullVolumeFrame;
110 
111  private GUIFrame saveAssemblyFrame;
112  private GUIFrame snapToGridFrame;
113 
114  const int PreviouslyUsedCount = 10;
115  private GUIFrame previouslyUsedPanel;
116  private GUIListBox previouslyUsedList;
117 
118  private GUIButton visibilityButton;
119  private GUIFrame layerPanel;
120  private GUIListBox layerList;
121  private List<GUIButton> layerSpecificButtons = new List<GUIButton>();
122 
123  private GUIFrame undoBufferPanel;
124  private GUIFrame undoBufferDisclaimer;
125  private GUIListBox undoBufferList;
126 
127  private GUIDropDown linkedSubBox;
128 
129  private static GUIComponent autoSaveLabel;
130  private static int MaxAutoSaves => GameSettings.CurrentConfig.MaxAutoSaves;
131 
132  public static readonly object ItemAddMutex = new object(), ItemRemoveMutex = new object();
133 
134  public static bool TransparentWiringMode = true;
135 
136  public static bool SkipInventorySlotUpdate;
137 
138  private static object bulkItemBufferinUse;
139 
140  public static object BulkItemBufferInUse
141  {
142  get => bulkItemBufferinUse;
143  set
144  {
145  if (value != bulkItemBufferinUse && bulkItemBufferinUse != null)
146  {
147  CommitBulkItemBuffer();
148  }
149 
150  bulkItemBufferinUse = value;
151  }
152  }
153  public static List<AddOrDeleteCommand> BulkItemBuffer = new List<AddOrDeleteCommand>();
154 
155  public static List<WarningType> SuppressedWarnings = new List<WarningType>();
156 
157  public static readonly EditorImageManager ImageManager = new EditorImageManager();
158 
159  public static bool ShouldDrawGrid = false;
160 
161  //a Character used for picking up and manipulating items
162  private Character dummyCharacter;
163 
169 
173  private Item OpenedItem;
174 
178  private Vector2 oldItemPosition;
179 
184  public static readonly List<Command> Commands = new List<Command>();
185  private static int commandIndex;
186 
187  private GUIFrame wiringToolPanel;
188 
189  private Option<DateTime> editorSelectedTime;
190 
191  private GUIImage previewImage;
192  private GUILayoutGroup previewImageButtonHolder;
193 
194  private const int submarineNameLimit = 30;
195  private GUITextBlock submarineNameCharacterCount;
196 
197  private const int submarineDescriptionLimit = 500;
198  private GUITextBlock submarineDescriptionCharacterCount;
199 
200  private Mode mode;
201 
202  private Vector2 MeasurePositionStart = Vector2.Zero;
203 
204  // Prevent the mode from changing
205  private bool lockMode;
206 
207  private static bool isAutoSaving;
208 
209  private KeyOrMouse toggleEntityListBind;
210 
211  public override Camera Cam => cam;
212 
213  public bool DrawCharacterInventory => dummyCharacter != null && WiringMode;
214 
215  public static XDocument AutoSaveInfo;
216  private static readonly string autoSavePath = Path.Combine("Submarines", ".AutoSaves");
217  private static readonly string autoSaveInfoPath = Path.Combine(autoSavePath, "autosaves.xml");
218 
219  private static string GetSubDescription()
220  {
221  if (MainSub?.Info != null)
222  {
223  LocalizedString localizedDescription = TextManager.Get($"submarine.description.{MainSub.Info.Name ?? ""}");
224  if (!localizedDescription.IsNullOrEmpty()) { return localizedDescription.Value; }
225  return MainSub.Info.Description?.Value ?? "";
226  }
227  return "";
228  }
229 
230  private static LocalizedString GetTotalHullVolume()
231  {
232  return $"{TextManager.Get("TotalHullVolume")}:\n{Hull.HullList.Sum(h => h.Volume)}";
233  }
234 
235  private static LocalizedString GetSelectedHullVolume()
236  {
237  float buoyancyVol = 0.0f;
238  float selectedVol = 0.0f;
239  float neutralPercentage = SubmarineBody.NeutralBallastPercentage;
240  Hull.HullList.ForEach(h =>
241  {
242  buoyancyVol += h.Volume;
243  if (h.IsSelected)
244  {
245  selectedVol += h.Volume;
246  }
247  });
248  buoyancyVol *= neutralPercentage;
249  string retVal = $"{TextManager.Get("SelectedHullVolume")}:\n{selectedVol}";
250  if (selectedVol > 0.0f && buoyancyVol > 0.0f)
251  {
252  if (buoyancyVol / selectedVol < 1.0f)
253  {
254  retVal += $" ({TextManager.GetWithVariable("OptimalBallastLevel", "[value]", (buoyancyVol / selectedVol).ToString("0.0000"))})";
255  }
256  else
257  {
258  retVal += $" ({TextManager.Get("InsufficientBallast")})";
259  }
260  }
261  return retVal;
262  }
263 
264  public bool WiringMode => mode == Mode.Wiring;
265 
266  private static readonly Dictionary<string, LayerData> Layers = new Dictionary<string, LayerData>();
267 
269  {
270  cam = new Camera
271  {
272  MaxZoom = 10f
273  };
274  WayPoint.ShowWayPoints = false;
275  WayPoint.ShowSpawnPoints = false;
276  Hull.ShowHulls = false;
277  Gap.ShowGaps = false;
278  CreateUI();
279  }
280 
281  private void CreateUI()
282  {
283  TopPanel = new GUIFrame(new RectTransform(new Vector2(GUI.Canvas.RelativeSize.X, 0.01f), GUI.Canvas) { MinSize = new Point(0, 35) }, "GUIFrameTop");
284 
285  GUILayoutGroup paddedTopPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.8f), TopPanel.RectTransform, Anchor.Center),
286  isHorizontal: true, childAnchor: Anchor.CenterLeft)
287  {
288  RelativeSpacing = 0.005f
289  };
290 
291  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonToggleLeft")
292  {
293  ToolTip = TextManager.Get("back"),
294  OnClicked = (b, d) =>
295  {
296  var msgBox = new GUIMessageBox("", TextManager.Get("PauseMenuQuitVerificationEditor"), new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") })
297  {
298  UserData = "verificationprompt"
299  };
300  msgBox.Buttons[0].OnClicked = (yesBtn, userdata) =>
301  {
302  GUIMessageBox.CloseAll();
303  GameMain.MainMenuScreen.Select();
304  return true;
305  };
306  msgBox.Buttons[0].OnClicked += msgBox.Close;
307  msgBox.Buttons[1].OnClicked = (_, userdata) =>
308  {
309  msgBox.Close();
310  return true;
311  };
312  return true;
313  }
314  };
315 
316  new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine");
317 
318  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "OpenButton")
319  {
320  ToolTip = TextManager.Get("OpenSubButton"),
321  OnClicked = (btn, data) =>
322  {
323  saveFrame = null;
324  CreateLoadScreen();
325 
326  return true;
327  }
328  };
329 
330  new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine");
331 
332  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "SaveButton")
333  {
334  ToolTip = RichString.Rich(TextManager.Get("SaveSubButton") + "‖color:125,125,125‖\nCtrl + S‖color:end‖"),
335  OnClicked = (btn, data) =>
336  {
337 #if DEBUG
338  if (ContentPackageManager.EnabledPackages.All.Any(cp => cp != ContentPackageManager.VanillaCorePackage && cp.Files.Any(f => f is not BaseSubFile)))
339  {
340  var msgBox = new GUIMessageBox("DEBUG-ONLY WARNING", "You currently have some mods enabled. Are you sure you want to save the submarine? If the mods override any vanilla content, saving the submarine may cause unintended changes.",
341  new LocalizedString[] { "Yes, I know what I'm doing", "Cancel" });
342  msgBox.Buttons[0].OnClicked = (btn, data) =>
343  {
344  msgBox.Close();
345  loadFrame = null;
346  CreateSaveScreen();
347  return true;
348  };
349  msgBox.Buttons[1].OnClicked += msgBox.Close;
350  return false;
351  }
352 #endif
353  loadFrame = null;
354  CreateSaveScreen();
355  return true;
356  }
357  };
358 
359  new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine");
360 
361  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "TestButton")
362  {
363  ToolTip = TextManager.Get("TestSubButton"),
364  OnClicked = TestSubmarine
365  };
366 
367  new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine");
368 
369  visibilityButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "SetupVisibilityButton")
370  {
371  ToolTip = TextManager.Get("SubEditorVisibilityButton") + '\n' + TextManager.Get("SubEditorVisibilityToolTip"),
372  OnClicked = (btn, userData) =>
373  {
374  previouslyUsedPanel.Visible = false;
375  undoBufferPanel.Visible = false;
376  layerPanel.Visible = false;
378  showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height);
379  return true;
380  }
381  };
382 
383  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditorLayerButton")
384  {
385  ToolTip = TextManager.Get("editor.layer.button") + '\n' + TextManager.Get("editor.layer.tooltip"),
386  OnClicked = (btn, userData) =>
387  {
388  previouslyUsedPanel.Visible = false;
389  showEntitiesPanel.Visible = false;
390  undoBufferPanel.Visible = false;
391  layerPanel.Visible = !layerPanel.Visible;
392  layerPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height);
393  return true;
394  }
395  };
396 
397  var previouslyUsedButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "RecentlyUsedButton")
398  {
399  ToolTip = TextManager.Get("PreviouslyUsedLabel"),
400  OnClicked = (btn, userData) =>
401  {
402  showEntitiesPanel.Visible = false;
403  undoBufferPanel.Visible = false;
404  layerPanel.Visible = false;
405  previouslyUsedPanel.Visible = !previouslyUsedPanel.Visible;
406  previouslyUsedPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height);
407  return true;
408  }
409  };
410 
411  var undoBufferButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "UndoHistoryButton")
412  {
413  ToolTip = TextManager.Get("Editor.UndoHistoryButton"),
414  OnClicked = (btn, userData) =>
415  {
416  showEntitiesPanel.Visible = false;
417  previouslyUsedPanel.Visible = false;
418  layerPanel.Visible = false;
419  undoBufferPanel.Visible = !undoBufferPanel.Visible;
420  undoBufferPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(Math.Max(btn.Rect.X, entityCountPanel.Rect.Right), saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height);
421  return true;
422  }
423  };
424 
425  new GUIFrame(new RectTransform(new Vector2(0.01f, 0.9f), paddedTopPanel.RectTransform), style: "VerticalLine");
426 
427  subNameLabel = new GUITextBlock(new RectTransform(new Vector2(0.3f, 0.9f), paddedTopPanel.RectTransform, Anchor.CenterLeft),
428  TextManager.Get("unspecifiedsubfilename"), font: GUIStyle.LargeFont, textAlignment: Alignment.CenterLeft);
429 
430  linkedSubBox = new GUIDropDown(new RectTransform(new Vector2(0.15f, 0.9f), paddedTopPanel.RectTransform),
431  TextManager.Get("AddSubButton"), elementCount: 20)
432  {
433  ToolTip = TextManager.Get("AddSubToolTip")
434  };
435 
436  List<(string Name, SubmarineInfo Sub)> subs = new List<(string Name, SubmarineInfo Sub)>();
437 
438  foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
439  {
440  if (sub.Type != SubmarineType.Player) { continue; }
441  subs.Add((sub.Name, sub));
442  }
443 
444  foreach (var (name, sub) in subs.OrderBy(tuple => tuple.Name))
445  {
446  linkedSubBox.AddItem(name, sub);
447  }
448 
449  linkedSubBox.OnSelected += SelectLinkedSub;
450  linkedSubBox.OnDropped += (component, obj) =>
451  {
452  MapEntity.SelectedList.Clear();
453  return true;
454  };
455 
456  var spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null);
457  new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine");
458 
459  defaultModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "EditSubButton")
460  {
461  ToolTip = RichString.Rich(TextManager.Get("SubEditorEditingMode") + "‖color:125,125,125‖\nCtrl + 1‖color:end‖"),
462  OnSelected = tBox =>
463  {
464  if (!lockMode)
465  {
466  if (tBox.Selected) { SetMode(Mode.Default); }
467 
468  return true;
469  }
470 
471  return false;
472  }
473  };
474 
475  wiringModeTickBox = new GUITickBox(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "WiringModeButton")
476  {
477  ToolTip = RichString.Rich(TextManager.Get("WiringModeButton") + '\n' + TextManager.Get("WiringModeToolTip") + "‖color:125,125,125‖\nCtrl + 2‖color:end‖"),
478  OnSelected = tBox =>
479  {
480  if (!lockMode)
481  {
482  SetMode(tBox.Selected ? Mode.Wiring : Mode.Default);
483  return true;
484  }
485 
486  return false;
487  }
488  };
489 
490  spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null);
491  new GUIFrame(new RectTransform(new Vector2(0.1f, 0.9f), spacing.RectTransform, Anchor.Center), style: "VerticalLine");
492 
493  new GUIButton(new RectTransform(new Vector2(0.9f, 0.9f), paddedTopPanel.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "GenerateWaypointsButton")
494  {
495  ToolTip = TextManager.Get("GenerateWaypointsButton") + '\n' + TextManager.Get("GenerateWaypointsToolTip"),
496  OnClicked = (btn, userdata) =>
497  {
498  if (WayPoint.WayPointList.Any())
499  {
500  var generateWaypointsVerification = new GUIMessageBox("", TextManager.Get("generatewaypointsverification"), new[] { TextManager.Get("ok"), TextManager.Get("cancel") });
501  generateWaypointsVerification.Buttons[0].OnClicked = delegate
502  {
503  if (GenerateWaypoints())
504  {
505  GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green);
506  }
507  WayPoint.ShowWayPoints = true;
508  var matchingTickBox = showEntitiesTickBoxes?.Find(tb => tb.UserData as string == "waypoint");
509  if (matchingTickBox != null)
510  {
511  matchingTickBox.Selected = true;
512  }
513  generateWaypointsVerification.Close();
514  return true;
515  };
516  generateWaypointsVerification.Buttons[1].OnClicked = generateWaypointsVerification.Close;
517  }
518  else
519  {
520  if (GenerateWaypoints())
521  {
522  GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green);
523  }
524  WayPoint.ShowWayPoints = true;
525 
526  }
527  return true;
528  }
529  };
530 
531  spacing = new GUIFrame(new RectTransform(new Vector2(0.02f, 1.0f), paddedTopPanel.RectTransform), style: null);
532 
533  var selectedLayerText = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), paddedTopPanel.RectTransform),
534  string.Empty, textAlignment: Alignment.Center);
535  selectedLayerText.TextGetter = () =>
536  {
537  string selectedLayer = layerList.SelectedData as string;
538  if (selectedLayer != prevSelectedLayer)
539  {
540  prevSelectedLayer = selectedLayer;
541  return selectedLayer.IsNullOrEmpty() ? string.Empty : TextManager.GetWithVariable("editor.layer.editinglayer", "[layer]", selectedLayer);
542  }
543  return selectedLayerText.Text;
544  };
545 
546  TopPanel.RectTransform.MinSize = new Point(0, (int)(paddedTopPanel.RectTransform.Children.Max(c => c.MinSize.Y) / paddedTopPanel.RectTransform.RelativeSize.Y));
547  paddedTopPanel.Recalculate();
548 
549  //-----------------------------------------------
550 
551  previouslyUsedPanel = new GUIFrame(new RectTransform(new Vector2(0.1f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) })
552  {
553  Visible = false
554  };
555  previouslyUsedList = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), previouslyUsedPanel.RectTransform, Anchor.Center))
556  {
557  PlaySoundOnSelect = true,
558  ScrollBarVisible = true,
559  OnSelected = SelectPrefab
560  };
561 
562  //-----------------------------------------------
563 
564  layerPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.4f), GUI.Canvas, minSize: new Point(300, 320)))
565  {
566  Visible = false
567  };
568 
569  GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.9f), layerPanel.RectTransform, anchor: Anchor.Center));
570 
571  layerList = new GUIListBox(new RectTransform(new Vector2(1f, 0.8f), layerGroup.RectTransform))
572  {
573  ScrollBarVisible = true,
574  AutoHideScrollBar = false,
575  OnSelected = (component, userdata) =>
576  {
577  //toggling selection is not how listboxes normally work, need to do that manually here
578  SoundPlayer.PlayUISound(GUISoundType.Select);
579  if (layerList.SelectedData == userdata)
580  {
581  layerSpecificButtons.ForEach(btn => btn.Enabled = false);
582  layerList.Deselect();
583  return false;
584  }
585  else
586  {
587  layerSpecificButtons.ForEach(btn => btn.Enabled = true);
588  return true;
589  }
590  }
591  };
592 
593  GUILayoutGroup layerButtonGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), layerGroup.RectTransform));
594  GUILayoutGroup layerButtonTopGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true);
595  GUILayoutGroup layerButtonBottomGroup = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), layerButtonGroup.RectTransform), isHorizontal: true);
596 
597  GUIButton layerAddButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.newlayer"), style: "GUIButtonFreeScale")
598  {
599  OnClicked = (button, o) =>
600  {
601  CreateNewLayer(null, MapEntity.SelectedList.ToList());
602  return true;
603  }
604  };
605 
606  GUIButton layerDeleteButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), layerButtonTopGroup.RectTransform), text: TextManager.Get("editor.layer.deletelayer"), style: "GUIButtonFreeScale")
607  {
608  Enabled = false,
609  OnClicked = (button, o) =>
610  {
611  if (layerList.SelectedData is string layer)
612  {
613  RenameLayer(layer, null);
614  }
615  return true;
616  }
617  };
618  layerSpecificButtons.Add(layerDeleteButton);
619 
620  GUIButton layerRenameButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.renamelayer"), style: "GUIButtonFreeScale")
621  {
622  Enabled = false,
623  OnClicked = (button, o) =>
624  {
625  if (layerList.SelectedData is string layer)
626  {
627  GUI.PromptTextInput(TextManager.Get("editor.layer.renamelayer"), layer, newName =>
628  {
629  RenameLayer(layer, newName);
630  });
631  }
632  return true;
633  }
634  };
635  layerSpecificButtons.Add(layerRenameButton);
636 
637  GUIButton selectLayerButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), layerButtonBottomGroup.RectTransform), text: TextManager.Get("editor.layer.selectlayer"), style: "GUIButtonFreeScale")
638  {
639  Enabled = false,
640  OnClicked = (button, o) =>
641  {
642  if (layerList.SelectedData is string layer)
643  {
644  foreach (MapEntity entity in MapEntity.MapEntityList.Where(me => !me.Removed && me.Layer == layer))
645  {
646  if (entity.IsSelected) { continue; }
647  MapEntity.SelectedList.Add(entity);
648  }
649  }
650  return true;
651  }
652  };
653  layerSpecificButtons.Add(selectLayerButton);
654 
655  GUITextBlock.AutoScaleAndNormalize(layerAddButton.TextBlock, layerDeleteButton.TextBlock, layerRenameButton.TextBlock, selectLayerButton.TextBlock);
656 
657  Vector2 subPanelSize = new Vector2(0.925f, 0.9f);
658 
659  undoBufferPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.2f), GUI.Canvas) { MinSize = new Point(200, 200) })
660  {
661  Visible = false
662  };
663 
664  undoBufferList = new GUIListBox(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center))
665  {
666  PlaySoundOnSelect = true,
667  ScrollBarVisible = true,
668  OnSelected = (_, userData) =>
669  {
670  int index;
671  if (userData is Command command)
672  {
673  index = Commands.IndexOf(command);
674  }
675  else
676  {
677  index = -1;
678  }
679 
680  int diff = index- commandIndex;
681  int amount = Math.Abs(diff);
682 
683  if (diff >= 0)
684  {
685  Redo(amount + 1);
686  }
687  else
688  {
689  Undo(amount - 1);
690  }
691 
692  return true;
693  }
694  };
695 
696  undoBufferDisclaimer = new GUIFrame(new RectTransform(subPanelSize, undoBufferPanel.RectTransform, Anchor.Center), style: null)
697  {
698  Color = Color.Black,
699  Visible = false
700  };
701  new GUITextBlock(new RectTransform(Vector2.One, undoBufferDisclaimer.RectTransform, Anchor.Center), text: TextManager.Get("editor.undounavailable"), textAlignment: Alignment.Center, wrap: true, font: GUIStyle.SubHeadingFont)
702  {
703  TextColor = GUIStyle.Orange
704  };
705 
707 
708  //-----------------------------------------------
709 
710  showEntitiesPanel = new GUIFrame(new RectTransform(new Vector2(0.15f, 0.5f), GUI.Canvas)
711  {
712  MinSize = new Point(190, 0)
713  })
714  {
715  Visible = false
716  };
717 
718  GUILayoutGroup paddedShowEntitiesPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.98f), showEntitiesPanel.RectTransform, Anchor.Center))
719  {
720  Stretch = true
721  };
722 
723  var tickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowLighting"))
724  {
725  UserData = "lighting",
726  Selected = lightingEnabled,
727  OnSelected = (GUITickBox obj) =>
728  {
729  lightingEnabled = obj.Selected;
730  return true;
731  }
732  };
733  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowWalls"))
734  {
735  UserData = "wall",
736  Selected = Structure.ShowWalls,
737  OnSelected = (GUITickBox obj) => { Structure.ShowWalls = obj.Selected; return true; }
738  };
739  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowStructures"))
740  {
741  UserData = "structure",
742  Selected = Structure.ShowStructures,
743  OnSelected = (GUITickBox obj) => { Structure.ShowStructures = obj.Selected; return true; }
744  };
745  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowItems"))
746  {
747  UserData = "item",
748  Selected = Item.ShowItems,
749  OnSelected = (GUITickBox obj) => { Item.ShowItems = obj.Selected; return true; }
750  };
751  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowWires"))
752  {
753  UserData = "wire",
754  Selected = Item.ShowWires,
755  OnSelected = (GUITickBox obj) => { Item.ShowWires = obj.Selected; return true; }
756  };
757  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowWaypoints"))
758  {
759  UserData = "waypoint",
760  Selected = WayPoint.ShowWayPoints,
761  OnSelected = (GUITickBox obj) => { WayPoint.ShowWayPoints = obj.Selected; return true; }
762  };
763  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowSpawnpoints"))
764  {
765  UserData = "spawnpoint",
766  Selected = WayPoint.ShowSpawnPoints,
767  OnSelected = (GUITickBox obj) => { WayPoint.ShowSpawnPoints = obj.Selected; return true; }
768  };
769  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowLinks"))
770  {
771  UserData = "link",
772  Selected = Item.ShowLinks,
773  OnSelected = (GUITickBox obj) => { Item.ShowLinks = obj.Selected; return true; }
774  };
775  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowHulls"))
776  {
777  UserData = "hull",
778  Selected = Hull.ShowHulls,
779  OnSelected = (GUITickBox obj) => { Hull.ShowHulls = obj.Selected; return true; }
780  };
781  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("ShowGaps"))
782  {
783  UserData = "gap",
784  Selected = Gap.ShowGaps,
785  OnSelected = (GUITickBox obj) => { Gap.ShowGaps = obj.Selected; return true; },
786  };
787  showEntitiesTickBoxes.AddRange(paddedShowEntitiesPanel.Children.Select(c => c as GUITickBox));
788 
789  var subcategoryHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedShowEntitiesPanel.RectTransform), TextManager.Get("subcategories"), font: GUIStyle.SubHeadingFont);
790  subcategoryHeader.RectTransform.MinSize = new Point(0, (int)(subcategoryHeader.Rect.Height * 1.5f));
791 
792  var subcategoryList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedShowEntitiesPanel.RectTransform) { MinSize = new Point(0, showEntitiesPanel.Rect.Height / 3) });
793  List<string> availableSubcategories = new List<string>();
794  foreach (var prefab in MapEntityPrefab.List)
795  {
796  if (!string.IsNullOrEmpty(prefab.Subcategory) && !availableSubcategories.Contains(prefab.Subcategory))
797  {
798  availableSubcategories.Add(prefab.Subcategory);
799  }
800  }
801  foreach (string subcategory in availableSubcategories)
802  {
803  var tb = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.15f), subcategoryList.Content.RectTransform),
804  TextManager.Get("subcategory." + subcategory).Fallback(subcategory), font: GUIStyle.SmallFont)
805  {
806  UserData = subcategory,
807  Selected = !IsSubcategoryHidden(subcategory),
808  OnSelected = (GUITickBox obj) => { hiddenSubCategories[(string)obj.UserData] = !obj.Selected; return true; },
809  };
810  tb.TextBlock.Wrap = true;
811  }
812 
813  GUITextBlock.AutoScaleAndNormalize(subcategoryList.Content.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock));
814  foreach (GUIComponent child in subcategoryList.Content.Children)
815  {
816  if (child is GUITickBox tb && tb.TextBlock.TextSize.X > tb.TextBlock.Rect.Width * 1.25f)
817  {
818  tb.ToolTip = tb.Text;
819  tb.Text = ToolBox.LimitString(tb.Text.Value, tb.Font, (int)(tb.TextBlock.Rect.Width * 1.25f));
820  }
821  }
822 
824  new Point(
825  (int)Math.Max(showEntitiesPanel.RectTransform.NonScaledSize.X, paddedShowEntitiesPanel.RectTransform.Children.Max(c => (int)((c.GUIComponent as GUITickBox)?.TextBlock.TextSize.X ?? 0)) / paddedShowEntitiesPanel.RectTransform.RelativeSize.X),
826  (int)(paddedShowEntitiesPanel.RectTransform.Children.Sum(c => c.MinSize.Y) / paddedShowEntitiesPanel.RectTransform.RelativeSize.Y));
827  GUITextBlock.AutoScaleAndNormalize(paddedShowEntitiesPanel.Children.Where(c => c is GUITickBox).Select(c => ((GUITickBox)c).TextBlock));
828 
829  //-----------------------------------------------
830 
831  float longestTextWidth = GUIStyle.SmallFont.MeasureString(TextManager.Get("SubEditorShadowCastingLights")).X;
832  entityCountPanel = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), GUI.Canvas)
833  {
834  MinSize = new Point(Math.Max(170, (int)(longestTextWidth * 1.5f)), 0),
835  AbsoluteOffset = new Point(0, TopPanel.Rect.Height)
836  });
837 
838  GUILayoutGroup paddedEntityCountPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), entityCountPanel.RectTransform, Anchor.Center))
839  {
840  Stretch = true,
841  AbsoluteSpacing = (int)(GUI.Scale * 4)
842  };
843 
844  var itemCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Items"),
845  textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont);
846  var itemCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), itemCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight);
847  itemCount.TextGetter = () =>
848  {
849  int count = Item.ItemList.Count;
850  if (dummyCharacter?.Inventory != null)
851  {
852  count -= dummyCharacter.Inventory.AllItems.Count();
853  }
854  itemCount.TextColor = count > MaxItems ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxItems);
855  return count.ToString();
856  };
857 
858  var structureCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Structures"),
859  textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont);
860  var structureCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), structureCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight);
861  structureCount.TextGetter = () =>
862  {
863  int count = MapEntity.MapEntityList.Count - Item.ItemList.Count - Hull.HullList.Count - WayPoint.WayPointList.Count - Gap.GapList.Count;
864  structureCount.TextColor = count > MaxStructures ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, count / (float)MaxStructures);
865  return count.ToString();
866  };
867 
868  var wallCountText = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("Walls"),
869  textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont);
870  var wallCount = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), wallCountText.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight);
871  wallCount.TextGetter = () =>
872  {
873  wallCount.TextColor = Structure.WallList.Count > MaxWalls ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, Structure.WallList.Count / (float)MaxWalls);
874  return Structure.WallList.Count.ToString();
875  };
876 
877  var lightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorLights"),
878  textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont);
879  var lightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), lightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight);
880  lightCountText.TextGetter = () =>
881  {
882  int lightCount = 0;
883  foreach (Item item in Item.ItemList)
884  {
885  if (item.ParentInventory != null) { continue; }
886  lightCount += item.GetComponents<LightComponent>().Count();
887  }
888  lightCountText.TextColor = lightCount > MaxLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxLights);
889  return lightCount.ToString() + "/" + MaxLights;
890  };
891  var shadowCastingLightCountLabel = new GUITextBlock(new RectTransform(new Vector2(0.75f, 0.0f), paddedEntityCountPanel.RectTransform), TextManager.Get("SubEditorShadowCastingLights"),
892  textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, wrap: true);
893  var shadowCastingLightCountText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1.0f), shadowCastingLightCountLabel.RectTransform, Anchor.TopRight, Pivot.TopLeft), "", textAlignment: Alignment.CenterRight);
894  shadowCastingLightCountText.TextGetter = () =>
895  {
896  int lightCount = 0;
897  foreach (Item item in Item.ItemList)
898  {
899  if (item.ParentInventory != null) { continue; }
900  lightCount += item.GetComponents<LightComponent>().Count(l => l.CastShadows && !l.DrawBehindSubs);
901  }
902  shadowCastingLightCountText.TextColor = lightCount > MaxShadowCastingLights ? GUIStyle.Red : Color.Lerp(GUIStyle.Green, GUIStyle.Orange, lightCount / (float)MaxShadowCastingLights);
903  return lightCount.ToString() + "/" + MaxShadowCastingLights;
904  };
905  entityCountPanel.RectTransform.NonScaledSize =
906  new Point(
907  (int)(paddedEntityCountPanel.RectTransform.Children.Max(c => (int)((GUITextBlock) c.GUIComponent).TextSize.X / 0.75f) / paddedEntityCountPanel.RectTransform.RelativeSize.X),
908  (int)(paddedEntityCountPanel.RectTransform.Children.Sum(c => (int)(c.NonScaledSize.Y * 1.5f) + paddedEntityCountPanel.AbsoluteSpacing) / paddedEntityCountPanel.RectTransform.RelativeSize.Y));
909  //GUITextBlock.AutoScaleAndNormalize(paddedEntityCountPanel.Children.Where(c => c is GUITextBlock).Cast<GUITextBlock>());
910 
911  //-----------------------------------------------
912 
913  hullVolumeFrame = new GUIFrame(new RectTransform(new Vector2(0.15f, 2.0f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft, minSize: new Point(300, 85)) { AbsoluteOffset = new Point(entityCountPanel.Rect.Width, 0) }, "GUIToolTip")
914  {
915  Visible = false
916  };
917  GUITextBlock totalHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform), "", font: GUIStyle.SmallFont)
918  {
919  TextGetter = GetTotalHullVolume
920  };
921  GUITextBlock selectedHullVolume = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), hullVolumeFrame.RectTransform) { RelativeOffset = new Vector2(0.0f, 0.5f) }, "", font: GUIStyle.SmallFont)
922  {
923  TextGetter = GetSelectedHullVolume
924  };
925 
926  saveAssemblyFrame = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft)
927  { MinSize = new Point((int)(250 * GUI.Scale), (int)(80 * GUI.Scale)), AbsoluteOffset = new Point((int)(10 * GUI.Scale), -entityCountPanel.Rect.Height - (int)(10 * GUI.Scale)) }, "InnerFrame")
928  {
929  Visible = false
930  };
931  var saveAssemblyButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), saveAssemblyFrame.RectTransform, Anchor.Center), TextManager.Get("SaveItemAssembly"));
932  saveAssemblyButton.TextBlock.AutoScaleHorizontal = true;
933  saveAssemblyButton.OnClicked += (btn, userdata) =>
934  {
935  CreateSaveAssemblyScreen();
936  return true;
937  };
938  saveAssemblyFrame.RectTransform.MinSize = new Point(saveAssemblyFrame.Rect.Width, (int)(saveAssemblyButton.Rect.Height / saveAssemblyButton.RectTransform.RelativeSize.Y));
939 
940  snapToGridFrame = new GUIFrame(new RectTransform(new Vector2(0.08f, 0.5f), TopPanel.RectTransform, Anchor.BottomLeft, Pivot.TopLeft)
941  { MinSize = new Point((int)(250 * GUI.Scale), (int)(80 * GUI.Scale)), AbsoluteOffset = new Point((int)(10 * GUI.Scale), -saveAssemblyFrame.Rect.Height - entityCountPanel.Rect.Height - (int)(10 * GUI.Scale)) }, "InnerFrame")
942  {
943  Visible = false
944  };
945  var saveStampButton = new GUIButton(new RectTransform(new Vector2(0.9f, 0.8f), snapToGridFrame.RectTransform, Anchor.Center), TextManager.Get("subeditor.snaptogrid", "spriteeditor.snaptogrid"));
946  saveStampButton.TextBlock.AutoScaleHorizontal = true;
947  saveStampButton.OnClicked += (btn, userdata) =>
948  {
949  SnapToGrid();
950  return true;
951  };
952  snapToGridFrame.RectTransform.MinSize = new Point(snapToGridFrame.Rect.Width, (int)(saveStampButton.Rect.Height / saveStampButton.RectTransform.RelativeSize.Y));
953 
954  //Entity menu
955  //------------------------------------------------
956 
957  EntityMenu = new GUIFrame(new RectTransform(new Point(GameMain.GraphicsWidth, (int)(359 * GUI.Scale)), GUI.Canvas, Anchor.BottomRight));
958 
959  toggleEntityMenuButton = new GUIButton(new RectTransform(new Vector2(0.15f, 0.08f), EntityMenu.RectTransform, Anchor.TopCenter, Pivot.BottomCenter) { MinSize = new Point(0, 15) },
960  style: "UIToggleButtonVertical")
961  {
962  OnClicked = (btn, userdata) =>
963  {
964  entityMenuOpen = !entityMenuOpen;
965  SetMode(Mode.Default);
966  foreach (GUIComponent child in btn.Children)
967  {
968  child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically;
969  }
970  return true;
971  }
972  };
973 
974  var paddedTab = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.96f), EntityMenu.RectTransform, Anchor.BottomCenter), childAnchor: Anchor.TopCenter)
975  {
976  RelativeSpacing = 0.04f,
977  Stretch = true
978  };
979 
980  var entityMenuTop = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.13f), paddedTab.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
981  {
982  Stretch = true
983  };
984 
985  selectedCategoryButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), entityMenuTop.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CategoryButton.All")
986  {
987  CanBeFocused = false
988  };
989  selectedCategoryText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("MapEntityCategory.All"), font: GUIStyle.LargeFont);
990 
991  var filterText = new GUITextBlock(new RectTransform(new Vector2(0.1f, 1.0f), entityMenuTop.RectTransform), TextManager.Get("serverlog.filter"), font: GUIStyle.SubHeadingFont);
992  filterText.RectTransform.MaxSize = new Point((int)(filterText.TextSize.X * 1.5f), int.MaxValue);
993  entityFilterBox = new GUITextBox(new RectTransform(new Vector2(0.17f, 1.0f), entityMenuTop.RectTransform), font: GUIStyle.Font, createClearButton: true);
994  entityFilterBox.OnTextChanged += (textBox, text) =>
995  {
996  if (text == lastFilter) { return true; }
997  lastFilter = text;
998  FilterEntities(text);
999  return true;
1000  };
1001 
1002  //spacing
1003  new GUIFrame(new RectTransform(new Vector2(0.075f, 1.0f), entityMenuTop.RectTransform), style: null);
1004 
1005  entityCategoryButtons.Clear();
1006  entityCategoryButtons.Add(
1007  new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), entityMenuTop.RectTransform, scaleBasis: ScaleBasis.BothHeight), "", style: "CategoryButton.All")
1008  {
1009  OnClicked = (btn, userdata) =>
1010  {
1011  OpenEntityMenu(null);
1012  return true;
1013  }
1014  });
1015 
1016  foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory)))
1017  {
1018  if (category == MapEntityCategory.None) { continue; }
1019  entityCategoryButtons.Add(new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), entityMenuTop.RectTransform, scaleBasis: ScaleBasis.BothHeight),
1020  "", style: "CategoryButton." + category.ToString())
1021  {
1022  UserData = category,
1023  ToolTip = TextManager.Get("MapEntityCategory." + category.ToString()),
1024  OnClicked = (btn, userdata) =>
1025  {
1026  MapEntityCategory newCategory = (MapEntityCategory)userdata;
1027  OpenEntityMenu(newCategory);
1028  return true;
1029  }
1030  });
1031  }
1032  entityCategoryButtons.ForEach(b => b.RectTransform.MaxSize = new Point(b.Rect.Height));
1033 
1034  new GUIFrame(new RectTransform(new Vector2(0.8f, 0.01f), paddedTab.RectTransform), style: "HorizontalLine");
1035 
1036  var entityListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.9f), paddedTab.RectTransform), style: null);
1037  categorizedEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true);
1038  allEntityList = new GUIListBox(new RectTransform(Vector2.One, entityListContainer.RectTransform), useMouseDownToSelect: true)
1039  {
1040  OnSelected = SelectPrefab,
1041  UseGridLayout = true,
1042  CheckSelected = MapEntityPrefab.GetSelected,
1043  Visible = false,
1044  PlaySoundOnSelect = true,
1045  };
1046 
1047  paddedTab.Recalculate();
1048  UpdateLayerPanel();
1049  screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
1050  }
1051 
1052  private bool TestSubmarine(GUIButton button, object obj)
1053  {
1054  List<LocalizedString> errorMsgs = new List<LocalizedString>();
1055 
1056  if (!Hull.HullList.Any())
1057  {
1058  errorMsgs.Add(TextManager.Get("NoHullsWarning"));
1059  }
1060 
1061  if (!WayPoint.WayPointList.Any(wp => wp.ShouldBeSaved && wp.SpawnType == SpawnType.Human))
1062  {
1063  errorMsgs.Add(TextManager.Get("NoHumanSpawnpointWarning"));
1064  }
1065 
1066  if (errorMsgs.Any())
1067  {
1068  new GUIMessageBox(TextManager.Get("Error"), LocalizedString.Join("\n\n", errorMsgs), new Vector2(0.25f, 0.0f), new Point(400, 200));
1069  return true;
1070  }
1071 
1072  CloseItem();
1073 
1074  backedUpSubInfo = new SubmarineInfo(MainSub);
1075 
1076  GameSession gameSession = new GameSession(backedUpSubInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null);
1077 
1078  // if testing an outpost module, we will generate an entire outpost in place of the main submarine down the line
1079  if (backedUpSubInfo.OutpostModuleInfo != null)
1080  {
1081  gameSession.ForceOutpostModule = new SubmarineInfo(MainSub);
1082  }
1083 
1084  GameMain.GameScreen.Select();
1085 
1086  gameSession.StartRound(null, false);
1087 
1088  foreach ((string layerName, LayerData layerData) in Layers)
1089  {
1090  Identifier identifier = layerName.ToIdentifier();
1091  bool enabled = layerData.IsVisible;
1092  MainSub.SetLayerEnabled(identifier, enabled);
1093  }
1094 
1095  if (gameSession.GameMode is TestGameMode testGameMode)
1096  {
1097  testGameMode.OnRoundEnd = () =>
1098  {
1099  Submarine.Unload();
1100  GameMain.SubEditorScreen.Select();
1101  };
1102  }
1103 
1104  return true;
1105  }
1106 
1107  public void ClearBackedUpSubInfo()
1108  {
1109  backedUpSubInfo = null;
1110  }
1111 
1112  private void UpdateEntityList()
1113  {
1114  categorizedEntityList.Content.ClearChildren();
1115  allEntityList.Content.ClearChildren();
1116 
1117  int maxTextWidth = (int)(GUIStyle.SubHeadingFont.MeasureString(TextManager.Get("mapentitycategory.misc")).X + GUI.IntScale(50));
1118  Dictionary<string, List<MapEntityPrefab>> entityLists = new Dictionary<string, List<MapEntityPrefab>>();
1119  Dictionary<string, MapEntityCategory> categoryKeys = new Dictionary<string, MapEntityCategory>();
1120 
1121  foreach (MapEntityCategory category in Enum.GetValues(typeof(MapEntityCategory)))
1122  {
1123  if (category == MapEntityCategory.None) { continue; }
1124  LocalizedString categoryName = TextManager.Get("MapEntityCategory." + category);
1125  maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(categoryName.Replace(" ", "\n")).X + GUI.IntScale(50));
1126  foreach (MapEntityPrefab ep in MapEntityPrefab.List)
1127  {
1128  if (!ep.Category.HasFlag(category)) { continue; }
1129 
1130  if (!entityLists.ContainsKey(category + ep.Subcategory))
1131  {
1132  entityLists[category + ep.Subcategory] = new List<MapEntityPrefab>();
1133  }
1134  entityLists[category + ep.Subcategory].Add(ep);
1135  categoryKeys[category + ep.Subcategory] = category;
1136  LocalizedString subcategoryName = TextManager.Get("subcategory." + ep.Subcategory).Fallback(ep.Subcategory);
1137  if (subcategoryName != null)
1138  {
1139  maxTextWidth = (int)Math.Max(maxTextWidth, GUIStyle.SubHeadingFont.MeasureString(subcategoryName.Replace(" ", "\n")).X + GUI.IntScale(50));
1140  }
1141  }
1142  }
1143 
1144  categorizedEntityList.Content.ClampMouseRectToParent = true;
1145  int entitiesPerRow = (int)Math.Ceiling(categorizedEntityList.Content.Rect.Width / Math.Max(125 * GUI.Scale, 60));
1146  foreach (string categoryKey in entityLists.Keys)
1147  {
1148  var categoryFrame = new GUIFrame(new RectTransform(Vector2.One, categorizedEntityList.Content.RectTransform), style: null)
1149  {
1150  ClampMouseRectToParent = true,
1151  UserData = categoryKeys[categoryKey]
1152  };
1153 
1154  new GUIFrame(new RectTransform(Vector2.One, categoryFrame.RectTransform), style: "HorizontalLine");
1155 
1156  LocalizedString categoryName = TextManager.Get("MapEntityCategory." + entityLists[categoryKey].First().Category);
1157  LocalizedString subCategoryName = entityLists[categoryKey].First().Subcategory;
1158  if (subCategoryName.IsNullOrEmpty())
1159  {
1160  new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft),
1161  categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true)
1162  {
1163  Padding = new Vector4(GUI.IntScale(10))
1164  };
1165 
1166  }
1167  else
1168  {
1169  subCategoryName = subCategoryName.IsNullOrEmpty() ?
1170  TextManager.Get("mapentitycategory.misc") :
1171  (TextManager.Get($"subcategory.{subCategoryName}").Fallback(subCategoryName));
1172  var categoryTitle = new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft),
1173  categoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.Font, wrap: true)
1174  {
1175  Padding = new Vector4(GUI.IntScale(10))
1176  };
1177  new GUITextBlock(new RectTransform(new Point(maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(0, (int)(categoryTitle.TextSize.Y + GUI.IntScale(10))) },
1178  subCategoryName, textAlignment: Alignment.TopLeft, font: GUIStyle.SubHeadingFont, wrap: true)
1179  {
1180  Padding = new Vector4(GUI.IntScale(10))
1181  };
1182  }
1183 
1184  var entityListInner = new GUIListBox(new RectTransform(new Point(categoryFrame.Rect.Width - maxTextWidth, categoryFrame.Rect.Height), categoryFrame.RectTransform, Anchor.CenterRight),
1185  style: null,
1186  useMouseDownToSelect: true)
1187  {
1188  ScrollBarVisible = false,
1189  AutoHideScrollBar = false,
1190  OnSelected = SelectPrefab,
1191  UseGridLayout = true,
1192  CheckSelected = MapEntityPrefab.GetSelected,
1193  ClampMouseRectToParent = true,
1194  PlaySoundOnSelect = true,
1195  };
1196  entityListInner.ContentBackground.ClampMouseRectToParent = true;
1197  entityListInner.Content.ClampMouseRectToParent = true;
1198 
1199  foreach (MapEntityPrefab ep in entityLists[categoryKey])
1200  {
1201 #if !DEBUG
1202  if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; }
1203 #endif
1204  CreateEntityElement(ep, entitiesPerRow, entityListInner.Content);
1205  }
1206 
1207  entityListInner.UpdateScrollBarSize();
1208  int contentHeight = (int)(entityListInner.TotalSize + entityListInner.Padding.Y + entityListInner.Padding.W);
1209  categoryFrame.RectTransform.NonScaledSize = new Point(categoryFrame.Rect.Width, contentHeight);
1210  categoryFrame.RectTransform.MinSize = new Point(0, contentHeight);
1211  entityListInner.RectTransform.NonScaledSize = new Point(entityListInner.Rect.Width, contentHeight);
1212  entityListInner.RectTransform.MinSize = new Point(0, contentHeight);
1213 
1214  entityListInner.Content.RectTransform.SortChildren((i1, i2) =>
1215  string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData)?.Name.Value, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name.Value, StringComparison.Ordinal));
1216  }
1217 
1218  foreach (MapEntityPrefab ep in MapEntityPrefab.List)
1219  {
1220 #if !DEBUG
1221  if ((ep.HideInMenus || ep.HideInEditors) && !GameMain.DebugDraw) { continue; }
1222 #endif
1223  CreateEntityElement(ep, entitiesPerRow, allEntityList.Content);
1224  }
1225  allEntityList.Content.RectTransform.SortChildren((i1, i2) =>
1226  string.Compare(((MapEntityPrefab)i1.GUIComponent.UserData)?.Name.Value, (i2.GUIComponent.UserData as MapEntityPrefab)?.Name.Value, StringComparison.Ordinal));
1227 
1228  }
1229 
1230  private void CreateEntityElement(MapEntityPrefab ep, int entitiesPerRow, GUIComponent parent)
1231  {
1232  bool legacy = ep.Category.HasFlag(MapEntityCategory.Legacy);
1233 
1234  float relWidth = 1.0f / entitiesPerRow;
1235  GUIFrame frame = new GUIFrame(new RectTransform(
1236  new Vector2(relWidth, relWidth * ((float)parent.Rect.Width / parent.Rect.Height)),
1237  parent.RectTransform)
1238  { MinSize = new Point(0, 50) },
1239  style: "GUITextBox")
1240  {
1241  UserData = ep,
1242  ClampMouseRectToParent = true
1243  };
1244  frame.RectTransform.MinSize = new Point(0, frame.Rect.Width);
1245  frame.RectTransform.MaxSize = new Point(int.MaxValue, frame.Rect.Width);
1246 
1247  LocalizedString name = legacy ? TextManager.GetWithVariable("legacyitemformat", "[name]", ep.Name) : ep.Name;
1248  frame.ToolTip = ep.CreateTooltipText();
1249 
1250  if (ep.IsModded)
1251  {
1252  frame.Color = Color.Magenta;
1253  }
1254 
1255  if (ep.HideInMenus || ep.HideInEditors)
1256  {
1257  frame.Color = Color.Red;
1258  name = "[HIDDEN] " + name;
1259  }
1260  frame.ToolTip = RichString.Rich(frame.ToolTip);
1261 
1262  GUILayoutGroup paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 0.8f), frame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter)
1263  {
1264  Stretch = true,
1265  RelativeSpacing = 0.03f,
1266  CanBeFocused = false
1267  };
1268 
1269  Sprite icon = ep.Sprite;
1270  Color iconColor = Color.White;
1271  if (ep is ItemPrefab itemPrefab)
1272  {
1273  if (itemPrefab.InventoryIcon != null)
1274  {
1275  icon = itemPrefab.InventoryIcon;
1276  iconColor = itemPrefab.InventoryIconColor;
1277  }
1278  else
1279  {
1280  iconColor = itemPrefab.SpriteColor;
1281  }
1282  }
1283  GUIImage img = null;
1284  if (ep.Sprite != null)
1285  {
1286  img = new GUIImage(new RectTransform(new Vector2(1.0f, 0.8f),
1287  paddedFrame.RectTransform, Anchor.TopCenter), icon)
1288  {
1289  CanBeFocused = false,
1290  LoadAsynchronously = true,
1291  SpriteEffects = icon.effects,
1292  Color = legacy ? iconColor * 0.6f : iconColor
1293  };
1294  }
1295 
1296  if (ep is ItemAssemblyPrefab itemAssemblyPrefab)
1297  {
1298  new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.75f),
1299  paddedFrame.RectTransform, Anchor.TopCenter), onDraw: (sb, customComponent) =>
1300  {
1301  if (GUIImage.LoadingTextures) { return; }
1302  itemAssemblyPrefab.DrawIcon(sb, customComponent);
1303  })
1304  {
1305  HideElementsOutsideFrame = true,
1306  ToolTip = frame.ToolTip.SanitizedString
1307  };
1308  }
1309 
1310  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter),
1311  text: name, textAlignment: Alignment.Center, font: GUIStyle.SmallFont)
1312  {
1313  CanBeFocused = false
1314  };
1315  if (legacy) { textBlock.TextColor *= 0.6f; }
1316  if (name.IsNullOrEmpty())
1317  {
1318  DebugConsole.AddWarning($"Entity \"{ep.Identifier.Value}\" has no name!",
1319  contentPackage: ep.ContentPackage);
1320  textBlock.Text = frame.ToolTip = ep.Identifier.Value;
1321  textBlock.TextColor = GUIStyle.Red;
1322  }
1323  textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width);
1324 
1325  if (ep.Category == MapEntityCategory.ItemAssembly
1326  && ep.ContentPackage?.Files.Length == 1
1327  && ContentPackageManager.LocalPackages.Contains(ep.ContentPackage))
1328  {
1329  var deleteButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform, Anchor.BottomCenter) { MinSize = new Point(0, 20) },
1330  TextManager.Get("Delete"), style: "GUIButtonSmall")
1331  {
1332  UserData = ep,
1333  OnClicked = (btn, userData) =>
1334  {
1335  ItemAssemblyPrefab assemblyPrefab = (ItemAssemblyPrefab)userData;
1336  if (assemblyPrefab != null)
1337  {
1338  var msgBox = new GUIMessageBox(
1339  TextManager.Get("DeleteDialogLabel"),
1340  TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", assemblyPrefab.Name),
1341  new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
1342  msgBox.Buttons[0].OnClicked += (deleteBtn, userData2) =>
1343  {
1344  try
1345  {
1346  assemblyPrefab.Delete();
1347  OpenEntityMenu(MapEntityCategory.ItemAssembly);
1348  }
1349  catch (Exception e)
1350  {
1351  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", assemblyPrefab.Name), e);
1352  }
1353  return true;
1354  };
1355  msgBox.Buttons[0].OnClicked += msgBox.Close;
1356  msgBox.Buttons[1].OnClicked += msgBox.Close;
1357  }
1358 
1359  return true;
1360  }
1361  };
1362  }
1363  paddedFrame.Recalculate();
1364  if (img != null)
1365  {
1366  img.Scale = Math.Min(Math.Min(img.Rect.Width / img.Sprite.size.X, img.Rect.Height / img.Sprite.size.Y), 1.5f);
1367  img.RectTransform.NonScaledSize = new Point((int)(img.Sprite.size.X * img.Scale), img.Rect.Height);
1368  }
1369  }
1370 
1371  public override void Select()
1372  {
1373  Select(enableAutoSave: true);
1374 
1376  }
1377 
1378  public void Select(bool enableAutoSave = true)
1379  {
1380  base.Select();
1381 
1382  TaskPool.Add(
1383  $"DeterminePublishedItemIds",
1384  SteamManager.Workshop.GetPublishedItems(),
1385  t =>
1386  {
1387  if (!t.TryGetResult(out ISet<Steamworks.Ugc.Item> items)) { return; }
1388 
1389  publishedWorkshopItemIds.Clear();
1390  publishedWorkshopItemIds.UnionWith(items.Select(it => it.Id.Value));
1391  });
1392 
1393  GUI.PreventPauseMenuToggle = false;
1394  if (!Directory.Exists(autoSavePath))
1395  {
1396  if (Directory.CreateDirectory(autoSavePath, catchUnauthorizedAccessExceptions: true) is { Exists: true } directoryInfo)
1397  {
1398  directoryInfo.Attributes = System.IO.FileAttributes.Directory | System.IO.FileAttributes.Hidden;
1399  }
1400  else
1401  {
1402  DebugConsole.ThrowError("Failed to create auto save directory!");
1403  }
1404  }
1405 
1406  if (!File.Exists(autoSaveInfoPath))
1407  {
1408  try
1409  {
1410  AutoSaveInfo = new XDocument(new XElement("AutoSaves"));
1411  AutoSaveInfo.SaveSafe(autoSaveInfoPath, throwExceptions: true);
1412  }
1413  catch (Exception e)
1414  {
1415  DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e);
1416  }
1417  }
1418  else
1419  {
1420  AutoSaveInfo = XMLExtensions.TryLoadXml(autoSaveInfoPath);
1421  }
1422 
1423  GameMain.LightManager.AmbientLight =
1424  Level.Loaded?.GenerationParams?.AmbientLightColor ??
1425  new Color(3, 3, 3, 3);
1426 
1427  isAutoSaving = false;
1428 
1429  if (!wasSelectedBefore)
1430  {
1431  OpenEntityMenu(null);
1432  wasSelectedBefore = true;
1433  }
1434  else
1435  {
1436  OpenEntityMenu(selectedCategory);
1437  }
1438 
1439  if (backedUpSubInfo != null)
1440  {
1441  Submarine.Unload();
1442  }
1443 
1444  string name = (MainSub == null) ? TextManager.Get("unspecifiedsubfilename").Value : MainSub.Info.Name;
1445  if (backedUpSubInfo != null) { name = backedUpSubInfo.Name; }
1446  subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width);
1447 
1448  editorSelectedTime = Option<DateTime>.Some(DateTime.Now);
1449 
1450  GUI.ForceMouseOn(null);
1451  SetMode(Mode.Default);
1452 
1453  if (backedUpSubInfo != null)
1454  {
1455  MainSub = new Submarine(backedUpSubInfo);
1456  if (previewImage != null && backedUpSubInfo.PreviewImage?.Texture != null && !backedUpSubInfo.PreviewImage.Texture.IsDisposed)
1457  {
1458  previewImage.Sprite = backedUpSubInfo.PreviewImage;
1459  }
1460  backedUpSubInfo = null;
1461  }
1462  else if (MainSub == null)
1463  {
1464  var subInfo = new SubmarineInfo();
1465  MainSub = new Submarine(subInfo, showErrorMessages: false);
1466  ReconstructLayers();
1467  }
1468 
1469  MainSub.UpdateTransform(interpolate: false);
1470  cam.Position = MainSub.Position + MainSub.HiddenSubPosition;
1471 
1472  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, 0.0f);
1473  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, 0.0f);
1474 
1475  string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
1476  linkedSubBox.ClearChildren();
1477 
1478  List<(string Name, SubmarineInfo Sub)> subs = new List<(string Name, SubmarineInfo Sub)>();
1479 
1480  foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
1481  {
1482  if (sub.Type != SubmarineType.Player) { continue; }
1483  if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; }
1484  subs.Add((sub.Name, sub));
1485  }
1486 
1487  foreach (var (subName, sub) in subs.OrderBy(tuple => tuple.Name))
1488  {
1489  linkedSubBox.AddItem(subName, sub);
1490  }
1491 
1492  cam.UpdateTransform();
1493 
1494  CreateDummyCharacter();
1495 
1496  if (GameSettings.CurrentConfig.EnableSubmarineAutoSave && enableAutoSave)
1497  {
1498  CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave");
1499  }
1500 
1501  ImageManager.OnEditorSelected();
1502  if (Layers.None())
1503  {
1504  ReconstructLayers();
1505  }
1506  }
1507 
1508  public override void OnFileDropped(string filePath, string extension)
1509  {
1510  switch (extension)
1511  {
1512  case ".sub": // Submarine
1513  SubmarineInfo info = new SubmarineInfo(filePath);
1514  if (info.IsFileCorrupted)
1515  {
1516  DebugConsole.ThrowError($"Could not drag and drop the file. File \"{filePath}\" is corrupted!");
1517  info.Dispose();
1518  return;
1519  }
1520 
1521  LocalizedString body = TextManager.GetWithVariable("SubEditor.LoadConfirmBody", "[submarine]", info.Name);
1522  GUI.AskForConfirmation(TextManager.Get("Load"), body, onConfirm: () => LoadSub(info), onDeny: () => info.Dispose());
1523  break;
1524 
1525  case ".xml": // Item Assembly
1526  string text = File.ReadAllText(filePath);
1527  // PlayerInput.MousePosition doesn't update while the window is not active so we need to use this method
1528  Vector2 mousePos = Mouse.GetState().Position.ToVector2();
1529  PasteAssembly(text, cam.ScreenToWorld(mousePos));
1530  break;
1531 
1532  case ".png": // submarine preview
1533  case ".jpg":
1534  case ".jpeg":
1535  if (saveFrame == null) { break; }
1536 
1537  Texture2D texture = Sprite.LoadTexture(filePath, compress: false);
1538  previewImage.Sprite = new Sprite(texture, null, null);
1539  if (MainSub != null)
1540  {
1541  MainSub.Info.PreviewImage = previewImage.Sprite;
1542  }
1543 
1544  break;
1545 
1546  default:
1547  DebugConsole.ThrowError($"Could not drag and drop the file. \"{extension}\" is not a valid file extension! (expected .xml, .sub, .png or .jpg)");
1548  break;
1549  }
1550  }
1551 
1557  private static IEnumerable<CoroutineStatus> AutoSaveCoroutine()
1558  {
1559  DateTime target = DateTime.Now.AddSeconds(GameSettings.CurrentConfig.AutoSaveIntervalSeconds);
1560  DateTime tempTarget = DateTime.Now;
1561 
1562  bool wasPaused = false;
1563 
1564  while (DateTime.Now < target && Selected is SubEditorScreen || GameMain.Instance.Paused || wasPaused)
1565  {
1566  if (GameMain.Instance.Paused && !wasPaused)
1567  {
1568  AutoSave();
1569  tempTarget = DateTime.Now;
1570  wasPaused = true;
1571  }
1572 
1573  if (!GameMain.Instance.Paused && wasPaused)
1574  {
1575  wasPaused = false;
1576  target = target.AddSeconds((DateTime.Now - tempTarget).TotalSeconds);
1577  }
1578  yield return CoroutineStatus.Running;
1579  }
1580 
1581  if (Selected is SubEditorScreen)
1582  {
1583  AutoSave();
1584  CoroutineManager.StartCoroutine(AutoSaveCoroutine(), "SubEditorAutoSave");
1585  }
1586  yield return CoroutineStatus.Success;
1587  }
1588 
1589  protected override void DeselectEditorSpecific()
1590  {
1591  CloseItem();
1592 
1593  autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel);
1594  autoSaveLabel = null;
1595 
1596  if (editorSelectedTime.TryUnwrap(out DateTime selectedTime))
1597  {
1598  TimeSpan timeInEditor = DateTime.Now - selectedTime;
1599  //this is intended for diagnosing why the "x hours in editor" achievement seems to sometimes trigger too soon
1600  //require the time in editor to be x1.5 higher to disregard any rounding errors or discrepancies in Datetime.Now and the game's own timekeeping
1601  if (timeInEditor.TotalSeconds > Timing.TotalTime * 1.5)
1602  {
1603  DebugConsole.ThrowErrorAndLogToGA(
1604  "SubEditorScreen.DeselectEditorSpecific:InvalidTimeInEditor",
1605  $"Error in sub editor screen. Calculated time in editor {timeInEditor} was larger than the time the game has run ({Timing.TotalTime} s).");
1606  }
1607  else
1608  {
1609  AchievementManager.IncrementStat(AchievementStat.HoursInEditor, (float)timeInEditor.TotalHours);
1610  editorSelectedTime = Option<DateTime>.None();
1611  }
1612  }
1613 
1614  GUI.ForceMouseOn(null);
1615 
1616  if (ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); }
1617 
1618  MapEntityPrefab.Selected = null;
1619 
1620  saveFrame = null;
1621  loadFrame = null;
1622 
1624  ClearUndoBuffer();
1625 
1626 #if !DEBUG
1627  DebugConsole.DeactivateCheats();
1628 #endif
1629 
1630  SetMode(Mode.Default);
1631 
1632  SoundPlayer.OverrideMusicType = Identifier.Empty;
1633  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, GameSettings.CurrentConfig.Audio.SoundVolume);
1634  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, GameSettings.CurrentConfig.Audio.SoundVolume);
1635 
1636  if (CoroutineManager.IsCoroutineRunning("SubEditorAutoSave"))
1637  {
1638  CoroutineManager.StopCoroutines("SubEditorAutoSave");
1639  }
1640 
1641  if (dummyCharacter != null)
1642  {
1643  dummyCharacter.Remove();
1644  dummyCharacter = null;
1645  GameMain.World.ProcessChanges();
1646  }
1647 
1648  GUIMessageBox.MessageBoxes.ForEachMod(component =>
1649  {
1650  if (component is GUIMessageBox { Closed: false, UserData: "colorpicker" } msgBox)
1651  {
1652  foreach (GUIColorPicker colorPicker in msgBox.GetAllChildren<GUIColorPicker>())
1653  {
1654  colorPicker.Dispose();
1655  }
1656 
1657  msgBox.Close();
1658  }
1659  });
1660 
1661  ClearFilter();
1662  }
1663 
1664  private void CreateDummyCharacter()
1665  {
1666  if (dummyCharacter != null) { RemoveDummyCharacter(); }
1667 
1668  dummyCharacter = Character.Create(CharacterPrefab.HumanSpeciesName, Vector2.Zero, "", id: Entity.DummyID, hasAi: false);
1669  dummyCharacter.Info.Name = "Galldren";
1670 
1671  //make space for the entity menu
1672  for (int i = 0; i < dummyCharacter.Inventory.SlotPositions.Length; i++)
1673  {
1674  if (CharacterInventory.PersonalSlots.HasFlag(dummyCharacter.Inventory.SlotTypes[i])) { continue; }
1675  if (dummyCharacter.Inventory.SlotPositions[i].Y > GameMain.GraphicsHeight / 2)
1676  {
1677  dummyCharacter.Inventory.SlotPositions[i].Y -= 50 * GUI.Scale;
1678  }
1679  }
1680  dummyCharacter.Inventory.CreateSlots();
1681 
1682  Character.Controlled = dummyCharacter;
1683  GameMain.World.ProcessChanges();
1684  }
1685 
1691  private static void AutoSave()
1692  {
1693  if (MapEntity.MapEntityList.Any() && GameSettings.CurrentConfig.EnableSubmarineAutoSave && !isAutoSaving)
1694  {
1695  if (MainSub != null)
1696  {
1697  isAutoSaving = true;
1698  if (!Directory.Exists(autoSavePath)) { return; }
1699 
1700  XDocument doc = new XDocument(new XElement("Submarine"));
1701  MainSub.SaveToXElement(doc.Root);
1702  Thread saveThread = new Thread(start =>
1703  {
1704  try
1705  {
1706  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
1707  TimeSpan time = DateTime.UtcNow - DateTime.MinValue;
1708  string filePath = Path.Combine(autoSavePath, $"AutoSave_{(ulong)time.TotalMilliseconds}.sub");
1709  SaveUtil.CompressStringToFile(filePath, doc.ToString());
1710 
1711  CrossThread.RequestExecutionOnMainThread(() =>
1712  {
1713  if (AutoSaveInfo?.Root == null || MainSub?.Info == null) { return; }
1714 
1715  int saveCount = AutoSaveInfo.Root.Elements().Count();
1716  while (AutoSaveInfo.Root.Elements().Count() > MaxAutoSaves)
1717  {
1718  XElement min = AutoSaveInfo.Root.Elements().OrderBy(element => element.GetAttributeUInt64("time", 0)).FirstOrDefault();
1719  #warning TODO: revise
1720  string path = min.GetAttributeStringUnrestricted("file", "");
1721  if (string.IsNullOrWhiteSpace(path)) { continue; }
1722 
1723  if (IO.File.Exists(path)) { IO.File.Delete(path); }
1724  min?.Remove();
1725  }
1726 
1727  XElement newElement = new XElement("AutoSave",
1728  new XAttribute("file", filePath),
1729  new XAttribute("name", MainSub.Info.Name),
1730  new XAttribute("time", (ulong)time.TotalSeconds));
1731  AutoSaveInfo.Root.Add(newElement);
1732 
1733  try
1734  {
1735  IO.SafeXML.SaveSafe(AutoSaveInfo, autoSaveInfoPath);
1736  }
1737  catch (Exception e)
1738  {
1739  DebugConsole.ThrowError("Saving auto save info to \"" + autoSaveInfoPath + "\" failed!", e);
1740  }
1741  });
1742 
1743  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
1744  CrossThread.RequestExecutionOnMainThread(DisplayAutoSavePrompt);
1745  }
1746  catch (Exception e)
1747  {
1748  CrossThread.RequestExecutionOnMainThread(() => DebugConsole.ThrowError("Auto saving submarine failed!", e));
1749  }
1750  isAutoSaving = false;
1751  }) { Name = "Auto Save Thread" };
1752  saveThread.Start();
1753  }
1754  }
1755  }
1756 
1757  private static void DisplayAutoSavePrompt()
1758  {
1759  if (Selected != GameMain.SubEditorScreen) { return; }
1760  autoSaveLabel?.Parent?.RemoveChild(autoSaveLabel);
1761 
1762  LocalizedString label = TextManager.Get("AutoSaved");
1763  autoSaveLabel = new GUILayoutGroup(new RectTransform(new Point(GUI.IntScale(150), GUI.IntScale(32)), GameMain.SubEditorScreen.EntityMenu.RectTransform, Anchor.TopRight)
1764  {
1765  ScreenSpaceOffset = new Point(-GUI.IntScale(16), -GUI.IntScale(48))
1766  }, isHorizontal: true)
1767  {
1768  CanBeFocused = false
1769  };
1770 
1771  GUIImage checkmark = new GUIImage(new RectTransform(new Vector2(0.25f, 1f), autoSaveLabel.RectTransform), style: "MissionCompletedIcon", scaleToFit: true);
1772  GUITextBlock labelComponent = new GUITextBlock(new RectTransform(new Vector2(0.75f, 1f), autoSaveLabel.RectTransform), label, font: GUIStyle.SubHeadingFont, color: GUIStyle.Green)
1773  {
1774  Padding = Vector4.Zero,
1775  AutoScaleHorizontal = true,
1776  AutoScaleVertical = true
1777  };
1778 
1779  labelComponent.FadeOut(0.5f, true, 1f);
1780  checkmark.FadeOut(0.5f, true, 1f);
1781  autoSaveLabel?.FadeOut(0.5f, true, 1f);
1782  }
1783 
1784  private bool SaveSub(ContentPackage packageToSaveTo)
1785  {
1786  void handleExceptions(Action action)
1787  {
1788  try
1789  {
1790  action();
1791  }
1792  catch (Exception e)
1793  {
1794  DebugConsole.ThrowError($"An error occurred while trying to save {nameBox.Text}", e, createMessageBox: true);
1795  }
1796  }
1797 
1798  if (string.IsNullOrWhiteSpace(nameBox.Text))
1799  {
1800  GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red);
1801  nameBox.Flash();
1802  return false;
1803  }
1804 
1805  // when creating a new local package, prevent name clashes with existing packages
1806  if (packageToSaveTo == null)
1807  {
1808  var subFiles = ContentPackageManager.EnabledPackages.All
1809  .SelectMany(p => p.GetFiles<SubmarineFile>());
1810 
1811  var nameConflictFile = subFiles.FirstOrDefault(file => Path.GetFileNameWithoutExtension(file.Path.Value).Equals(nameBox.Text, StringComparison.InvariantCultureIgnoreCase));
1812 
1813  if (nameConflictFile != null)
1814  {
1815  new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.duplicatefilenameerror", "[packagename]", nameConflictFile.ContentPackage.Name));
1816  return false;
1817  }
1818  }
1819 
1820  if (MainSub.Info.Type != SubmarineType.Player)
1821  {
1822  if (MainSub.Info.Type == SubmarineType.OutpostModule &&
1823  MainSub.Info.OutpostModuleInfo != null)
1824  {
1825  MainSub.Info.PreviewImage = null;
1826  }
1827  }
1828  else if (MainSub.Info.SubmarineClass == SubmarineClass.Undefined && !MainSub.Info.HasTag(SubmarineTag.Shuttle))
1829  {
1830  var msgBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("undefinedsubmarineclasswarning"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
1831 
1832  msgBox.Buttons[0].OnClicked = (bt, userdata) =>
1833  {
1834  handleExceptions(() => SaveSubToFile(nameBox.Text, packageToSaveTo));
1835  saveFrame = null;
1836  msgBox.Close();
1837  return true;
1838  };
1839  msgBox.Buttons[1].OnClicked = (bt, userdata) =>
1840  {
1841  msgBox.Close();
1842  return true;
1843  };
1844  return true;
1845  }
1846 
1847  bool result = false;
1848  handleExceptions(() => result = SaveSubToFile(nameBox.Text, packageToSaveTo));
1849  saveFrame = null;
1850  return result;
1851  }
1852 
1853  private void ReloadModifiedPackage(ContentPackage p)
1854  {
1855  if (p is null) { return; }
1856  p.ReloadSubsAndItemAssemblies();
1857  if (p.Files.Length == 0)
1858  {
1859  Directory.Delete(p.Dir, recursive: true);
1860  ContentPackageManager.LocalPackages.Refresh();
1861  ContentPackageManager.EnabledPackages.DisableRemovedMods();
1862  }
1863  }
1864 
1865  public static Type DetermineSubFileType(SubmarineType type)
1866  => type switch
1867  {
1868  SubmarineType.Outpost => typeof(OutpostFile),
1869  SubmarineType.OutpostModule => typeof(OutpostModuleFile),
1870  SubmarineType.Ruin => typeof(OutpostModuleFile),
1871  SubmarineType.Wreck => typeof(WreckFile),
1872  SubmarineType.BeaconStation => typeof(BeaconStationFile),
1873  SubmarineType.EnemySubmarine => typeof(EnemySubmarineFile),
1874  SubmarineType.Player => typeof(SubmarineFile),
1875  _ => null
1876  };
1877 
1878  private bool SaveSubToFile(string name, ContentPackage packageToSaveTo)
1879  {
1880  Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
1881 
1882  static string getExistingFilePath(ContentPackage package, string fileName)
1883  {
1884  if (Submarine.MainSub?.Info == null) { return null; }
1885  if (package.Files.Any(f => f.Path == MainSub.Info.FilePath && Path.GetFileName(f.Path.Value) == fileName))
1886  {
1887  return MainSub.Info.FilePath;
1888  }
1889  return null;
1890  }
1891 
1892  if (!GameMain.DebugDraw)
1893  {
1894  if (Submarine.GetLightCount() > MaxLights)
1895  {
1896  new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.lightcounterror", "[max]", MaxLights.ToString()));
1897  return false;
1898  }
1899 
1900  if (Submarine.GetShadowCastingLightCount() > MaxShadowCastingLights)
1901  {
1902  new GUIMessageBox(TextManager.Get("error"), TextManager.GetWithVariable("subeditor.shadowcastinglightcounterror", "[max]", MaxShadowCastingLights.ToString()));
1903  return false;
1904  }
1905  }
1906 
1907  if (string.IsNullOrWhiteSpace(name))
1908  {
1909  GUI.AddMessage(TextManager.Get("SubNameMissingWarning"), GUIStyle.Red);
1910  return false;
1911  }
1912 
1913  foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform())
1914  {
1915  if (!name.Contains(illegalChar)) { continue; }
1916  GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red);
1917  return false;
1918  }
1919 
1920  name = name.Trim();
1921 
1922  string newLocalModDir = $"{ContentPackage.LocalModsDir}/{name}";
1923 
1924  string savePath = $"{name}.sub";
1925  string prevSavePath = null;
1926  if (packageToSaveTo != null)
1927  {
1928  var modProject = new ModProject(packageToSaveTo);
1929  var fileListPath = packageToSaveTo.Path;
1930  if (packageToSaveTo == ContentPackageManager.VanillaCorePackage)
1931  {
1932 #if !DEBUG
1933  throw new InvalidOperationException("Cannot save to Vanilla package");
1934 #endif
1935  savePath =
1936  getExistingFilePath(packageToSaveTo, savePath) ??
1937  string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch
1938  {
1939  SubmarineType.Player => "Content/Submarines/{0}",
1940  SubmarineType.Outpost => "Content/Map/Outposts/{0}",
1941  SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore...
1942  SubmarineType.Wreck => "Content/Map/Wrecks/{0}",
1943  SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}",
1944  SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}",
1945  SubmarineType.OutpostModule => MainSub.Info.FilePath.Contains("RuinModules") ? "Content/Map/RuinModules/{0}" : "Content/Map/Outposts/{0}",
1946  _ => throw new InvalidOperationException()
1947  }, savePath);
1948  modProject.ModVersion = "";
1949  }
1950  else
1951  {
1952  string existingFilePath = getExistingFilePath(packageToSaveTo, savePath);
1953  //if we're trying to save a sub that's already included in the package with the same name as before, save directly in the same path
1954  if (existingFilePath != null)
1955  {
1956  savePath = existingFilePath;
1957  }
1958  //otherwise make sure we're not trying to overwrite another sub in the same package
1959  else
1960  {
1961  savePath = Path.Combine(packageToSaveTo.Dir, savePath);
1962  if (File.Exists(savePath))
1963  {
1964  var verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"),
1965  new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
1966  verification.Buttons[0].OnClicked = (_, _) =>
1967  {
1968  addSubAndSave(modProject, savePath, fileListPath);
1969  verification.Close();
1970  return true;
1971  };
1972  verification.Buttons[1].OnClicked = verification.Close;
1973  return false;
1974  }
1975  }
1976  }
1977  addSubAndSave(modProject, savePath, fileListPath);
1978  }
1979  else
1980  {
1981  savePath = Path.Combine(newLocalModDir, savePath);
1982  if (File.Exists(savePath))
1983  {
1984  new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariable("subeditor.packagealreadyexists", "[name]", name));
1985  return false;
1986  }
1987  else
1988  {
1989  ModProject modProject = new ModProject { Name = name };
1990  addSubAndSave(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName));
1991  }
1992  }
1993 
1994  void addSubAndSave(ModProject modProject, string filePath, string packagePath)
1995  {
1996  filePath = filePath.CleanUpPath();
1997  packagePath = packagePath.CleanUpPath();
1998  string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false);
1999  if (filePath.StartsWith(packageDir))
2000  {
2001  filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
2002  }
2003  if (!modProject.Files.Any(f => f.Type == subFileType && f.Path == filePath))
2004  {
2005  //check if there's a file with the same name but different filename case
2006  var matchingFile = modProject.Files.FirstOrDefault(f => f.Type == subFileType && filePath.CleanUpPath().Equals(f.Path.CleanUpPath(), StringComparison.OrdinalIgnoreCase));
2007  if (matchingFile != null)
2008  {
2009  File.Delete(matchingFile.Path.Replace(ContentPath.ModDirStr, packageDir, StringComparison.OrdinalIgnoreCase));
2010  modProject.RemoveFile(matchingFile);
2011  }
2012  var newFile = ModProject.File.FromPath(filePath, subFileType);
2013  modProject.AddFile(newFile);
2014  }
2015 
2016  using var _ = Validation.SkipInDebugBuilds();
2017  modProject.DiscardHashAndInstallTime();
2018  modProject.Save(packagePath);
2019 
2020  savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false);
2021  if (MainSub != null)
2022  {
2023  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2024  if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule)
2025  {
2026  bool savePreviewImage = true;
2027  using System.IO.MemoryStream imgStream = new System.IO.MemoryStream();
2028  try
2029  {
2030  previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height);
2031  }
2032  catch (Exception e)
2033  {
2034  DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e);
2035  savePreviewImage = false;
2036  }
2037  MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null);
2038  }
2039  else
2040  {
2041  MainSub.TrySaveAs(savePath);
2042  }
2043  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2044 
2045  MainSub.CheckForErrors();
2046 
2047  GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green);
2048 
2049  if (savePath.StartsWith(newLocalModDir))
2050  {
2051  ContentPackageManager.LocalPackages.Refresh();
2052  var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir));
2053  if (newPackage is RegularPackage regular)
2054  {
2055  ContentPackageManager.EnabledPackages.EnableRegular(regular);
2056  GameSettings.SaveCurrentConfig();
2057  }
2058  }
2059  if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); }
2060  SubmarineInfo.RefreshSavedSub(savePath);
2061  if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); }
2062  MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage;
2063 
2064  string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
2065  linkedSubBox.ClearChildren();
2066  foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
2067  {
2068  if (sub.Type != SubmarineType.Player) { continue; }
2069  if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; }
2070  linkedSubBox.AddItem(sub.Name, sub);
2071  }
2072  subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
2073  }
2074  }
2075 
2076  return false;
2077  }
2078 
2079  private void CreateSaveScreen(bool quickSave = false)
2080  {
2081  if (saveFrame != null) { return; }
2082 
2083  if (!quickSave)
2084  {
2085  CloseItem();
2086  SetMode(Mode.Default);
2087  }
2088 
2089  saveFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker");
2090 
2091  var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.7f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(750, 500) });
2092  var paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.02f };
2093 
2094  var columnArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), paddedSaveFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true };
2095  var leftColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.55f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.01f, Stretch = true };
2096  var rightColumn = new GUILayoutGroup(new RectTransform(new Vector2(0.42f, 1.0f), columnArea.RectTransform)) { RelativeSpacing = 0.02f, Stretch = true };
2097 
2098  // left column -----------------------------------------------------------------------
2099 
2100  var nameHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), true);
2101  var saveSubLabel = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform),
2102  TextManager.Get("SaveSubDialogName"), font: GUIStyle.SubHeadingFont);
2103 
2104  submarineNameCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), nameHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight);
2105 
2106  nameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.05f), leftColumn.RectTransform))
2107  {
2108  OnEnterPressed = ChangeSubName
2109  };
2110  nameBox.OnTextChanged += (textBox, text) =>
2111  {
2112  if (text.Length > submarineNameLimit)
2113  {
2114  nameBox.Text = text.Substring(0, submarineNameLimit);
2115  nameBox.Flash(GUIStyle.Red);
2116  return true;
2117  }
2118 
2119  submarineNameCharacterCount.Text = text.Length + " / " + submarineNameLimit;
2120  return true;
2121  };
2122 
2123  nameBox.Text = MainSub?.Info.Name ?? "";
2124 
2125  submarineNameCharacterCount.Text = nameBox.Text.Length + " / " + submarineNameLimit;
2126 
2127  var descriptionHeaderGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.03f), leftColumn.RectTransform), isHorizontal: true);
2128 
2129  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), descriptionHeaderGroup.RectTransform), TextManager.Get("SaveSubDialogDescription"), font: GUIStyle.SubHeadingFont);
2130  submarineDescriptionCharacterCount = new GUITextBlock(new RectTransform(new Vector2(.5f, 1f), descriptionHeaderGroup.RectTransform), string.Empty, textAlignment: Alignment.TopRight);
2131 
2132  var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.25f), leftColumn.RectTransform));
2133  descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.Center),
2134  font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft)
2135  {
2136  Padding = new Vector4(10 * GUI.Scale)
2137  };
2138 
2139  descriptionBox.OnTextChanged += (textBox, text) =>
2140  {
2141  if (text.Length > submarineDescriptionLimit)
2142  {
2143  descriptionBox.Text = text.Substring(0, submarineDescriptionLimit);
2144  descriptionBox.Flash(GUIStyle.Red);
2145  return true;
2146  }
2147 
2148  Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText);
2149  textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10));
2150  descriptionContainer.UpdateScrollBarSize();
2151  descriptionContainer.BarScroll = 1.0f;
2152  ChangeSubDescription(textBox, text);
2153  return true;
2154  };
2155 
2156  descriptionBox.Text = GetSubDescription();
2157 
2158  var subTypeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2159  {
2160  Stretch = true
2161  };
2162 
2163  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), subTypeContainer.RectTransform), TextManager.Get("submarinetype"));
2164  var subTypeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.6f, 1f), subTypeContainer.RectTransform));
2165  subTypeContainer.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y));
2166  foreach (SubmarineType subType in Enum.GetValues(typeof(SubmarineType)))
2167  {
2168  if (subType == SubmarineType.Ruin) { continue; }
2169  string textTag = "SubmarineType." + subType;
2170  if (subType == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag))
2171  {
2172  textTag = "MissionType.Pirate";
2173  }
2174  subTypeDropdown.AddItem(TextManager.Get(textTag), subType);
2175  }
2176 
2177  if (Layers.Any())
2178  {
2179  var layerVisibilityGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.01f), leftColumn.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2180  var visibleLayers = Layers.Where(l => !MainSub.Info.LayersHiddenByDefault.Contains(l.Key.ToIdentifier()));
2181  LocalizedString visibleLayersString = LocalizedString.Join(", ", visibleLayers.Select(l => TextManager.Capitalize(l.Key)) ?? ((LocalizedString)"None").ToEnumerable());
2182  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), TextManager.Get("editor.layer.visiblebydefault"), textAlignment: Alignment.CenterLeft);
2183  var layerVisibilityDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), layerVisibilityGroup.RectTransform), text: visibleLayersString, selectMultiple: true);
2184  foreach (var layer in Layers)
2185  {
2186  string layerName = layer.Key;
2187  layerVisibilityDropDown.AddItem(TextManager.Capitalize(layerName), layerName);
2188  if (visibleLayers.Contains(layer))
2189  {
2190  layerVisibilityDropDown.SelectItem(layerName);
2191  }
2192  }
2193  layerVisibilityDropDown.AfterSelected += (button, _) =>
2194  {
2195  MainSub.Info.LayersHiddenByDefault.Clear();
2196  foreach (var layer in Layers)
2197  {
2198  if (!layerVisibilityDropDown.SelectedDataMultiple.Contains(layer.Key))
2199  {
2200  MainSub.Info.LayersHiddenByDefault.Add(layer.Key.ToIdentifier());
2201  }
2202  }
2203  UpdateLayerPanel();
2204  layerVisibilityDropDown.Text = ToolBox.LimitString(layerVisibilityDropDown.Text.Value, layerVisibilityDropDown.Font, layerVisibilityDropDown.Rect.Width);
2205  return true;
2206  };
2207  layerVisibilityGroup.RectTransform.MinSize = layerVisibilityDropDown.RectTransform.MinSize = new Point(0, layerVisibilityDropDown.RectTransform.Children.Max(c => c.MinSize.Y));
2208  }
2209 
2210  //---------------------------------------
2211 
2212  var subTypeDependentSettingFrame = new GUIFrame(new RectTransform((1.0f, 0.6f), leftColumn.RectTransform), style: "InnerFrame");
2213 
2214  var outpostModuleSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform))
2215  {
2216  CanBeFocused = true,
2217  Visible = false,
2218  Stretch = true
2219  };
2220 
2221  // module flags ---------------------
2222 
2223  var outpostModuleGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2224 
2225  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform), TextManager.Get("outpostmoduletype"), textAlignment: Alignment.CenterLeft);
2226  HashSet<Identifier> availableFlags = new HashSet<Identifier>();
2227  foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); }
2228  foreach (Identifier flag in RuinGeneration.RuinGenerationParams.RuinParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); }
2229  foreach (var sub in SubmarineInfo.SavedSubmarines)
2230  {
2231  if (sub.OutpostModuleInfo == null) { continue; }
2232  foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags)
2233  {
2234  if (flag == "none") { continue; }
2235  availableFlags.Add(flag);
2236  }
2237  }
2238  if (MainSub?.Info?.OutpostModuleInfo is { } moduleInfo)
2239  {
2240  foreach (var moduleType in moduleInfo.ModuleFlags)
2241  {
2242  availableFlags.Add(moduleType);
2243  }
2244  }
2245  var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), outpostModuleGroup.RectTransform),
2246  text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.ModuleFlags.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"None").ToEnumerable()), selectMultiple: true);
2247  foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase))
2248  {
2249  moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag);
2250  if (MainSub?.Info?.OutpostModuleInfo == null) { continue; }
2251  if (MainSub.Info.OutpostModuleInfo.ModuleFlags.Contains(flag))
2252  {
2253  moduleTypeDropDown.SelectItem(flag);
2254  }
2255  }
2256  moduleTypeDropDown.AfterSelected += (_, __) =>
2257  {
2258  if (MainSub?.Info?.OutpostModuleInfo == null) { return false; }
2259  MainSub.Info.OutpostModuleInfo.SetFlags(moduleTypeDropDown.SelectedDataMultiple.Cast<Identifier>());
2260  moduleTypeDropDown.Text = ToolBox.LimitString(
2261  MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? moduleTypeDropDown.Text : "None",
2262  moduleTypeDropDown.Font, moduleTypeDropDown.Rect.Width);
2263  return true;
2264  };
2265  outpostModuleGroup.RectTransform.MinSize = new Point(0, outpostModuleGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2266 
2267  var addTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2268  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), addTypeGroup.RectTransform), TextManager.Get("leveleditor.addmoduletype"), textAlignment: Alignment.CenterLeft);
2269  var textBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1f), addTypeGroup.RectTransform));
2270 
2271  new GUIButton(new RectTransform(new Vector2(0.1f, 0.9f), addTypeGroup.RectTransform), text: "+", style: "GUIButtonSmallFreeScale")
2272  {
2273  OnClicked = (btn, _) =>
2274  {
2275  if (textBox.Text.IsNullOrEmpty())
2276  {
2277  textBox.Flash();
2278  return false;
2279  }
2280  if (MainSub?.Info?.OutpostModuleInfo is { } moduleInfo)
2281  {
2282  moduleInfo.SetFlags(moduleInfo.ModuleFlags.Append(textBox.Text.ToIdentifier()).ToList());
2283  //refresh
2284  saveFrame = null;
2285  CreateSaveScreen();
2286  }
2287  return true;
2288  }
2289  };
2290  addTypeGroup.RectTransform.MinSize = new Point(0, addTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2291 
2292  // module flags ---------------------
2293 
2294  var allowAttachGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2295 
2296  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform), TextManager.Get("outpostmoduleallowattachto"), textAlignment: Alignment.CenterLeft);
2297 
2298  var allowAttachDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), allowAttachGroup.RectTransform),
2299  text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowAttachToModules.Select(s => TextManager.Capitalize(s.Value)) ?? ((LocalizedString)"Any").ToEnumerable()), selectMultiple: true);
2300  allowAttachDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier());
2301  if (MainSub.Info.OutpostModuleInfo == null ||
2302  !MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Any() ||
2303  MainSub.Info.OutpostModuleInfo.AllowAttachToModules.All(s => s == "any"))
2304  {
2305  allowAttachDropDown.SelectItem("any".ToIdentifier());
2306  }
2307  foreach (Identifier flag in availableFlags.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase))
2308  {
2309  if (flag == "any" || flag == "none") { continue; }
2310  allowAttachDropDown.AddItem(TextManager.Capitalize(flag.Value), flag);
2311  if (MainSub?.Info?.OutpostModuleInfo == null) { continue; }
2312  if (MainSub.Info.OutpostModuleInfo.AllowAttachToModules.Contains(flag))
2313  {
2314  allowAttachDropDown.SelectItem(flag);
2315  }
2316  }
2317  allowAttachDropDown.AfterSelected += (_, __) =>
2318  {
2319  if (MainSub?.Info?.OutpostModuleInfo == null) { return false; }
2320  MainSub.Info.OutpostModuleInfo.SetAllowAttachTo(allowAttachDropDown.SelectedDataMultiple.Cast<Identifier>());
2321  allowAttachDropDown.Text = ToolBox.LimitString(
2322  MainSub.Info.OutpostModuleInfo.ModuleFlags.Any(f => f != "none") ? allowAttachDropDown.Text.Value : "None",
2323  allowAttachDropDown.Font, allowAttachDropDown.Rect.Width);
2324  return true;
2325  };
2326  allowAttachGroup.RectTransform.MinSize = new Point(0, allowAttachGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2327 
2328  // location types ---------------------
2329 
2330  var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2331 
2332  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft);
2333  HashSet<Identifier> availableLocationTypes = new HashSet<Identifier>();
2334  foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); }
2335 
2336  var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform),
2337  text: LocalizedString.Join(", ", MainSub?.Info?.OutpostModuleInfo?.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true);
2338  locationTypeDropDown.AddItem(TextManager.Capitalize("any"), "any".ToIdentifier());
2339  foreach (Identifier locationType in availableLocationTypes.OrderBy(f => f.Value, StringComparer.InvariantCultureIgnoreCase))
2340  {
2341  locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType);
2342  if (MainSub?.Info?.OutpostModuleInfo == null) { continue; }
2343  if (MainSub.Info.OutpostModuleInfo.AllowedLocationTypes.Contains(locationType))
2344  {
2345  locationTypeDropDown.SelectItem(locationType);
2346  }
2347  }
2348  if (!MainSub.Info?.OutpostModuleInfo?.AllowedLocationTypes?.Any() ?? true) { locationTypeDropDown.SelectItem("any".ToIdentifier()); }
2349 
2350  locationTypeDropDown.AfterSelected += (_, __) =>
2351  {
2352  MainSub?.Info?.OutpostModuleInfo?.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast<Identifier>());
2353  locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text.Value, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width);
2354  return true;
2355  };
2356  locationTypeGroup.RectTransform.MinSize = new Point(0, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2357 
2358  // gap positions ---------------------
2359 
2360  var gapPositionGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2361  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform), TextManager.Get("outpostmodulegappositions"), textAlignment: Alignment.CenterLeft);
2362  var gapPositionDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), gapPositionGroup.RectTransform),
2363  text: "", selectMultiple: true);
2364 
2365  var outpostModuleInfo = MainSub.Info?.OutpostModuleInfo;
2366  if (outpostModuleInfo != null)
2367  {
2368  if (outpostModuleInfo.GapPositions == OutpostModuleInfo.GapPosition.None)
2369  {
2370  outpostModuleInfo.DetermineGapPositions(MainSub);
2371  }
2372  foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition)))
2373  {
2374  if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; }
2375  gapPositionDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos);
2376  if (outpostModuleInfo.GapPositions.HasFlag(gapPos))
2377  {
2378  gapPositionDropDown.SelectItem(gapPos);
2379  }
2380  }
2381  }
2382 
2383  gapPositionDropDown.AfterSelected += (_, __) =>
2384  {
2385  if (MainSub.Info?.OutpostModuleInfo == null) { return false; }
2386  MainSub.Info.OutpostModuleInfo.GapPositions = OutpostModuleInfo.GapPosition.None;
2387  if (gapPositionDropDown.SelectedDataMultiple.Any())
2388  {
2389  List<LocalizedString> gapPosTexts = new List<LocalizedString>();
2390  foreach (OutpostModuleInfo.GapPosition gapPos in gapPositionDropDown.SelectedDataMultiple)
2391  {
2392  MainSub.Info.OutpostModuleInfo.GapPositions |= gapPos;
2393  gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()));
2394  }
2395  gapPositionDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), gapPositionDropDown.Font, gapPositionDropDown.Rect.Width);
2396  }
2397  else
2398  {
2399  gapPositionDropDown.Text = ToolBox.LimitString("None", gapPositionDropDown.Font, gapPositionDropDown.Rect.Width);
2400  }
2401  return true;
2402  };
2403  gapPositionGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2404 
2405  var canAttachToPrevGroup = new GUILayoutGroup(new RectTransform(new Vector2(.975f, 0.1f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
2406  new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform), TextManager.Get("canattachtoprevious"), textAlignment: Alignment.CenterLeft)
2407  {
2408  ToolTip = TextManager.Get("canattachtoprevious.tooltip")
2409  };
2410  var canAttachToPrevDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), canAttachToPrevGroup.RectTransform),
2411  text: "", selectMultiple: true);
2412  if (outpostModuleInfo != null)
2413  {
2414  foreach (OutpostModuleInfo.GapPosition gapPos in Enum.GetValues(typeof(OutpostModuleInfo.GapPosition)))
2415  {
2416  if (gapPos == OutpostModuleInfo.GapPosition.None) { continue; }
2417  canAttachToPrevDropDown.AddItem(TextManager.Capitalize(gapPos.ToString()), gapPos);
2418  if (outpostModuleInfo.CanAttachToPrevious.HasFlag(gapPos))
2419  {
2420  canAttachToPrevDropDown.SelectItem(gapPos);
2421  }
2422  }
2423  }
2424 
2425  canAttachToPrevDropDown.AfterSelected += (_, __) =>
2426  {
2427  if (Submarine.MainSub.Info?.OutpostModuleInfo == null) { return false; }
2428  Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious = OutpostModuleInfo.GapPosition.None;
2429  if (canAttachToPrevDropDown.SelectedDataMultiple.Any())
2430  {
2431  List<string> gapPosTexts = new List<string>();
2432  foreach (OutpostModuleInfo.GapPosition gapPos in canAttachToPrevDropDown.SelectedDataMultiple)
2433  {
2434  Submarine.MainSub.Info.OutpostModuleInfo.CanAttachToPrevious |= gapPos;
2435  gapPosTexts.Add(TextManager.Capitalize(gapPos.ToString()).Value);
2436  }
2437  canAttachToPrevDropDown.Text = ToolBox.LimitString(string.Join(", ", gapPosTexts), canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width);
2438  }
2439  else
2440  {
2441  canAttachToPrevDropDown.Text = ToolBox.LimitString("None", canAttachToPrevDropDown.Font, canAttachToPrevDropDown.Rect.Width);
2442  }
2443  return true;
2444  };
2445  canAttachToPrevGroup.RectTransform.MinSize = new Point(0, gapPositionGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2446 
2447 
2448  // -------------------
2449 
2450  var maxModuleCountGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true)
2451  {
2452  Stretch = true
2453  };
2454  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), maxModuleCountGroup.RectTransform),
2455  TextManager.Get("OutPostModuleMaxCount"), textAlignment: Alignment.CenterLeft, wrap: true)
2456  {
2457  ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip")
2458  };
2459  new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxModuleCountGroup.RectTransform), NumberType.Int)
2460  {
2461  ToolTip = TextManager.Get("OutPostModuleMaxCountToolTip"),
2462  IntValue = MainSub?.Info?.OutpostModuleInfo?.MaxCount ?? 1000,
2463  MinValueInt = 0,
2464  MaxValueInt = 1000,
2465  OnValueChanged = (numberInput) =>
2466  {
2467  MainSub.Info.OutpostModuleInfo.MaxCount = numberInput.IntValue;
2468  }
2469  };
2470  maxModuleCountGroup.RectTransform.MinSize = new Point(0, maxModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2471 
2472  var commonnessGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), outpostModuleSettingsContainer.RectTransform), isHorizontal: true)
2473  {
2474  Stretch = true
2475  };
2476  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), commonnessGroup.RectTransform),
2477  TextManager.Get("subeditor.outpostcommonness"), textAlignment: Alignment.CenterLeft, wrap: true);
2478  new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), commonnessGroup.RectTransform), NumberType.Float)
2479  {
2480  FloatValue = MainSub?.Info?.OutpostModuleInfo?.Commonness ?? 10,
2481  MinValueFloat = 0,
2482  MaxValueFloat = 100,
2483  OnValueChanged = (numberInput) =>
2484  {
2485  MainSub.Info.OutpostModuleInfo.Commonness = numberInput.FloatValue;
2486  }
2487  };
2488  commonnessGroup.RectTransform.MinSize = new Point(0, commonnessGroup.RectTransform.Children.Max(c => c.MinSize.Y));
2489 
2490  outpostModuleSettingsContainer.RectTransform.MinSize = new Point(0, outpostModuleSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
2491 
2492  //---------------------------------------
2493 
2494  var extraSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.5f), subTypeDependentSettingFrame.RectTransform))
2495  {
2496  CanBeFocused = true,
2497  Visible = false,
2498  Stretch = true
2499  };
2500 
2501  var minDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true)
2502  {
2503  Stretch = true
2504  };
2505  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), minDifficultyGroup.RectTransform),
2506  TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true);
2507  var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), minDifficultyGroup.RectTransform), NumberType.Int)
2508  {
2509  IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MinLevelDifficulty ?? 0),
2510  MinValueInt = 0,
2511  MaxValueInt = 100,
2512  OnValueChanged = (numberInput) =>
2513  {
2514  MainSub.Info.GetExtraSubmarineInfo.MinLevelDifficulty = numberInput.IntValue;
2515  }
2516  };
2517  minDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
2518  var maxDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true)
2519  {
2520  Stretch = true
2521  };
2522  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), maxDifficultyGroup.RectTransform),
2523  TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true);
2524  numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxDifficultyGroup.RectTransform), NumberType.Int)
2525  {
2526  IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MaxLevelDifficulty ?? 100),
2527  MinValueInt = 0,
2528  MaxValueInt = 100,
2529  OnValueChanged = (numberInput) =>
2530  {
2531  MainSub.Info.GetExtraSubmarineInfo.MaxLevelDifficulty = numberInput.IntValue;
2532  }
2533  };
2534  maxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
2535 
2536 
2537  //---------------------------------------
2538 
2539  var outpostSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform))
2540  {
2541  CanBeFocused = true,
2542  Visible = false,
2543  Stretch = true
2544  };
2545 
2546  var outpostTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), outpostSettingsContainer.RectTransform), isHorizontal: true)
2547  {
2548  Stretch = true
2549  };
2550  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), outpostTagsGroup.RectTransform),
2551  TextManager.Get("sp.item.tags.name"), textAlignment: Alignment.CenterLeft, wrap: true);
2552  var outpostTagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), outpostTagsGroup.RectTransform))
2553  {
2554  OnEnterPressed = (GUITextBox textBox, string text) =>
2555  {
2556  MainSub.Info.OutpostTags = text.ToIdentifiers().ToImmutableHashSet();
2557  return true;
2558  },
2559  OverflowClip = true,
2560  Text = "default"
2561  };
2562  outpostTagsBox.OnDeselected += (textbox, _) =>
2563  {
2564  MainSub.Info.OutpostTags = outpostTagsBox.Text.ToIdentifiers().ToImmutableHashSet();
2565  };
2566  if (MainSub.Info.OutpostTags != null)
2567  {
2568  outpostTagsBox.Text = MainSub.Info.OutpostTags.ConvertToString();
2569  }
2570 
2571  outpostTagsGroup.RectTransform.MaxSize = outpostTagsBox.RectTransform.MaxSize;
2572 
2573  //---------------------------------------
2574 
2575  var enemySubmarineSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform))
2576  {
2577  CanBeFocused = true,
2578  Visible = false,
2579  Stretch = true
2580  };
2581 
2582  // -------------------
2583 
2584  var enemySubmarineRewardGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true)
2585  {
2586  Stretch = true
2587  };
2588  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineRewardGroup.RectTransform),
2589  TextManager.Get("enemysub.reward"), textAlignment: Alignment.CenterLeft, wrap: true);
2590  numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineRewardGroup.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden)
2591  {
2592  IntValue = (int)(MainSub?.Info?.EnemySubmarineInfo?.Reward ?? 4000),
2593  MinValueInt = 0,
2594  MaxValueInt = 999999,
2595  OnValueChanged = (numberInput) =>
2596  {
2597  MainSub.Info.EnemySubmarineInfo.Reward = numberInput.IntValue;
2598  }
2599  };
2600  enemySubmarineRewardGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
2601  var enemySubmarineDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true)
2602  {
2603  Stretch = true
2604  };
2605  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineDifficultyGroup.RectTransform),
2606  TextManager.Get("preferreddifficulty"), textAlignment: Alignment.CenterLeft, wrap: true);
2607  numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineDifficultyGroup.RectTransform), NumberType.Int)
2608  {
2609  IntValue = (int)(MainSub?.Info?.EnemySubmarineInfo?.PreferredDifficulty ?? 50),
2610  MinValueInt = 0,
2611  MaxValueInt = 100,
2612  OnValueChanged = (numberInput) =>
2613  {
2614  MainSub.Info.EnemySubmarineInfo.PreferredDifficulty = numberInput.IntValue;
2615  }
2616  };
2617  enemySubmarineDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
2618  var enemySubmarineTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true)
2619  {
2620  Stretch = true
2621  };
2622  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineTagsGroup.RectTransform),
2623  TextManager.Get("sp.item.tags.name"), textAlignment: Alignment.CenterLeft, wrap: true);
2624  var tagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineTagsGroup.RectTransform))
2625  {
2626  OnEnterPressed = ChangeEnemySubTags,
2627  OverflowClip = true,
2628  Text = "default"
2629  };
2630  tagsBox.OnDeselected += (textbox, _) => ChangeEnemySubTags(textbox, textbox.Text);
2631  if (MainSub?.Info?.EnemySubmarineInfo?.MissionTags != null)
2632  {
2633  tagsBox.Text = string.Join(',', MainSub.Info.EnemySubmarineInfo.MissionTags);
2634  }
2635 
2636  enemySubmarineTagsGroup.RectTransform.MaxSize = tagsBox.RectTransform.MaxSize;
2637  enemySubmarineSettingsContainer.RectTransform.MinSize = new Point(0, enemySubmarineSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
2638 
2639  //--------------------------------------------------------
2640 
2641  var beaconSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraSettingsContainer.RectTransform))
2642  {
2643  CanBeFocused = true,
2644  Visible = false,
2645  Stretch = true
2646  };
2647 
2648  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamagedwalls"))
2649  {
2650  Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedWalls ?? true,
2651  OnSelected = (tb) =>
2652  {
2653  MainSub.Info.BeaconStationInfo.AllowDamagedWalls = tb.Selected;
2654  return true;
2655  }
2656  };
2657  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdamageddevices"))
2658  {
2659  Selected = MainSub?.Info?.BeaconStationInfo?.AllowDamagedDevices ?? true,
2660  OnSelected = (tb) =>
2661  {
2662  MainSub.Info.BeaconStationInfo.AllowDamagedDevices = tb.Selected;
2663  return true;
2664  }
2665  };
2666  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("allowdisconnectedwires"))
2667  {
2668  Selected = MainSub?.Info?.BeaconStationInfo?.AllowDisconnectedWires ?? true,
2669  OnSelected = (tb) =>
2670  {
2671  MainSub.Info.BeaconStationInfo.AllowDisconnectedWires = tb.Selected;
2672  return true;
2673  }
2674  };
2675  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.25f), beaconSettingsContainer.RectTransform), TextManager.Get("beaconstationplacement"))
2676  {
2677  Selected = MainSub.Info.BeaconStationInfo is { Placement: Level.PlacementType.Top },
2678  OnSelected = (tb) =>
2679  {
2680  MainSub.Info.BeaconStationInfo.Placement = tb.Selected ?
2681  Level.PlacementType.Top :
2682  Level.PlacementType.Bottom;
2683  return true;
2684  }
2685  };
2686  beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
2687 
2688  //------------------------------------------------------------------
2689 
2690  var subSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform))
2691  {
2692  Stretch = true
2693  };
2694 
2695  var priceGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true)
2696  {
2697  Stretch = true
2698  };
2699  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), priceGroup.RectTransform),
2700  TextManager.Get("subeditor.price"), textAlignment: Alignment.CenterLeft, wrap: true);
2701 
2702 
2703  int basePrice = (GameMain.DebugDraw ? 0 : MainSub?.CalculateBasePrice()) ?? 1000;
2704  new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), priceGroup.RectTransform), NumberType.Int, buttonVisibility: GUINumberInput.ButtonVisibility.ForceHidden)
2705  {
2706  IntValue = Math.Max(MainSub?.Info?.Price ?? basePrice, basePrice),
2707  MinValueInt = basePrice,
2708  MaxValueInt = 999999,
2709  OnValueChanged = (numberInput) =>
2710  {
2711  MainSub.Info.Price = numberInput.IntValue;
2712  }
2713  };
2714  if (MainSub?.Info != null)
2715  {
2716  MainSub.Info.Price = Math.Max(MainSub.Info.Price, basePrice);
2717  }
2718 
2719  var classGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2720  {
2721  Stretch = true
2722  };
2723  var classText = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), classGroup.RectTransform),
2724  TextManager.Get("submarineclass"), textAlignment: Alignment.CenterLeft, wrap: true)
2725  {
2726  ToolTip = TextManager.Get("submarineclass.description")
2727  };
2728  GUIDropDown classDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1.0f), classGroup.RectTransform));
2729  classDropDown.RectTransform.MinSize = new Point(0, subTypeContainer.RectTransform.Children.Max(c => c.MinSize.Y));
2730  foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass)))
2731  {
2732  classDropDown.AddItem(TextManager.Get($"{nameof(SubmarineClass)}.{subClass}"), subClass, toolTip: TextManager.Get($"submarineclass.{subClass}.description"));
2733  }
2734  classDropDown.AddItem(TextManager.Get(nameof(SubmarineTag.Shuttle)), SubmarineTag.Shuttle);
2735  classDropDown.OnSelected += (selected, userdata) =>
2736  {
2737  switch (userdata)
2738  {
2739  case SubmarineClass submarineClass:
2740  MainSub.Info.RemoveTag(SubmarineTag.Shuttle);
2741  MainSub.Info.SubmarineClass = submarineClass;
2742  break;
2743  case SubmarineTag.Shuttle:
2744  MainSub.Info.AddTag(SubmarineTag.Shuttle);
2745  MainSub.Info.SubmarineClass = SubmarineClass.Undefined;
2746  break;
2747  }
2748  return true;
2749  };
2750  classDropDown.SelectItem(!MainSub.Info.HasTag(SubmarineTag.Shuttle) ? MainSub.Info.SubmarineClass : (object)SubmarineTag.Shuttle);
2751 
2752  var tierGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true)
2753  {
2754  Stretch = true
2755  };
2756  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), tierGroup.RectTransform),
2757  TextManager.Get("subeditor.tier"), textAlignment: Alignment.CenterLeft, wrap: true)
2758  {
2759  ToolTip = TextManager.Get("submarinetier.description")
2760  };
2761 
2762  new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), tierGroup.RectTransform), NumberType.Int)
2763  {
2764  IntValue = MainSub.Info.Tier,
2765  MinValueInt = 1,
2766  MaxValueInt = SubmarineInfo.HighestTier,
2767  OnValueChanged = (numberInput) =>
2768  {
2769  MainSub.Info.Tier = numberInput.IntValue;
2770  }
2771  };
2772  if (MainSub?.Info != null)
2773  {
2774  MainSub.Info.Tier = Math.Clamp(MainSub.Info.Tier, 1, SubmarineInfo.HighestTier);
2775  }
2776 
2777  var crewSizeArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true)
2778  {
2779  Stretch = true,
2780  AbsoluteSpacing = 5
2781  };
2782 
2783  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewSizeArea.RectTransform),
2784  TextManager.Get("RecommendedCrewSize"), textAlignment: Alignment.CenterLeft, wrap: true);
2785  var crewSizeMin = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f)
2786  {
2787  MinValueInt = 1,
2788  MaxValueInt = 128
2789  };
2790  new GUITextBlock(new RectTransform(new Vector2(0.06f, 1.0f), crewSizeArea.RectTransform), "-", textAlignment: Alignment.Center);
2791  var crewSizeMax = new GUINumberInput(new RectTransform(new Vector2(0.17f, 1.0f), crewSizeArea.RectTransform), NumberType.Int, relativeButtonAreaWidth: 0.25f)
2792  {
2793  MinValueInt = 1,
2794  MaxValueInt = 128
2795  };
2796 
2797  crewSizeMin.OnValueChanged += (numberInput) =>
2798  {
2799  crewSizeMax.IntValue = Math.Max(crewSizeMax.IntValue, numberInput.IntValue);
2800  MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue;
2801  MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue;
2802  };
2803 
2804  crewSizeMax.OnValueChanged += (numberInput) =>
2805  {
2806  crewSizeMin.IntValue = Math.Min(crewSizeMin.IntValue, numberInput.IntValue);
2807  MainSub.Info.RecommendedCrewSizeMin = crewSizeMin.IntValue;
2808  MainSub.Info.RecommendedCrewSizeMax = crewSizeMax.IntValue;
2809  };
2810 
2811  var crewExpArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true)
2812  {
2813  Stretch = true,
2814  AbsoluteSpacing = 5
2815  };
2816 
2817  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), crewExpArea.RectTransform),
2818  TextManager.Get("RecommendedCrewExperience"), textAlignment: Alignment.CenterLeft, wrap: true);
2819 
2820  var toggleExpLeft = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleLeft");
2821  var experienceText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), crewExpArea.RectTransform),
2822  text: TextManager.Get(SubmarineInfo.CrewExperienceLevel.CrewExperienceLow.ToIdentifier()), textAlignment: Alignment.Center);
2823  var toggleExpRight = new GUIButton(new RectTransform(new Vector2(0.05f, 1.0f), crewExpArea.RectTransform), style: "GUIButtonToggleRight");
2824 
2825  toggleExpLeft.OnClicked += (btn, userData) =>
2826  {
2827  MainSub.Info.RecommendedCrewExperience--;
2828  if (MainSub.Info.RecommendedCrewExperience < SubmarineInfo.CrewExperienceLevel.CrewExperienceLow)
2829  {
2830  MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh;
2831  }
2832  experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier());
2833  return true;
2834  };
2835 
2836  toggleExpRight.OnClicked += (btn, userData) =>
2837  {
2838  MainSub.Info.RecommendedCrewExperience++;
2839  if (MainSub.Info.RecommendedCrewExperience > SubmarineInfo.CrewExperienceLevel.CrewExperienceHigh)
2840  {
2841  MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow;
2842  }
2843  experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier());
2844  return true;
2845  };
2846 
2847  var hideInMenusArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2848  {
2849  Stretch = true,
2850  AbsoluteSpacing = 5
2851  };
2852  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), hideInMenusArea.RectTransform),
2853  TextManager.Get("HideInMenus"), textAlignment: Alignment.CenterLeft, wrap: true);
2854 
2855  new GUITickBox(new RectTransform((0.4f, 1.0f), hideInMenusArea.RectTransform), "")
2856  {
2857  Selected = MainSub.Info.HasTag(SubmarineTag.HideInMenus),
2858  OnSelected = box =>
2859  {
2860  if (box.Selected)
2861  {
2862  MainSub.Info.AddTag(SubmarineTag.HideInMenus);
2863  }
2864  else
2865  {
2866  MainSub.Info.RemoveTag(SubmarineTag.HideInMenus);
2867  }
2868  return true;
2869  }
2870  };
2871 
2872  var outFittingArea = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), subSettingsContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
2873  {
2874  Stretch = true,
2875  AbsoluteSpacing = 5
2876  };
2877  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), outFittingArea.RectTransform),
2878  TextManager.Get("ManuallyOutfitted"), textAlignment: Alignment.CenterLeft, wrap: true)
2879  {
2880  ToolTip = TextManager.Get("manuallyoutfittedtooltip")
2881  };
2882  new GUITickBox(new RectTransform((0.4f, 1.0f), outFittingArea.RectTransform), "")
2883  {
2884  ToolTip = TextManager.Get("manuallyoutfittedtooltip"),
2885  Selected = MainSub.Info.IsManuallyOutfitted,
2886  OnSelected = box =>
2887  {
2888  MainSub.Info.IsManuallyOutfitted = box.Selected;
2889  return true;
2890  }
2891  };
2892 
2893  if (MainSub != null)
2894  {
2895  int min = MainSub.Info.RecommendedCrewSizeMin;
2896  int max = MainSub.Info.RecommendedCrewSizeMax;
2897  crewSizeMin.IntValue = min;
2898  crewSizeMax.IntValue = max;
2899  if (MainSub.Info.RecommendedCrewExperience == SubmarineInfo.CrewExperienceLevel.Unknown)
2900  {
2901  MainSub.Info.RecommendedCrewExperience = SubmarineInfo.CrewExperienceLevel.CrewExperienceLow;
2902  }
2903  experienceText.Text = TextManager.Get(MainSub.Info.RecommendedCrewExperience.ToIdentifier());
2904  }
2905 
2906  subTypeDropdown.OnSelected += (selected, userdata) =>
2907  {
2908  SubmarineType type = (SubmarineType)userdata;
2909  MainSub.Info.Type = type;
2910  if (type == SubmarineType.OutpostModule)
2911  {
2912  MainSub.Info.OutpostModuleInfo ??= new OutpostModuleInfo(MainSub.Info);
2913  }
2914  else if (type == SubmarineType.BeaconStation)
2915  {
2916  MainSub.Info.BeaconStationInfo ??= new BeaconStationInfo(MainSub.Info);
2917  }
2918  else if (type == SubmarineType.Wreck)
2919  {
2920  MainSub.Info.WreckInfo ??= new WreckInfo(MainSub.Info);
2921  }
2922  else if (type == SubmarineType.EnemySubmarine)
2923  {
2924  MainSub.Info.EnemySubmarineInfo ??= new EnemySubmarineInfo(MainSub.Info);
2925  }
2926  previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage);
2927  outpostModuleSettingsContainer.Visible = type == SubmarineType.OutpostModule;
2928  extraSettingsContainer.Visible = type == SubmarineType.BeaconStation || type == SubmarineType.Wreck;
2929  beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation;
2930  enemySubmarineSettingsContainer.Visible = type == SubmarineType.EnemySubmarine;
2931  subSettingsContainer.Visible = type == SubmarineType.Player;
2932  outpostSettingsContainer.Visible = type == SubmarineType.Outpost;
2933  return true;
2934  };
2935  subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
2936 
2937  int minHeight = subSettingsContainer.Children.First().Children.Max(c => c.RectTransform.MinSize.Y);
2938  foreach (var child in subSettingsContainer.Children)
2939  {
2940  child.RectTransform.MinSize = new Point(0, minHeight);
2941  }
2942 
2943  // right column ---------------------------------------------------
2944 
2945  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), rightColumn.RectTransform), TextManager.Get("SubPreviewImage"), font: GUIStyle.SubHeadingFont);
2946 
2947  var previewImageHolder = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), rightColumn.RectTransform), style: null) { Color = Color.Black, CanBeFocused = false };
2948  previewImage = new GUIImage(new RectTransform(Vector2.One, previewImageHolder.RectTransform), MainSub?.Info.PreviewImage, scaleToFit: true);
2949 
2950  previewImageButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), rightColumn.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f };
2951 
2952  new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageCreate"), style: "GUIButtonSmall")
2953  {
2954  Enabled = MainSub?.Info.AllowPreviewImage ?? false,
2955  OnClicked = (btn, userdata) =>
2956  {
2957  using (System.IO.MemoryStream imgStream = new System.IO.MemoryStream())
2958  {
2959  CreateImage(defaultPreviewImageSize.X, defaultPreviewImageSize.Y, imgStream);
2960  previewImage.Sprite = new Sprite(TextureLoader.FromStream(imgStream, compress: false), null, null);
2961  if (MainSub != null)
2962  {
2963  MainSub.Info.PreviewImage = previewImage.Sprite;
2964  }
2965  }
2966  return true;
2967  }
2968  };
2969 
2970  new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), previewImageButtonHolder.RectTransform), TextManager.Get("SubPreviewImageBrowse"), style: "GUIButtonSmall")
2971  {
2972  Enabled = MainSub?.Info.AllowPreviewImage ?? false,
2973  OnClicked = (btn, userdata) =>
2974  {
2975  FileSelection.OnFileSelected = (file) =>
2976  {
2977  if (new FileInfo(file).Length > 2048 * 2048)
2978  {
2979  new GUIMessageBox(TextManager.Get("Error"), TextManager.Get("WorkshopItemPreviewImageTooLarge"));
2980  return;
2981  }
2982 
2983  previewImage.Sprite = new Sprite(file, sourceRectangle: null);
2984  if (MainSub != null)
2985  {
2986  MainSub.Info.PreviewImage = previewImage.Sprite;
2987  }
2988  };
2989  FileSelection.ClearFileTypeFilters();
2990  FileSelection.AddFileTypeFilter("PNG", "*.png");
2991  FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg");
2992  FileSelection.AddFileTypeFilter("All files", "*.*");
2993  FileSelection.SelectFileTypeFilter("*.png");
2994  FileSelection.Open = true;
2995  return false;
2996  }
2997  };
2998 
2999  previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y));
3000 
3001  var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true);
3002 
3003  GUIButton createTabberBtn(string labelTag)
3004  {
3005  var btn = new GUIButton(new RectTransform((0.5f, 1.0f), contentPackageTabber.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter), TextManager.Get(labelTag), style: "GUITabButton");
3006  btn.TextBlock.Wrap = true;
3007  btn.TextBlock.SetTextPos();
3008  btn.RectTransform.MaxSize = RectTransform.MaxPoint;
3009  btn.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint);
3010  btn.Font = GUIStyle.SmallFont;
3011  return btn;
3012  }
3013 
3014  var saveToPackageTabBtn = createTabberBtn("SaveToLocalPackage");
3015  saveToPackageTabBtn.Selected = true;
3016  var reqPackagesTabBtn = createTabberBtn("RequiredContentPackages");
3017  reqPackagesTabBtn.Selected = false;
3018 
3019  var horizontalArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightColumn.RectTransform), style: null);
3020 
3021  var saveInPackageLayout = new GUILayoutGroup(new RectTransform(Vector2.One,
3022  horizontalArea.RectTransform, Anchor.BottomRight))
3023  {
3024  Stretch = true
3025  };
3026 
3027  var packageToSaveInList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f),
3028  saveInPackageLayout.RectTransform));
3029 
3030  var packToSaveInFilter
3031  = new GUITextBox(new RectTransform((1.0f, 0.15f), saveInPackageLayout.RectTransform),
3032  createClearButton: true);
3033 
3034  GUILayoutGroup addItemToPackageToSaveList(LocalizedString itemText, ContentPackage p)
3035  {
3036  var listItem = new GUIFrame(new RectTransform((1.0f, 0.15f), packageToSaveInList.Content.RectTransform),
3037  style: "ListBoxElement")
3038  {
3039  UserData = p
3040  };
3041  if (p != null && p != ContentPackageManager.VanillaCorePackage) { listItem.ToolTip = p.Dir; }
3042  var retVal =
3043  new GUILayoutGroup(new RectTransform(Vector2.One, listItem.RectTransform),
3044  isHorizontal: true) { Stretch = true };
3045  var iconFrame =
3046  new GUIFrame(
3047  new RectTransform(Vector2.One, retVal.RectTransform, scaleBasis: ScaleBasis.BothHeight),
3048  style: null) { CanBeFocused = false };
3049  var pkgText = new GUITextBlock(new RectTransform(Vector2.One, retVal.RectTransform), itemText)
3050  { CanBeFocused = false };
3051  return retVal;
3052  }
3053 
3054 #if DEBUG
3055  //this is a debug-only option so I won't bother submitting it for localization
3056  var modifyVanillaListItem = addItemToPackageToSaveList("Modify Vanilla content package", ContentPackageManager.VanillaCorePackage);
3057  var modifyVanillaListIcon = modifyVanillaListItem.GetChild<GUIFrame>();
3058  GUIStyle.Apply(modifyVanillaListIcon, "WorkshopMenu.EditButton");
3059 #endif
3060 
3061  var newPackageListItem = addItemToPackageToSaveList(TextManager.Get("CreateNewLocalPackage"), null);
3062  var newPackageListIcon = newPackageListItem.GetChild<GUIFrame>();
3063  var newPackageListText = newPackageListItem.GetChild<GUITextBlock>();
3064  GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon");
3065  new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform),
3066  onUpdate: (f, component) =>
3067  {
3068  foreach (GUIComponent contentChild in packageToSaveInList.Content.Children)
3069  {
3070  contentChild.Visible &= !(contentChild.GetChild<GUILayoutGroup>()?.GetChild<GUITextBlock>() is GUITextBlock tb &&
3071  !tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase));
3072  }
3073  });
3074  ContentPackage ownerPkg = null;
3075  if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); }
3076  foreach (var p in ContentPackageManager.LocalPackages)
3077  {
3078  var packageListItem = addItemToPackageToSaveList(p.Name, p);
3079  if (p == ownerPkg)
3080  {
3081  var packageListIcon = packageListItem.GetChild<GUIFrame>();
3082  var packageListText = packageListItem.GetChild<GUITextBlock>();
3083  GUIStyle.Apply(packageListIcon, "WorkshopMenu.EditButton");
3084  packageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", p.Name);
3085  }
3086  }
3087  if (ownerPkg != null)
3088  {
3089  var element = packageToSaveInList.Content.FindChild(ownerPkg);
3090  element?.RectTransform.SetAsFirstChild();
3091  }
3092  packageToSaveInList.Select(0);
3093 
3094  var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One,
3095  horizontalArea.RectTransform, Anchor.BottomRight))
3096  {
3097  Stretch = true,
3098  Visible = false
3099  };
3100 
3101  var requiredContentPackList = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f),
3102  requiredContentPackagesLayout.RectTransform));
3103 
3104  var filterLayout = new GUILayoutGroup(
3105  new RectTransform((1.0f, 0.15f), requiredContentPackagesLayout.RectTransform),
3106  isHorizontal: true, childAnchor: Anchor.CenterLeft);
3107 
3108  var contentPackFilter
3109  = new GUITextBox(new RectTransform((0.6f, 1.0f), filterLayout.RectTransform),
3110  createClearButton: true);
3111  contentPackFilter.OnTextChanged += (box, text) =>
3112  {
3113  requiredContentPackList.Content.Children.ForEach(c
3114  => c.Visible = !(c is GUITickBox tb &&
3115  !tb.Text.Contains(text, StringComparison.OrdinalIgnoreCase)));
3116  return true;
3117  };
3118 
3119  var autoDetectBtn = new GUIButton(new RectTransform((0.4f, 1.0f), filterLayout.RectTransform),
3120  text: TextManager.Get("AutoDetectRequiredPackages"), style: "GUIButtonSmall")
3121  {
3122  OnClicked = (button, o) =>
3123  {
3124  var requiredPackages = MapEntity.MapEntityList.Select(e => e?.Prefab?.ContentPackage)
3125  .Where(cp => cp != null)
3126  .Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
3127  var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
3128  tickboxes.ForEach(tb => tb.Selected = requiredPackages.Contains(tb.UserData as string ?? ""));
3129  return false;
3130  }
3131  };
3132 
3133  if (MainSub != null)
3134  {
3135  List<string> allContentPacks = MainSub.Info.RequiredContentPackages.ToList();
3136  foreach (ContentPackage contentPack in ContentPackageManager.AllPackages)
3137  {
3138  //don't show content packages that only define submarine files
3139  //(it doesn't make sense to require another sub to be installed to install this one)
3140  if (contentPack.Files.All(f => f is SubmarineFile || f is ItemAssemblyFile)) { continue; }
3141 
3142  if (!allContentPacks.Contains(contentPack.Name))
3143  {
3144  string altName = contentPack.AltNames.FirstOrDefault(n => allContentPacks.Contains(n));
3145  if (!string.IsNullOrEmpty(altName))
3146  {
3147  if (MainSub.Info.RequiredContentPackages.Contains(altName))
3148  {
3149  MainSub.Info.RequiredContentPackages.Remove(altName);
3150  MainSub.Info.RequiredContentPackages.Add(contentPack.Name);
3151  }
3152  allContentPacks.Remove(altName);
3153  }
3154  allContentPacks.Add(contentPack.Name);
3155  }
3156  }
3157 
3158  foreach (string contentPackageName in allContentPacks)
3159  {
3160  var cpTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.2f), requiredContentPackList.Content.RectTransform), contentPackageName, font: GUIStyle.SmallFont)
3161  {
3162  Selected = MainSub.Info.RequiredContentPackages.Contains(contentPackageName),
3163  UserData = contentPackageName
3164  };
3165  cpTickBox.OnSelected += tickBox =>
3166  {
3167  if (tickBox.Selected)
3168  {
3169  MainSub.Info.RequiredContentPackages.Add((string)tickBox.UserData);
3170  }
3171  else
3172  {
3173  MainSub.Info.RequiredContentPackages.Remove((string)tickBox.UserData);
3174  }
3175  return true;
3176  };
3177  }
3178  }
3179 
3180  GUIButton.OnClickedHandler switchToTab(GUIButton tabBtn, GUIComponent tab)
3181  => (button, obj) =>
3182  {
3183  horizontalArea.Children.ForEach(c => c.Visible = false);
3184  contentPackageTabber.Children.ForEach(c => c.Selected = false);
3185  tabBtn.Selected = true;
3186  tab.Visible = true;
3187  return false;
3188  };
3189 
3190  saveToPackageTabBtn.OnClicked = switchToTab(saveToPackageTabBtn, saveInPackageLayout);
3191  reqPackagesTabBtn.OnClicked = switchToTab(reqPackagesTabBtn, requiredContentPackagesLayout);
3192 
3193  var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), paddedSaveFrame.RectTransform, Anchor.BottomCenter, minSize: new Point(0, 30)), style: null);
3194 
3195  var cancelButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft),
3196  TextManager.Get("Cancel"))
3197  {
3198  OnClicked = (GUIButton btn, object userdata) =>
3199  {
3200  saveFrame = null;
3201  return true;
3202  }
3203  };
3204 
3205  var saveButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight),
3206  TextManager.Get("SaveSubButton").Fallback(TextManager.Get("save")))
3207  {
3208  OnClicked = (button, o) => SaveSub(packageToSaveInList.SelectedData as ContentPackage)
3209  };
3210  paddedSaveFrame.Recalculate();
3211  leftColumn.Recalculate();
3212 
3213  subSettingsContainer.RectTransform.MinSize = outpostModuleSettingsContainer.RectTransform.MinSize = beaconSettingsContainer.RectTransform.MinSize =
3214  new Point(0, Math.Max(subSettingsContainer.Rect.Height, outpostModuleSettingsContainer.Rect.Height));
3215  subSettingsContainer.Recalculate();
3216  outpostModuleSettingsContainer.Recalculate();
3217  beaconSettingsContainer.Recalculate();
3218  enemySubmarineSettingsContainer.Recalculate();
3219 
3220  descriptionBox.Text = MainSub == null ? "" : MainSub.Info.Description.Value;
3221  submarineDescriptionCharacterCount.Text = descriptionBox.Text.Length + " / " + submarineDescriptionLimit;
3222 
3223  subTypeDropdown.SelectItem(MainSub.Info.Type);
3224 
3225  if (quickSave) { SaveSub(packageToSaveInList.SelectedData as ContentPackage); }
3226  }
3227 
3228  private void CreateSaveAssemblyScreen()
3229  {
3230  SetMode(Mode.Default);
3231 
3232  saveFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null)
3233  {
3234  OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) saveFrame = null; return true; }
3235  };
3236 
3237  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, saveFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
3238 
3239  var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.25f, 0.35f), saveFrame.RectTransform, Anchor.Center) { MinSize = new Point(400, 350) });
3240  GUILayoutGroup paddedSaveFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center))
3241  {
3242  AbsoluteSpacing = GUI.IntScale(5),
3243  Stretch = true
3244  };
3245 
3246  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform),
3247  TextManager.Get("SaveItemAssemblyDialogHeader"), font: GUIStyle.LargeFont);
3248  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedSaveFrame.RectTransform),
3249  TextManager.Get("SaveItemAssemblyDialogName"));
3250  nameBox = new GUITextBox(new RectTransform(new Vector2(0.6f, 0.1f), paddedSaveFrame.RectTransform));
3251 
3252 #if DEBUG
3253  new GUITickBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedSaveFrame.RectTransform), TextManager.Get("SaveItemAssemblyHideInMenus"))
3254  {
3255  UserData = "hideinmenus"
3256  };
3257 #endif
3258 
3259  var descriptionContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.5f), paddedSaveFrame.RectTransform));
3260  descriptionBox = new GUITextBox(new RectTransform(Vector2.One, descriptionContainer.Content.RectTransform, Anchor.TopLeft),
3261  font: GUIStyle.SmallFont, style: "GUITextBoxNoBorder", wrap: true, textAlignment: Alignment.TopLeft)
3262  {
3263  Padding = new Vector4(10 * GUI.Scale)
3264  };
3265 
3266  descriptionBox.OnTextChanged += (textBox, text) =>
3267  {
3268  Vector2 textSize = textBox.Font.MeasureString(descriptionBox.WrappedText);
3269  textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(descriptionContainer.Content.Rect.Height, (int)textSize.Y + 10));
3270  descriptionContainer.UpdateScrollBarSize();
3271  descriptionContainer.BarScroll = 1.0f;
3272  return true;
3273  };
3274 
3275  var buttonArea = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), paddedSaveFrame.RectTransform), style: null);
3276  new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform, Anchor.BottomLeft),
3277  TextManager.Get("Cancel"))
3278  {
3279  OnClicked = (GUIButton btn, object userdata) =>
3280  {
3281  saveFrame = null;
3282  return true;
3283  }
3284  };
3285  new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), buttonArea.RectTransform, Anchor.BottomRight),
3286  TextManager.Get("SaveSubButton"))
3287  {
3288  OnClicked = SaveAssembly
3289  };
3290  buttonArea.RectTransform.MinSize = new Point(0, buttonArea.Children.First().RectTransform.MinSize.Y);
3291  }
3292 
3300  private List<Item> LoadItemAssemblyInventorySafe(ItemAssemblyPrefab assemblyPrefab)
3301  {
3302  var realItems = assemblyPrefab.CreateInstance(Vector2.Zero, MainSub);
3303  var itemInstance = new List<Item>();
3304  realItems.ForEach(entity =>
3305  {
3306  if (entity is Item it && it.ParentInventory == null)
3307  {
3308  itemInstance.Add(it);
3309  }
3310  });
3311  return itemInstance;
3312  }
3313 
3314  private bool SaveAssembly(GUIButton button, object obj)
3315  {
3316  if (string.IsNullOrWhiteSpace(nameBox.Text))
3317  {
3318  GUI.AddMessage(TextManager.Get("ItemAssemblyNameMissingWarning"), GUIStyle.Red);
3319 
3320  nameBox.Flash();
3321  return false;
3322  }
3323 
3324  foreach (char illegalChar in Path.GetInvalidFileNameCharsCrossPlatform())
3325  {
3326  if (nameBox.Text.Contains(illegalChar))
3327  {
3328  GUI.AddMessage(TextManager.GetWithVariable("ItemAssemblyNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red);
3329  nameBox.Flash();
3330  return false;
3331  }
3332  }
3333 
3334  nameBox.Text = nameBox.Text.Trim();
3335 
3336  bool hideInMenus = nameBox.Parent.GetChildByUserData("hideinmenus") is GUITickBox hideInMenusTickBox && hideInMenusTickBox.Selected;
3337  string saveFolder = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text);
3338  string filePath = Path.Combine(saveFolder, $"{nameBox.Text}.xml").CleanUpPathCrossPlatform();
3339  if (File.Exists(filePath))
3340  {
3341  var msgBox = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("ItemAssemblyFileExistsWarning"), new[] { TextManager.Get("Yes"), TextManager.Get("No") });
3342  msgBox.Buttons[0].OnClicked = (btn, userdata) =>
3343  {
3344  msgBox.Close();
3345  Save();
3346  return true;
3347  };
3348  msgBox.Buttons[1].OnClicked = msgBox.Close;
3349  }
3350  else
3351  {
3352  Save();
3353  }
3354 
3355  void Save()
3356  {
3357  ContentPackage existingContentPackage = ContentPackageManager.LocalPackages.Regular.FirstOrDefault(p => p.Files.Any(f => f.Path == filePath));
3358  if (existingContentPackage == null)
3359  {
3360  //content package doesn't exist, create one
3361  ModProject modProject = new ModProject { Name = nameBox.Text };
3362  var newFile = ModProject.File.FromPath<ItemAssemblyFile>(Path.Combine(ContentPath.ModDirStr, $"{nameBox.Text}.xml"));
3363  modProject.AddFile(newFile);
3364  string newPackagePath = ContentPackageManager.LocalPackages.SaveRegularMod(modProject);
3365  existingContentPackage = ContentPackageManager.LocalPackages.GetRegularModByPath(newPackagePath);
3366  }
3367 
3368  XDocument doc = new XDocument(ItemAssemblyPrefab.Save(MapEntity.SelectedList.ToList(), nameBox.Text, descriptionBox.Text, hideInMenus));
3369  try
3370  {
3371  doc.SaveSafe(filePath);
3372  }
3373  catch (Exception e)
3374  {
3375  DebugConsole.ThrowError($"Failed to save the item assembly to \"{filePath}\".", e);
3376  return;
3377  }
3378 
3379  var result = ContentPackageManager.ReloadContentPackage(existingContentPackage);
3380  if (!result.TryUnwrapSuccess(out var resultPackage))
3381  {
3382  throw new Exception($"Failed to reload content package \"{existingContentPackage.Name}\"",
3383  result.TryUnwrapFailure(out var exception) ? exception : null);
3384  }
3385  if (resultPackage is RegularPackage regularPackage
3386  && !ContentPackageManager.EnabledPackages.Regular.Contains(regularPackage))
3387  {
3388  ContentPackageManager.EnabledPackages.EnableRegular(regularPackage);
3389  GameSettings.SaveCurrentConfig();
3390  }
3391 
3392  UpdateEntityList();
3393  OpenEntityMenu(selectedCategory);
3394  }
3395 
3396  saveFrame = null;
3397  return false;
3398  }
3399 
3400  private static void SnapToGrid()
3401  {
3402  // First move components
3403  foreach (MapEntity e in MapEntity.SelectedList)
3404  {
3405  // Items snap to centre of nearest grid square
3406  Vector2 offset = e.Position;
3407  offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y);
3408  if (e is Item item)
3409  {
3410  var wire = item.GetComponent<Wire>();
3411  if (wire != null) { continue; }
3412  item.Move(offset);
3413  if (item.GetComponent<Door>()?.LinkedGap is Gap linkedGap)
3414  {
3415  linkedGap.Move(item.Position - linkedGap.Position);
3416  }
3417  }
3418  else if (e is Structure structure)
3419  {
3420  structure.Move(offset);
3421  }
3422  }
3423 
3424  // Then move wires, separated as moving components also moves the start and end node of wires
3425  foreach (Item item in MapEntity.SelectedList.Where(entity => entity is Item).Cast<Item>())
3426  {
3427  var wire = item.GetComponent<Wire>();
3428  if (wire != null)
3429  {
3430  for (int i = 0; i < wire.GetNodes().Count; i++)
3431  {
3432  // Items wire nodes to centre of nearest grid square
3433  Vector2 offset = wire.GetNodes()[i] + Submarine.MainSub.HiddenSubPosition;
3434  offset = new Vector2((MathF.Floor(offset.X / Submarine.GridSize.X) + .5f) * Submarine.GridSize.X - offset.X, (MathF.Floor(offset.Y / Submarine.GridSize.Y) + .5f) * Submarine.GridSize.Y - offset.Y);
3435  wire.MoveNode(i, offset);
3436  }
3437  }
3438  }
3439  }
3440 
3441  private static IEnumerable<SubmarineInfo> GetLoadableSubs()
3442  {
3443  string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
3444  return SubmarineInfo.SavedSubmarines.Where(s
3445  => Path.GetDirectoryName(Path.GetFullPath(s.FilePath)) != downloadFolder);
3446  }
3447 
3448  private void CreateLoadScreen()
3449  {
3450  CloseItem();
3451  SubmarineInfo.RefreshSavedSubs();
3452  SetMode(Mode.Default);
3453 
3454  loadFrame = new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, GUI.Canvas, Anchor.Center), style: "GUIBackgroundBlocker");
3455 
3456  new GUIButton(new RectTransform(Vector2.One, loadFrame.RectTransform, Anchor.Center), style: null)
3457  {
3458  OnClicked = (_, _) =>
3459  {
3460  loadFrame = null;
3461  return true;
3462  }
3463  };
3464 
3465  var innerFrame = new GUIFrame(new RectTransform(new Vector2(0.53f, 0.75f), loadFrame.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(350, 500) });
3466 
3467  var paddedLoadFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.9f), innerFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.01f };
3468 
3469  var deleteButtonHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.2f), paddedLoadFrame.RectTransform, Anchor.Center))
3470  {
3471  RelativeSpacing = 0.1f,
3472  Stretch = true
3473  };
3474 
3475  var searchBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), font: GUIStyle.Font, createClearButton: true);
3476  var searchTitle = new GUITextBlock(new RectTransform(Vector2.One, searchBox.RectTransform), TextManager.Get("serverlog.filter"),
3477  textAlignment: Alignment.CenterLeft, font: GUIStyle.Font)
3478  {
3479  CanBeFocused = false,
3480  IgnoreLayoutGroups = true
3481  };
3482  searchTitle.TextColor *= 0.5f;
3483 
3484  var subList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), paddedLoadFrame.RectTransform))
3485  {
3486  PlaySoundOnSelect = true,
3487  ScrollBarVisible = true,
3488  OnSelected = (GUIComponent selected, object userData) =>
3489  {
3490  if (deleteButtonHolder.FindChild("delete") is GUIButton deleteBtn)
3491  {
3492  deleteBtn.ToolTip = string.Empty;
3493  if (!(userData is SubmarineInfo subInfo))
3494  {
3495  deleteBtn.Enabled = false;
3496  return true;
3497  }
3498 
3499  var package = GetLocalPackageThatOwnsSub(subInfo);
3500  if (package != null)
3501  {
3502  deleteBtn.Enabled = true;
3503  }
3504  else
3505  {
3506  deleteBtn.Enabled = false;
3507  if (IsVanillaSub(subInfo))
3508  {
3509  deleteBtn.ToolTip = TextManager.Get("cantdeletevanillasub");
3510  }
3511  else if (GetPackageThatOwnsSub(subInfo, ContentPackageManager.AllPackages) is ContentPackage subPackage)
3512  {
3513  deleteBtn.ToolTip = TextManager.GetWithVariable("cantdeletemodsub", "[modname]", subPackage.Name);
3514  }
3515  }
3516  }
3517  return true;
3518  }
3519  };
3520 
3521  searchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; };
3522  searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); };
3523  searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; };
3524 
3525  var sortedSubs = GetLoadableSubs()
3526  .OrderBy(s => s.Type)
3527  .ThenBy(s => s.Name)
3528  .ToList();
3529 
3530  SubmarineInfo prevSub = null;
3531 
3532  foreach (SubmarineInfo sub in sortedSubs)
3533  {
3534  if (prevSub == null || prevSub.Type != sub.Type)
3535  {
3536  string textTag = "SubmarineType." + sub.Type;
3537  if (sub.Type == SubmarineType.EnemySubmarine && !TextManager.ContainsTag(textTag))
3538  {
3539  textTag = "MissionType.Pirate";
3540  }
3541  new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 35) },
3542  TextManager.Get(textTag), font: GUIStyle.LargeFont, textAlignment: Alignment.Center, style: "ListBoxElement")
3543  {
3544  CanBeFocused = false
3545  };
3546  prevSub = sub;
3547  }
3548 
3549  string pathWithoutUserName = Path.GetFullPath(sub.FilePath);
3550  string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
3551  if (pathWithoutUserName.StartsWith(saveFolder))
3552  {
3553  pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..];
3554  }
3555  else
3556  {
3557  pathWithoutUserName = sub.FilePath;
3558  }
3559 
3560  GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) },
3561  ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80))
3562  {
3563  UserData = sub,
3564  ToolTip = pathWithoutUserName
3565  };
3566 
3567  if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false))
3568  {
3569  if (GetLocalPackageThatOwnsSub(sub) == null &&
3570  ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage)
3571  {
3572  //workshop mod
3573  textBlock.OverrideTextColor(Color.MediumPurple);
3574  }
3575  else
3576  {
3577  //local mod
3578  textBlock.OverrideTextColor(GUIStyle.TextColorBright);
3579  }
3580  }
3581 
3582  if (sub.HasTag(SubmarineTag.Shuttle))
3583  {
3584  var shuttleText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight),
3585  TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont)
3586  {
3587  TextColor = textBlock.TextColor * 0.8f,
3588  ToolTip = textBlock.ToolTip.SanitizedString
3589  };
3590  }
3591  else if (sub.IsPlayer)
3592  {
3593  var classText = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), textBlock.RectTransform, Anchor.CenterRight),
3594  TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont)
3595  {
3596  TextColor = textBlock.TextColor * 0.8f,
3597  ToolTip = textBlock.ToolTip.SanitizedString
3598  };
3599  }
3600  }
3601 
3602  var deleteButton = new GUIButton(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.TopCenter),
3603  TextManager.Get("Delete"))
3604  {
3605  Enabled = false,
3606  UserData = "delete"
3607  };
3608  deleteButton.OnClicked = (btn, userdata) =>
3609  {
3610  if (subList.SelectedComponent != null)
3611  {
3612  TryDeleteSub(subList.SelectedComponent.UserData as SubmarineInfo);
3613  }
3614  deleteButton.Enabled = false;
3615  return true;
3616  };
3617 
3618 
3619  if (AutoSaveInfo?.Root != null)
3620  {
3621  int min = Math.Min(6, AutoSaveInfo.Root.Elements().Count());
3622  var loadAutoSave = new GUIDropDown(new RectTransform(Vector2.One, deleteButtonHolder.RectTransform, Anchor.BottomCenter), TextManager.Get("LoadAutoSave"), elementCount: min)
3623  {
3624  ToolTip = TextManager.Get("LoadAutoSaveTooltip"),
3625  UserData = "loadautosave",
3626  OnSelected = (button, o) =>
3627  {
3628  LoadAutoSave(o);
3629  return true;
3630  }
3631  };
3632  foreach (XElement saveElement in AutoSaveInfo.Root.Elements().Reverse())
3633  {
3634  DateTime time = DateTime.MinValue.AddSeconds(saveElement.GetAttributeUInt64("time", 0));
3635  TimeSpan difference = DateTime.UtcNow - time;
3636 
3637  LocalizedString tooltip = TextManager.GetWithVariables("subeditor.autosaveage",
3638  ("[hours]", ((int)Math.Floor(difference.TotalHours)).ToString()),
3639  ("[minutes]", difference.Minutes.ToString()),
3640  ("[seconds]", difference.Seconds.ToString()));
3641 
3642  string submarineName = saveElement.GetAttributeString("name", TextManager.Get("UnspecifiedSubFileName").Value);
3643  LocalizedString timeFormat;
3644 
3645  double totalMinutes = difference.TotalMinutes;
3646 
3647  if (totalMinutes < 1)
3648  {
3649  timeFormat = TextManager.Get("subeditor.savedjustnow");
3650  }
3651  else if (totalMinutes > 60)
3652  {
3653  timeFormat = TextManager.Get("subeditor.savedmorethanhour");
3654  }
3655  else
3656  {
3657  timeFormat = TextManager.GetWithVariable("subeditor.saveageminutes", "[minutes]", difference.Minutes.ToString());
3658  }
3659 
3660  LocalizedString entryName = TextManager.GetWithVariables("subeditor.autosaveentry", ("[submarine]", submarineName), ("[saveage]", timeFormat));
3661 
3662  loadAutoSave.AddItem(entryName, saveElement, tooltip);
3663  }
3664  }
3665 
3666  var controlBtnHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedLoadFrame.RectTransform), isHorizontal: true) { RelativeSpacing = 0.2f, Stretch = true };
3667 
3668  new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), controlBtnHolder.RectTransform, Anchor.BottomLeft),
3669  TextManager.Get("Cancel"))
3670  {
3671  OnClicked = (GUIButton btn, object userdata) =>
3672  {
3673  loadFrame = null;
3674  return true;
3675  }
3676  };
3677 
3678  new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), controlBtnHolder.RectTransform, Anchor.BottomRight),
3679  TextManager.Get("Load"))
3680  {
3681  OnClicked = HitLoadSubButton
3682  };
3683 
3684  controlBtnHolder.RectTransform.MaxSize = new Point(int.MaxValue, controlBtnHolder.Children.First().Rect.Height);
3685  }
3686 
3687  private void FilterSubs(GUIListBox subList, string filter)
3688  {
3689  foreach (GUIComponent child in subList.Content.Children)
3690  {
3691  if (!(child.UserData is SubmarineInfo sub)) { continue; }
3692  child.Visible = string.IsNullOrEmpty(filter) || sub.Name.ToLower().Contains(filter.ToLower());
3693  }
3694 
3695  //go through the elements backwards, and disable the labels for sub categories if there's no subs visible in them
3696  bool subVisibleInCategory = false;
3697  foreach (GUIComponent child in subList.Content.Children.Reverse())
3698  {
3699  if (!(child.UserData is SubmarineInfo sub))
3700  {
3701  if (child.Enabled)
3702  {
3703  child.Visible = subVisibleInCategory;
3704  }
3705  subVisibleInCategory = false;
3706  }
3707  else
3708  {
3709  subVisibleInCategory |= child.Visible;
3710  }
3711  }
3712  }
3713 
3718  private void LoadAutoSave(object userData)
3719  {
3720  if (userData is not XElement element) { return; }
3721 
3722 #warning TODO: revise
3723  string filePath = element.GetAttributeStringUnrestricted("file", "");
3724  if (string.IsNullOrWhiteSpace(filePath)) { return; }
3725 
3726  var loadedSub = Submarine.Load(new SubmarineInfo(filePath), true);
3727 
3728  try
3729  {
3730  loadedSub.Info.Name = loadedSub.Info.SubmarineElement.GetAttributeString("name", loadedSub.Info.Name);
3731  }
3732  catch (Exception e)
3733  {
3734  DebugConsole.ThrowError("Failed to find a name for the submarine.", e);
3735  var unspecifiedFileName = TextManager.Get("UnspecifiedSubFileName");
3736  loadedSub.Info.Name = unspecifiedFileName.Value;
3737  }
3738  MainSub = loadedSub;
3739  MainSub.SetPrevTransform(MainSub.Position);
3740  MainSub.UpdateTransform();
3741  MainSub.Info.Name = loadedSub.Info.Name;
3742  subNameLabel.Text = ToolBox.LimitString(loadedSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
3743 
3744  ReconstructLayers();
3745 
3746  CreateDummyCharacter();
3747 
3748  cam.Position = MainSub.Position + MainSub.HiddenSubPosition;
3749 
3750  loadFrame = null;
3751  }
3752 
3753  private bool HitLoadSubButton(GUIButton button, object obj)
3754  {
3755  if (loadFrame == null)
3756  {
3757  DebugConsole.NewMessage("load frame null", Color.Red);
3758  return false;
3759  }
3760 
3761  GUIListBox subList = loadFrame.GetAnyChild<GUIListBox>();
3762  if (subList == null)
3763  {
3764  DebugConsole.NewMessage("Sublist null", Color.Red);
3765  return false;
3766  }
3767 
3768  if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; }
3769 
3770  var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo);
3771  if (ownerPackage is null)
3772  {
3773  if (IsVanillaSub(selectedSubInfo))
3774  {
3775 #if DEBUG
3776  LoadSub(selectedSubInfo);
3777 #else
3778  AskLoadVanillaSub(selectedSubInfo);
3779 #endif
3780  }
3781  else if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage)
3782  {
3783  if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId)
3784  && publishedWorkshopItemIds.Contains(workshopId.Value))
3785  {
3786  AskLoadPublishedSub(selectedSubInfo, workshopPackage);
3787  }
3788  else
3789  {
3790  AskLoadSubscribedSub(selectedSubInfo);
3791  }
3792  }
3793  }
3794  else
3795  {
3796  LoadSub(selectedSubInfo);
3797  }
3798  return false;
3799  }
3800 
3801  void AskLoadSub(SubmarineInfo info, LocalizedString header, LocalizedString desc)
3802  {
3803  var msgBox = new GUIMessageBox(
3804  header,
3805  desc,
3806  new[] { TextManager.Get("LoadAnyway"), TextManager.Get("Cancel") });
3807  msgBox.Buttons[0].OnClicked = (button, o) =>
3808  {
3809  LoadSub(info);
3810  msgBox.Close();
3811  return false;
3812  };
3813  msgBox.Buttons[1].OnClicked = msgBox.Close;
3814  }
3815 
3816  void AskLoadPublishedSub(SubmarineInfo info, ContentPackage pkg)
3817  => AskLoadSub(info,
3818  TextManager.Get("LoadingPublishedSubmarineHeader"),
3819  TextManager.GetWithVariable("LoadingPublishedSubmarineDesc", "[modname]", pkg.Name));
3820 
3821  void AskLoadSubscribedSub(SubmarineInfo info)
3822  => AskLoadSub(info,
3823  TextManager.Get("LoadingSubscribedSubmarineHeader"),
3824  TextManager.Get("LoadingSubscribedSubmarineDesc"));
3825 
3826  void AskLoadVanillaSub(SubmarineInfo info)
3827  => AskLoadSub(info,
3828  TextManager.Get("LoadingVanillaSubmarineHeader"),
3829  TextManager.Get("LoadingVanillaSubmarineDesc"));
3830 
3831  public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true)
3832  {
3833  Submarine.Unload();
3834  Submarine selectedSub = null;
3835 
3836  if (checkIdConflicts)
3837  {
3838  Dictionary<int, Identifier> entities = new Dictionary<int, Identifier>();
3839  foreach (var subElement in info.SubmarineElement.Elements())
3840  {
3841  int id = subElement.GetAttributeInt("ID", -1);
3842  if (id == -1) { continue; }
3843  Identifier identifier = subElement.GetAttributeIdentifier("identifier", string.Empty);
3844  if (entities.TryGetValue(id, out Identifier duplicateEntity))
3845  {
3846  var errorMsg = new GUIMessageBox(
3847  TextManager.Get("error"),
3848  TextManager.GetWithVariables("subeditor.duplicateiderror",
3849  ("[entity1]", $"{duplicateEntity} ({id})"),
3850  ("[entity2]", $"{identifier} ({id})")),
3851  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
3852  errorMsg.Buttons[0].OnClicked = (bnt, userdata) =>
3853  {
3854  subElement.Remove();
3855  LoadSub(info, checkIdConflicts: false);
3856  errorMsg.Close();
3857  return true;
3858  };
3859  errorMsg.Buttons[1].OnClicked = (bnt, userdata) =>
3860  {
3861  LoadSub(info, checkIdConflicts: false);
3862  errorMsg.Close();
3863  return true;
3864  };
3865  return;
3866  }
3867  entities.Add(id, identifier);
3868  }
3869  }
3870 
3871  try
3872  {
3873  selectedSub = new Submarine(info);
3874  MainSub = selectedSub;
3875  MainSub.UpdateTransform(interpolate: false);
3876  }
3877  catch (Exception e)
3878  {
3879  DebugConsole.ThrowError("Failed to load the submarine. The submarine file might be corrupted.", e);
3880  return;
3881  }
3882  ClearUndoBuffer();
3883  CreateDummyCharacter();
3884 
3885  string name = MainSub.Info.Name;
3886  subNameLabel.Text = ToolBox.LimitString(name, subNameLabel.Font, subNameLabel.Rect.Width);
3887 
3888  cam.Position = MainSub.Position + MainSub.HiddenSubPosition;
3889 
3890  loadFrame = null;
3891 
3892  if (selectedSub.Info.GameVersion < new Version("0.8.9.0"))
3893  {
3894  var adjustLightsPrompt = new GUIMessageBox(TextManager.Get("Warning"), TextManager.Get("AdjustLightsPrompt"),
3895  new[] { TextManager.Get("Yes"), TextManager.Get("No") });
3896  adjustLightsPrompt.Buttons[0].OnClicked += adjustLightsPrompt.Close;
3897  adjustLightsPrompt.Buttons[0].OnClicked += (btn, userdata) =>
3898  {
3899  foreach (Item item in Item.ItemList)
3900  {
3901  if (item.ParentInventory != null || item.body != null) continue;
3902  var lightComponent = item.GetComponent<LightComponent>();
3903  foreach (var light in item.GetComponents<LightComponent>())
3904  {
3905  light.LightColor = new Color(light.LightColor, light.LightColor.A / 255.0f * 0.5f);
3906  }
3907  }
3908  new GUIMessageBox("", TextManager.Get("AdjustedLightsNotification"));
3909  return true;
3910  };
3911  adjustLightsPrompt.Buttons[1].OnClicked += adjustLightsPrompt.Close;
3912  }
3913 
3914  ReconstructLayers();
3915  }
3916 
3917  private static ContentPackage GetPackageThatOwnsSub(SubmarineInfo sub, IEnumerable<ContentPackage> packages)
3918  => packages.FirstOrDefault(package => package.Files.Any(f => f.Path == sub.FilePath));
3919 
3920  private static ContentPackage GetLocalPackageThatOwnsSub(SubmarineInfo sub)
3921  => GetPackageThatOwnsSub(sub, ContentPackageManager.LocalPackages);
3922 
3923  private static ContentPackage GetWorkshopPackageThatOwnsSub(SubmarineInfo sub)
3924  => GetPackageThatOwnsSub(sub, ContentPackageManager.WorkshopPackages);
3925 
3926  private static bool IsVanillaSub(SubmarineInfo sub)
3927  => GetPackageThatOwnsSub(sub, ContentPackageManager.VanillaCorePackage.ToEnumerable()) != null;
3928 
3929  private void TryDeleteSub(SubmarineInfo sub)
3930  {
3931  if (sub == null) { return; }
3932 
3933  //If the sub is included in a content package that only defines that one sub,
3934  //check that it's a local content package and only allow deletion if it is.
3935  //(deleting from the Submarines folder is also currently allowed, but this is temporary)
3936  var subPackage = GetLocalPackageThatOwnsSub(sub);
3937  if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; }
3938 
3939  var msgBox = new GUIMessageBox(
3940  TextManager.Get("DeleteDialogLabel"),
3941  TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name),
3942  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
3943  msgBox.Buttons[0].OnClicked += (btn, userData) =>
3944  {
3945  try
3946  {
3947  if (subPackage != null)
3948  {
3949  File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false);
3950  ModProject modProject = new ModProject(subPackage);
3951  modProject.RemoveFile(modProject.Files.First(f => ContentPath.FromRaw(subPackage, f.Path) == sub.FilePath));
3952  modProject.Save(subPackage.Path, catchUnauthorizedAccessExceptions: true);
3953  ReloadModifiedPackage(subPackage);
3954  if (MainSub?.Info != null && MainSub.Info.FilePath == sub.FilePath)
3955  {
3956  MainSub.Info.FilePath = null;
3957  }
3958  }
3959  sub.Dispose();
3960  CreateLoadScreen();
3961  }
3962  catch (Exception e)
3963  {
3964  DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
3965  }
3966  return true;
3967  };
3968  msgBox.Buttons[0].OnClicked += msgBox.Close;
3969  msgBox.Buttons[1].OnClicked += msgBox.Close;
3970  }
3971 
3972  private void OpenEntityMenu(MapEntityCategory? entityCategory)
3973  {
3974  UpdateEntityList();
3975 
3976  foreach (GUIButton categoryButton in entityCategoryButtons)
3977  {
3978  categoryButton.Selected = entityCategory.HasValue ?
3979  categoryButton.UserData is MapEntityCategory category && entityCategory.Value == category :
3980  categoryButton.UserData == null;
3981  string categoryName = entityCategory.HasValue ? entityCategory.Value.ToString() : "All";
3982  selectedCategoryText.Text = TextManager.Get("MapEntityCategory." + categoryName);
3983  selectedCategoryButton.ApplyStyle(GUIStyle.GetComponentStyle("CategoryButton." + categoryName));
3984  }
3985 
3986  selectedCategory = entityCategory;
3987 
3988  SetMode(Mode.Default);
3989 
3990  saveFrame = null;
3991  loadFrame = null;
3992 
3993  foreach (GUIComponent child in toggleEntityMenuButton.Children)
3994  {
3995  child.SpriteEffects = entityMenuOpen ? SpriteEffects.None : SpriteEffects.FlipVertically;
3996  }
3997 
3998  foreach (GUIComponent child in categorizedEntityList.Content.Children)
3999  {
4000  child.Visible = !entityCategory.HasValue || (MapEntityCategory)child.UserData == entityCategory;
4001  var innerList = child.GetChild<GUIListBox>();
4002  foreach (GUIComponent grandChild in innerList.Content.Children)
4003  {
4004  grandChild.Visible = true;
4005  }
4006  }
4007 
4008  if (!string.IsNullOrEmpty(entityFilterBox.Text))
4009  {
4010  FilterEntities(entityFilterBox.Text);
4011  }
4012 
4013  categorizedEntityList.UpdateScrollBarSize();
4014  categorizedEntityList.BarScroll = 0.0f;
4015  // categorizedEntityList.Visible = true;
4016  // allEntityList.Visible = false;
4017  }
4018 
4019  private void FilterEntities(string filter)
4020  {
4021  if (string.IsNullOrWhiteSpace(filter))
4022  {
4023  allEntityList.Visible = false;
4024  categorizedEntityList.Visible = true;
4025 
4026  foreach (GUIComponent child in categorizedEntityList.Content.Children)
4027  {
4028  child.Visible = !selectedCategory.HasValue || selectedCategory == (MapEntityCategory)child.UserData;
4029  if (!child.Visible) { return; }
4030  var innerList = child.GetChild<GUIListBox>();
4031  foreach (GUIComponent grandChild in innerList.Content.Children)
4032  {
4033  grandChild.Visible = ((MapEntityPrefab)grandChild.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase);
4034  }
4035  };
4036  categorizedEntityList.UpdateScrollBarSize();
4037  categorizedEntityList.BarScroll = 0.0f;
4038  return;
4039  }
4040 
4041  allEntityList.Visible = true;
4042  categorizedEntityList.Visible = false;
4043  filter = filter.ToLower();
4044  foreach (GUIComponent child in allEntityList.Content.Children)
4045  {
4046  child.Visible =
4047  (!selectedCategory.HasValue || ((MapEntityPrefab)child.UserData).Category.HasFlag(selectedCategory)) &&
4048  ((MapEntityPrefab)child.UserData).Name.Value.Contains(filter, StringComparison.OrdinalIgnoreCase);
4049  }
4050  allEntityList.UpdateScrollBarSize();
4051  allEntityList.BarScroll = 0.0f;
4052  }
4053 
4054  private void ClearFilter()
4055  {
4056  FilterEntities("");
4057  categorizedEntityList.UpdateScrollBarSize();
4058  categorizedEntityList.BarScroll = 0.0f;
4059  entityFilterBox.Text = "";
4060  }
4061 
4062  public void SetMode(Mode newMode)
4063  {
4064  if (newMode == mode) { return; }
4065  mode = newMode;
4066 
4067  lockMode = true;
4068  defaultModeTickBox.Selected = newMode == Mode.Default;
4069  wiringModeTickBox.Selected = newMode == Mode.Wiring;
4070  lockMode = false;
4071 
4073 
4076  ClearUndoBuffer();
4077 
4078  CreateDummyCharacter();
4079  if (newMode == Mode.Wiring)
4080  {
4081  var item = new Item(MapEntityPrefab.Find(null, "screwdriver") as ItemPrefab, Vector2.Zero, null);
4082  dummyCharacter.Inventory.TryPutItem(item, null, new List<InvSlotType>() { InvSlotType.RightHand });
4083  Point wirePos = new Point((int)(10 * GUI.Scale), TopPanel.Rect.Height + entityCountPanel.Rect.Height + (int)(10 * GUI.Scale));
4084  wiringToolPanel = CreateWiringPanel(wirePos, SelectWire);
4085  }
4086  }
4087 
4088  private void RemoveDummyCharacter()
4089  {
4090  if (dummyCharacter == null || dummyCharacter.Removed) { return; }
4091 
4092  dummyCharacter.Inventory.AllItems.ForEachMod(it => it.Remove());
4093  dummyCharacter.Remove();
4094  dummyCharacter = null;
4095  }
4096 
4097  private void CreateContextMenu()
4098  {
4099  if (GUIContextMenu.CurrentContextMenu != null) { return; }
4100 
4101  List<MapEntity> targets = MapEntity.HighlightedEntities.Any(me => !MapEntity.SelectedList.Contains(me)) ?
4102  MapEntity.HighlightedEntities.ToList() :
4103  new List<MapEntity>(MapEntity.SelectedList);
4104 
4105  bool allowOpening = false;
4106  var targetItem = (targets.Count == 1 ? targets.Single() : null) as Item;
4107  // Do not offer the ability to open the inventory if the inventory should never be drawn
4108  allowOpening = targetItem is not null && targetItem.Components.Any(static ic =>
4109  ic is not ConnectionPanel &&
4110  ic is not Repairable &&
4111  ic is not ItemContainer { DrawInventory: false } &&
4112  ic.GuiFrame != null);
4113 
4114  bool hasTargets = targets.Count > 0;
4115 
4116  // Holding shift brings up special context menu options
4117  if (PlayerInput.IsShiftDown())
4118  {
4119  GUIContextMenu.CreateContextMenu(
4120  new ContextMenuOption("SubEditor.EditBackgroundColor", isEnabled: true, onSelected: CreateBackgroundColorPicker),
4121  new ContextMenuOption("SubEditor.ToggleTransparency", isEnabled: true, onSelected: () => TransparentWiringMode = !TransparentWiringMode),
4122  new ContextMenuOption("SubEditor.ToggleGrid", isEnabled: true, onSelected: () => ShouldDrawGrid = !ShouldDrawGrid),
4123  new ContextMenuOption("SubEditor.PasteAssembly", isEnabled: true, () => PasteAssembly()),
4124  new ContextMenuOption("Editor.SelectSame", isEnabled: hasTargets, onSelected: delegate
4125  {
4126  bool doorGapSelected = targets.Any(t => t is Gap gap && gap.ConnectedDoor != null);
4127  foreach (MapEntity match in MapEntity.MapEntityList.Where(e => e.Prefab != null && targets.Any(t => t.Prefab?.Identifier == e.Prefab.Identifier) && !MapEntity.SelectedList.Contains(e)))
4128  {
4129  if (MapEntity.SelectedList.Contains(match)) { continue; }
4130  if (match is Gap gap)
4131  {
4132  //don't add non-door gaps if we've selected a door gap (and vice versa)
4133  if ((gap.ConnectedDoor == null) == doorGapSelected) { continue; }
4134  }
4135  else if (match is Item item)
4136  {
4137  //add door gaps too if we're selecting doors
4138  var door = item.GetComponent<Door>();
4139  if (door?.LinkedGap != null && !MapEntity.SelectedList.Contains(door.LinkedGap))
4140  {
4141  MapEntity.SelectedList.Add(door.LinkedGap);
4142  }
4143  }
4144  MapEntity.SelectedList.Add(match);
4145  }
4146  }),
4147  new ContextMenuOption("SubEditor.AddImage", isEnabled: true, onSelected: ImageManager.CreateImageWizard),
4148  new ContextMenuOption("SubEditor.ToggleImageEditing", isEnabled: true, onSelected: delegate
4149  {
4150  ImageManager.EditorMode = !ImageManager.EditorMode;
4151  if (!ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); }
4152  }));
4153  }
4154  else
4155  {
4156  List<ContextMenuOption> availableLayers = new List<ContextMenuOption>
4157  {
4158  new ContextMenuOption("editor.layer.nolayer", true, onSelected: () => { MoveToLayer(null, targets); })
4159  };
4160  availableLayers.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
4161 
4162  List<ContextMenuOption> availableLayerOptions = new List<ContextMenuOption>
4163  {
4164  new ContextMenuOption("editor.layer.movetolayer", isEnabled: hasTargets, availableLayers.ToArray()),
4165  new ContextMenuOption("editor.layer.createlayer", isEnabled: hasTargets, onSelected: () => { CreateNewLayer(null, targets); }),
4166  new ContextMenuOption("editor.layer.selectall", isEnabled: hasTargets, onSelected: () =>
4167  {
4168  foreach (MapEntity match in MapEntity.MapEntityList.Where(e => targets.Any(t => !string.IsNullOrWhiteSpace(t.Layer) && t.Layer == e.Layer && !MapEntity.SelectedList.Contains(e))))
4169  {
4170  if (MapEntity.SelectedList.Contains(match)) { continue; }
4171  MapEntity.SelectedList.Add(match);
4172  }
4173  }),
4174  };
4175  availableLayerOptions.AddRange(Layers.Select(layer => new ContextMenuOption(layer.Key, true, onSelected: () => { MoveToLayer(layer.Key, targets); })));
4176 
4177  GUIContextMenu.CreateContextMenu(
4178  new ContextMenuOption("label.openlabel", isEnabled: allowOpening, onSelected: () => OpenItem(targetItem)),
4179  new ContextMenuOption("editor.cut", isEnabled: hasTargets, onSelected: () => MapEntity.Cut(targets)),
4180  new ContextMenuOption("editor.copytoclipboard", isEnabled: hasTargets, onSelected: () => MapEntity.Copy(targets)),
4181  new ContextMenuOption("editor.paste", isEnabled: MapEntity.CopiedList.Any(), onSelected: () => MapEntity.Paste(cam.ScreenToWorld(PlayerInput.MousePosition))),
4182  new ContextMenuOption("delete", isEnabled: hasTargets, onSelected: () =>
4183  {
4184  StoreCommand(new AddOrDeleteCommand(targets, true));
4185  foreach (var me in targets)
4186  {
4187  if (!me.Removed) { me.Remove(); }
4188  }
4189  }),
4190  new ContextMenuOption(string.Empty, isEnabled: false, onSelected: () => { /*do nothing*/ }),
4191  new ContextMenuOption("editor.layer.movetoactivelayer", isEnabled: !(layerList?.SelectedData as string).IsNullOrEmpty(), onSelected: () => { MoveToLayer(layerList.SelectedData as string, targets); }),
4192  new ContextMenuOption("editor.layer.removefromlayer", isEnabled: targets.Any(t => t.Layer != string.Empty), onSelected: () => { targets.ForEach(t => t.Layer = string.Empty); }),
4193  new ContextMenuOption("editor.layeroptions", isEnabled: hasTargets, availableLayerOptions.ToArray()),
4194  new ContextMenuOption(TextManager.GetWithVariable("editortip.shiftforextraoptions", "[button]", PlayerInput.SecondaryMouseLabel) + '\n' + TextManager.Get("editortip.altforruler"), isEnabled: false, onSelected: null));
4195  }
4196  }
4197 
4198  private void MoveToLayer(string layer, List<MapEntity> content)
4199  {
4200  layer ??= string.Empty;
4201 
4202  foreach (MapEntity entity in content)
4203  {
4204  if (MapEntity.SelectedList.Contains(entity))
4205  {
4206  MapEntity.ResetEditingHUD();
4207  }
4208  entity.Layer = layer;
4209  }
4210  }
4211 
4212  private void CreateNewLayer(string name, List<MapEntity> content)
4213  {
4214  if (string.IsNullOrWhiteSpace(name))
4215  {
4216  name = TextManager.Get("editor.layer.newlayer").Value;
4217  }
4218 
4219  string incrementedName = name;
4220 
4221  for (int i = 1; Layers.ContainsKey(incrementedName); i++)
4222  {
4223  incrementedName = $"{name} ({i})";
4224  }
4225 
4226  name = incrementedName;
4227 
4228  if (content != null)
4229  {
4230  MoveToLayer(name, content);
4231  }
4232 
4233  Layers.Add(name, new LayerData());
4234  UpdateLayerPanel();
4235  }
4236 
4237  private void RenameLayer(string original, string newName)
4238  {
4239  Layers.Remove(original, out LayerData originalData);
4240 
4241  foreach (MapEntity entity in MapEntity.MapEntityList.Where(entity => entity.Layer == original))
4242  {
4243  entity.Layer = newName ?? string.Empty;
4244  }
4245 
4246  if (!string.IsNullOrWhiteSpace(newName))
4247  {
4248  Layers.TryAdd(newName, originalData);
4249  }
4250  UpdateLayerPanel();
4251  }
4252 
4253  public void ReconstructLayers()
4254  {
4255  ClearLayers();
4256  foreach (MapEntity entity in MapEntity.MapEntityList)
4257  {
4258  if (!string.IsNullOrWhiteSpace(entity.Layer))
4259  {
4260  Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
4261  }
4262  }
4263  UpdateLayerPanel();
4264  }
4265 
4266  private void ClearLayers()
4267  {
4268  Layers.Clear();
4269  UpdateLayerPanel();
4270  }
4271 
4272  private static void SetLayerVisibility(string layerName, bool isVisible)
4273  {
4274  if (Layers.Remove(layerName, out LayerData layerData))
4275  {
4276  Layers.Add(layerName, layerData with { IsVisible = isVisible });
4277  }
4278  else
4279  {
4280  Layers.Add(layerName, new LayerData(isVisible));
4281  }
4282  }
4283 
4284  private void PasteAssembly(string text = null, Vector2? pos = null)
4285  {
4286  pos ??= cam.ScreenToWorld(PlayerInput.MousePosition);
4287  text ??= Clipboard.GetText();
4288  if (string.IsNullOrWhiteSpace(text))
4289  {
4290  DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is empty.");
4291  return;
4292  }
4293 
4294  XElement element = null;
4295 
4296  try
4297  {
4298  element = XDocument.Parse(text).Root;
4299  }
4300  catch (Exception) { /* ignored */ }
4301 
4302  if (element == null)
4303  {
4304  DebugConsole.ThrowError("Unable to paste assembly: Clipboard content is not valid XML.");
4305  return;
4306  }
4307 
4308  Submarine sub = MainSub;
4309  List<MapEntity> entities;
4310  try
4311  {
4312  entities = ItemAssemblyPrefab.PasteEntities(pos.Value, sub, element, selectInstance: true);
4313  }
4314  catch (Exception e)
4315  {
4316  DebugConsole.ThrowError("Unable to paste assembly: Failed to load items.", e);
4317  return;
4318  }
4319 
4320  if (!entities.Any()) { return; }
4321  StoreCommand(new AddOrDeleteCommand(entities, false, handleInventoryBehavior: false));
4322  }
4323 
4324  public static GUIMessageBox CreatePropertyColorPicker(Color originalColor, SerializableProperty property, ISerializableEntity entity)
4325  {
4326  var entities = new List<(ISerializableEntity Entity, Color OriginalColor, SerializableProperty Property)> { (entity, originalColor, property) };
4327 
4328  foreach (ISerializableEntity selectedEntity in MapEntity.SelectedList.Where(selectedEntity => selectedEntity is ISerializableEntity && entity != selectedEntity).Cast<ISerializableEntity>())
4329  {
4330  switch (entity)
4331  {
4332  case ItemComponent _ when selectedEntity is Item item:
4333  foreach (var component in item.Components)
4334  {
4335  if (component.GetType() == entity.GetType() && component != entity)
4336  {
4337  entities.Add((component, (Color) property.GetValue(component), property));
4338  }
4339  }
4340  break;
4341  default:
4342  if (selectedEntity.GetType() == entity.GetType())
4343  {
4344  entities.Add((selectedEntity, (Color) property.GetValue(selectedEntity), property));
4345  }
4346  else if (selectedEntity is { SerializableProperties: { } props} )
4347  {
4348  if (props.TryGetValue(property.Name.ToIdentifier(), out SerializableProperty foundProp))
4349  {
4350  entities.Add((selectedEntity, (Color) foundProp.GetValue(selectedEntity), foundProp));
4351  }
4352  }
4353  break;
4354  }
4355  }
4356 
4357  bool setValues = true;
4358  object sliderMutex = new object(),
4359  sliderTextMutex = new object(),
4360  pickerMutex = new object(),
4361  hexMutex = new object();
4362 
4363  Vector2 relativeSize = new Vector2(0.4f * GUI.AspectRatioAdjustment, 0.3f);
4364 
4365  GUIMessageBox msgBox = new GUIMessageBox(string.Empty, string.Empty, Array.Empty<LocalizedString>(), relativeSize, type: GUIMessageBox.Type.Vote)
4366  {
4367  UserData = "colorpicker",
4368  Draggable = true
4369  };
4370 
4371  GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(Vector2.One, msgBox.Content.RectTransform));
4372  GUITextBlock headerText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), contentLayout.RectTransform), property.Name, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopCenter)
4373  {
4374  AutoScaleVertical = true
4375  };
4376 
4377  GUILayoutGroup colorLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), contentLayout.RectTransform), isHorizontal: true);
4378 
4379  GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), contentLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true)
4380  {
4381  RelativeSpacing = 0.1f,
4382  Stretch = true
4383  };
4384 
4385  GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("OK"), textAlignment: Alignment.Center);
4386  GUIButton cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("Cancel"), textAlignment: Alignment.Center);
4387 
4388  contentLayout.Recalculate();
4389  colorLayout.Recalculate();
4390 
4391  GUIColorPicker colorPicker = new GUIColorPicker(new RectTransform(new Point(colorLayout.Rect.Height), colorLayout.RectTransform));
4392  var (h, s, v) = ToolBox.RGBToHSV(originalColor);
4393  colorPicker.SelectedHue = float.IsNaN(h) ? 0f : h;
4394  colorPicker.SelectedSaturation = s;
4395  colorPicker.SelectedValue = v;
4396 
4397  colorLayout.Recalculate();
4398 
4399  GUILayoutGroup sliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f - colorPicker.RectTransform.RelativeSize.X, 1f), colorLayout.RectTransform), childAnchor: Anchor.TopRight);
4400 
4401  float currentHue = colorPicker.SelectedHue / 360f;
4402  GUILayoutGroup hueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.25f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
4403  new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), hueSliderLayout.RectTransform), text: "H:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Hue" };
4404  GUIScrollBar hueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), hueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = currentHue };
4405  GUINumberInput hueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), hueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) },
4406  inputType: NumberType.Float) { FloatValue = currentHue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 };
4407 
4408  GUILayoutGroup satSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
4409  new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), satSliderLayout.RectTransform), text: "S:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Saturation"};
4410  GUIScrollBar satScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), satSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedSaturation };
4411  GUINumberInput satTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), satSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) },
4412  inputType: NumberType.Float) { FloatValue = colorPicker.SelectedSaturation, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 };
4413 
4414  GUILayoutGroup valueSliderLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.2f), sliderLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true };
4415  new GUITextBlock(new RectTransform(new Vector2(0.1f, 0.2f), valueSliderLayout.RectTransform), text: "V:", font: GUIStyle.SubHeadingFont) { Padding = Vector4.Zero, ToolTip = "Value"};
4416  GUIScrollBar valueScrollBar = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1f), valueSliderLayout.RectTransform), style: "GUISlider", barSize: 0.05f) { BarScroll = colorPicker.SelectedValue };
4417  GUINumberInput valueTextBox = new GUINumberInput(new RectTransform(new Vector2(0.2f, 1f), valueSliderLayout.RectTransform) { MinSize = new Point(GUI.IntScale(100), 0) },
4418  inputType: NumberType.Float) { FloatValue = colorPicker.SelectedValue, MaxValueFloat = 1f, MinValueFloat = 0f, DecimalsToDisplay = 2 };
4419 
4420  GUILayoutGroup colorInfoLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.3f), sliderLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true)
4421  {
4422  Stretch = true,
4423  RelativeSpacing = 0.1f
4424  };
4425 
4426  new GUICustomComponent(new RectTransform(Vector2.One, colorInfoLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), (batch, component) =>
4427  {
4428  Rectangle rect = component.Rect;
4429  Point areaSize = new Point(rect.Width, rect.Height / 2);
4430  Rectangle newColorRect = new Rectangle(rect.Location, areaSize);
4431  Rectangle oldColorRect = new Rectangle(new Point(newColorRect.Left, newColorRect.Bottom), areaSize);
4432 
4433  GUI.DrawRectangle(batch, newColorRect, ToolBoxCore.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue), isFilled: true);
4434  GUI.DrawRectangle(batch, oldColorRect, originalColor, isFilled: true);
4435  GUI.DrawRectangle(batch, rect, Color.Black, isFilled: false);
4436  });
4437 
4438  GUITextBox hexValueBox = new GUITextBox(new RectTransform(new Vector2(0.3f, 1f), colorInfoLayout.RectTransform), text: ColorToHex(originalColor), createPenIcon: false) { OverflowClip = true };
4439 
4440  hueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; };
4441  hueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); };
4442 
4443  satScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; };
4444  satTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); };
4445 
4446  valueScrollBar.OnMoved = (bar, scroll) => { SetColor(sliderMutex); return true; };
4447  valueTextBox.OnValueChanged = input => { SetColor(sliderTextMutex); };
4448 
4449  colorPicker.OnColorSelected = (component, color) => { SetColor(pickerMutex); return true; };
4450 
4451  hexValueBox.OnEnterPressed = (box, text) => { SetColor(hexMutex); return true; };
4452  hexValueBox.OnDeselected += (sender, key) => { SetColor(hexMutex); };
4453 
4454  closeButton.OnClicked = (button, o) =>
4455  {
4456  colorPicker.Dispose();
4457  msgBox.Close();
4458 
4459  Color newColor = SetColor(null);
4460 
4461  if (!IsSubEditor()) { return true; }
4462 
4463  Dictionary<object, List<ISerializableEntity>> oldProperties = new Dictionary<object, List<ISerializableEntity>>();
4464 
4465  foreach (var (sEntity, color, _) in entities)
4466  {
4467  if (sEntity is MapEntity { Removed: true }) { continue; }
4468  if (!oldProperties.ContainsKey(color))
4469  {
4470  oldProperties.Add(color, new List<ISerializableEntity>());
4471  }
4472  oldProperties[color].Add(sEntity);
4473  }
4474 
4475  List<ISerializableEntity> affected = entities.Select(t => t.Entity).Where(se => se is MapEntity { Removed: false } || se is ItemComponent).ToList();
4476  StoreCommand(new PropertyCommand(affected, property.Name.ToIdentifier(), newColor, oldProperties));
4477 
4478  if (MapEntity.EditingHUD != null && (MapEntity.EditingHUD.UserData == entity || (!(entity is ItemComponent ic) || MapEntity.EditingHUD.UserData == ic.Item)))
4479  {
4481  if (list != null)
4482  {
4483  IEnumerable<SerializableEntityEditor> editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast<SerializableEntityEditor>();
4485  foreach (SerializableEntityEditor editor in editors)
4486  {
4487  if (editor.UserData == entity && editor.Fields.TryGetValue(property.Name.ToIdentifier(), out GUIComponent[] _))
4488  {
4489  editor.UpdateValue(property, newColor, flash: false);
4490  }
4491  }
4493  }
4494  }
4495  return true;
4496  };
4497 
4498  cancelButton.OnClicked = (button, o) =>
4499  {
4500  colorPicker.Dispose();
4501  msgBox.Close();
4502 
4503  foreach (var (e, color, prop) in entities)
4504  {
4505  if (e is MapEntity { Removed: true }) { continue; }
4506  prop.TrySetValue(e, color);
4507  }
4508  return true;
4509  };
4510 
4511  return msgBox;
4512 
4513  Color SetColor(object source)
4514  {
4515  if (setValues)
4516  {
4517  setValues = false;
4518 
4519  if (source == sliderMutex)
4520  {
4521  Vector3 hsv = new Vector3(hueScrollBar.BarScroll * 360f, satScrollBar.BarScroll, valueScrollBar.BarScroll);
4522  SetSliderTexts(hsv);
4523  SetColorPicker(hsv);
4524  SetHex(hsv);
4525  }
4526  else if (source == sliderTextMutex)
4527  {
4528  Vector3 hsv = new Vector3(hueTextBox.FloatValue * 360f, satTextBox.FloatValue, valueTextBox.FloatValue);
4529  SetSliders(hsv);
4530  SetColorPicker(hsv);
4531  SetHex(hsv);
4532  }
4533  else if (source == pickerMutex)
4534  {
4535  Vector3 hsv = new Vector3(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue);
4536  SetSliders(hsv);
4537  SetSliderTexts(hsv);
4538  SetHex(hsv);
4539  }
4540  else if (source == hexMutex)
4541  {
4542  Vector3 hsv = ToolBox.RGBToHSV(XMLExtensions.ParseColor(hexValueBox.Text, errorMessages: false));
4543  if (float.IsNaN(hsv.X)) { hsv.X = 0f; }
4544  SetSliders(hsv);
4545  SetSliderTexts(hsv);
4546  SetColorPicker(hsv);
4547  SetHex(hsv);
4548  }
4549 
4550  setValues = true;
4551  }
4552 
4553  Color color = ToolBoxCore.HSVToRGB(colorPicker.SelectedHue, colorPicker.SelectedSaturation, colorPicker.SelectedValue);
4554  foreach (var (e, origColor, prop) in entities)
4555  {
4556  if (e is MapEntity { Removed: true }) { continue; }
4557  color.A = origColor.A;
4558  prop.TrySetValue(e, color);
4559  }
4560  return color;
4561 
4562  void SetSliders(Vector3 hsv)
4563  {
4564  hueScrollBar.BarScroll = hsv.X / 360f;
4565  satScrollBar.BarScroll = hsv.Y;
4566  valueScrollBar.BarScroll = hsv.Z;
4567  }
4568 
4569  void SetSliderTexts(Vector3 hsv)
4570  {
4571  hueTextBox.FloatValue = hsv.X / 360f;
4572  satTextBox.FloatValue = hsv.Y;
4573  valueTextBox.FloatValue = hsv.Z;
4574  }
4575 
4576  void SetColorPicker(Vector3 hsv)
4577  {
4578  bool hueChanged = !MathUtils.NearlyEqual(colorPicker.SelectedHue, hsv.X);
4579  colorPicker.SelectedHue = hsv.X;
4580  colorPicker.SelectedSaturation = hsv.Y;
4581  colorPicker.SelectedValue = hsv.Z;
4582  if (hueChanged) { colorPicker.RefreshHue(); }
4583  }
4584 
4585  void SetHex(Vector3 hsv)
4586  {
4587  Color hexColor = ToolBoxCore.HSVToRGB(hsv.X, hsv.Y, hsv.Z);
4588  hexValueBox!.Text = ColorToHex(hexColor);
4589  }
4590  }
4591 
4592  static string ColorToHex(Color color) => $"#{(color.R << 16 | color.G << 8 | color.B):X6}";
4593  }
4594 
4595  public static GUIFrame CreateWiringPanel(Point offset, GUIListBox.OnSelectedHandler onWireSelected)
4596  {
4597  GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(0.03f, 0.35f), GUI.Canvas)
4598  { MinSize = new Point(120, 300), AbsoluteOffset = offset });
4599 
4600  GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center))
4601  {
4602  PlaySoundOnSelect = true,
4603  OnSelected = onWireSelected,
4604  CanTakeKeyBoardFocus = false
4605  };
4606 
4607  List<ItemPrefab> wirePrefabs = new List<ItemPrefab>();
4608 
4609  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
4610  {
4611  if (itemPrefab.Name.IsNullOrEmpty() || itemPrefab.HideInMenus || itemPrefab.HideInEditors) { continue; }
4612  if (!itemPrefab.Tags.Contains(Tags.WireItem)) { continue; }
4613  if (CircuitBox.IsInGame() && itemPrefab.Tags.Contains(Tags.Thalamus)) { continue; }
4614 
4615  wirePrefabs.Add(itemPrefab);
4616  }
4617 
4618  foreach (ItemPrefab itemPrefab in wirePrefabs.OrderBy(static w => !w.CanBeBought).ThenBy(static w => w.UintIdentifier))
4619  {
4620  GUIFrame imgFrame = new GUIFrame(new RectTransform(new Point(listBox.Content.Rect.Width, listBox.Rect.Width / 2), listBox.Content.RectTransform), style: "ListBoxElement")
4621  {
4622  UserData = itemPrefab
4623  };
4624  var img = new GUIImage(new RectTransform(new Vector2(0.9f), imgFrame.RectTransform, Anchor.Center), itemPrefab.Sprite, scaleToFit: true)
4625  {
4626  UserData = itemPrefab,
4627  Color = itemPrefab.SpriteColor,
4628  HoverColor = Color.Lerp(itemPrefab.SpriteColor, Color.White, 0.3f),
4629  SelectedColor = Color.Lerp(itemPrefab.SpriteColor, Color.White, 0.6f)
4630  };
4631  }
4632 
4633  return frame;
4634  }
4635 
4636  private bool SelectLinkedSub(GUIComponent selected, object userData)
4637  {
4638  if (userData is not SubmarineInfo submarine) { return false; }
4639  var prefab = new LinkedSubmarinePrefab(submarine);
4640  MapEntityPrefab.SelectPrefab(prefab);
4641  return true;
4642  }
4643 
4644  private bool SelectWire(GUIComponent component, object userData)
4645  {
4646  if (dummyCharacter == null) return false;
4647 
4648  //if the same type of wire has already been selected, deselect it and return
4649  Item existingWire = dummyCharacter.HeldItems.FirstOrDefault(i => i.Prefab == userData as ItemPrefab);
4650  if (existingWire != null)
4651  {
4652  existingWire.Drop(null);
4653  existingWire.Remove();
4654  return false;
4655  }
4656 
4657  var wire = new Item(userData as ItemPrefab, Vector2.Zero, null);
4658 
4659  int slotIndex = dummyCharacter.Inventory.FindLimbSlot(InvSlotType.LeftHand);
4660 
4661  //if there's some other type of wire in the inventory, remove it
4662  existingWire = dummyCharacter.Inventory.GetItemAt(slotIndex);
4663  if (existingWire != null && existingWire.Prefab != userData as ItemPrefab)
4664  {
4665  existingWire.Drop(null);
4666  existingWire.Remove();
4667  }
4668 
4669  dummyCharacter.Inventory.TryPutItem(wire, slotIndex, false, false, dummyCharacter);
4670 
4671  return true;
4672 
4673  }
4674 
4679  private void OpenItem(Item item)
4680  {
4681  if (dummyCharacter == null || item == null) { return; }
4682 
4683  if ((item.GetComponent<Holdable>() is { Attached: false } || item.GetComponent<Wearable>() != null) && item.GetComponent<ItemContainer>() != null)
4684  {
4685  // We teleport our dummy character to the item so it appears as the entity stays still when in reality the dummy is holding it
4686  oldItemPosition = item.SimPosition;
4687  TeleportDummyCharacter(oldItemPosition);
4688 
4689  // Override this so we can be sure the container opens
4690  var container = item.GetComponent<ItemContainer>();
4691  if (container != null) { container.KeepOpenWhenEquipped = true; }
4692 
4693  // We accept any slots except "Any" since that would take priority
4694  List<InvSlotType> allowedSlots = new List<InvSlotType>();
4695  item.AllowedSlots.ForEach(type =>
4696  {
4697  if (type != InvSlotType.Any) { allowedSlots.Add(type); }
4698  });
4699 
4700  // Try to place the item in the dummy character's inventory
4701  bool success = dummyCharacter.Inventory.TryPutItem(item, dummyCharacter, allowedSlots);
4702  if (success) { OpenedItem = item; }
4703  else { return; }
4704  }
4705  MapEntity.SelectedList.Clear();
4706  MapEntity.FilteredSelectedList.Clear();
4707  MapEntity.SelectEntity(item);
4708  dummyCharacter.SelectedItem = item;
4709  FilterEntities(entityFilterBox.Text);
4710  MapEntity.StopSelection();
4711  }
4712 
4716  private void CloseItem()
4717  {
4718  if (dummyCharacter == null) { return; }
4719  //nothing to close -> return
4720  if (DraggedItemPrefab == null && dummyCharacter?.SelectedItem == null && OpenedItem == null) { return; }
4721  DraggedItemPrefab = null;
4722  dummyCharacter.SelectedItem = null;
4723  OpenedItem?.Drop(dummyCharacter);
4724  OpenedItem?.SetTransform(oldItemPosition, 0f);
4725  OpenedItem = null;
4726  FilterEntities(entityFilterBox.Text);
4727  }
4728 
4733  private void TeleportDummyCharacter(Vector2 pos)
4734  {
4735  if (dummyCharacter != null)
4736  {
4737  foreach (Limb limb in dummyCharacter.AnimController.Limbs)
4738  {
4739  limb.body.SetTransform(pos, 0.0f);
4740  }
4741  dummyCharacter.AnimController.Collider.SetTransform(pos, 0);
4742  }
4743  }
4744 
4745  private bool ChangeSubName(GUITextBox textBox, string text)
4746  {
4747  if (string.IsNullOrWhiteSpace(text))
4748  {
4749  textBox.Flash(GUIStyle.Red);
4750  return false;
4751  }
4752 
4753  if (MainSub != null) { MainSub.Info.Name = text; }
4754  textBox.Deselect();
4755  textBox.Text = text;
4756  textBox.Flash(GUIStyle.Green);
4757 
4758  return true;
4759  }
4760 
4761  private bool ChangeEnemySubTags(GUITextBox textBox, string text)
4762  {
4763  if (string.IsNullOrWhiteSpace(text))
4764  {
4765  textBox.Flash(GUIStyle.Red);
4766  return false;
4767  }
4768 
4769  if (MainSub.Info.EnemySubmarineInfo is { } enemySubInfo)
4770  {
4771  enemySubInfo.MissionTags.Clear();
4772  string[] tags = text.Split(',');
4773  foreach (string tag in tags)
4774  {
4775  enemySubInfo.MissionTags.Add(tag.ToIdentifier());
4776  }
4777  }
4778  textBox.Text = text;
4779  textBox.Flash(GUIStyle.Green);
4780 
4781  return true;
4782  }
4783 
4784  private void ChangeSubDescription(GUITextBox textBox, string text)
4785  {
4786  if (MainSub != null)
4787  {
4788  MainSub.Info.Description = text;
4789  }
4790  else
4791  {
4792  textBox.UserData = text;
4793  }
4794 
4795  submarineDescriptionCharacterCount.Text = text.Length + " / " + submarineDescriptionLimit;
4796  }
4797 
4798  private bool SelectPrefab(GUIComponent component, object obj)
4799  {
4800  allEntityList.Deselect();
4801  categorizedEntityList.Deselect();
4802  if (GUI.MouseOn is GUIButton || GUI.MouseOn?.Parent is GUIButton) { return false; }
4803 
4804  AddPreviouslyUsed(obj as MapEntityPrefab);
4805 
4806  //if selecting a gap/hull/waypoint/spawnpoint, make sure the visibility is toggled on
4807  if (obj is CoreEntityPrefab prefab)
4808  {
4809  var matchingTickBox = showEntitiesTickBoxes.Find(tb => tb.UserData as string == prefab.Identifier);
4810  if (matchingTickBox != null && !matchingTickBox.Selected)
4811  {
4812  previouslyUsedPanel.Visible = false;
4813  showEntitiesPanel.Visible = true;
4814  showEntitiesPanel.RectTransform.AbsoluteOffset = new Point(Math.Max(entityCountPanel.Rect.Right, saveAssemblyFrame.Rect.Right), TopPanel.Rect.Height);
4815  matchingTickBox.Selected = true;
4816  matchingTickBox.Flash(GUIStyle.Green);
4817  }
4818  }
4819 
4820  if (dummyCharacter?.SelectedItem != null)
4821  {
4822  var inv = dummyCharacter?.SelectedItem?.OwnInventory;
4823  if (inv != null)
4824  {
4825  switch (obj)
4826  {
4827  case ItemAssemblyPrefab assemblyPrefab when PlayerInput.IsShiftDown():
4828  {
4829  var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab);
4830  var spawnedItem = false;
4831 
4832  itemInstance.ForEach(newItem =>
4833  {
4834  if (newItem != null)
4835  {
4836  var placedItem = inv.TryPutItem(newItem, dummyCharacter);
4837  spawnedItem |= placedItem;
4838 
4839  if (!placedItem)
4840  {
4841  // Remove everything inside of the item so we don't get the popup asking if we want to keep the contained items
4842  newItem.OwnInventory?.DeleteAllItems();
4843  newItem.Remove();
4844  }
4845  }
4846  });
4847 
4848  List<MapEntity> placedEntities = itemInstance.Where(it => !it.Removed).Cast<MapEntity>().ToList();
4849  if (placedEntities.Any())
4850  {
4851  StoreCommand(new AddOrDeleteCommand(placedEntities, false));
4852  }
4853  SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail);
4854  break;
4855  }
4856  case ItemPrefab itemPrefab when PlayerInput.IsShiftDown():
4857  {
4858  var item = new Item(itemPrefab, Vector2.Zero, MainSub);
4859  if (!inv.TryPutItem(item, dummyCharacter))
4860  {
4861  // We failed, remove the item so it doesn't stay at x:0,y:0
4862  SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
4863  item.Remove();
4864  }
4865  else
4866  {
4867  SoundPlayer.PlayUISound(GUISoundType.PickItem);
4868  }
4869 
4870  if (!item.Removed)
4871  {
4872  StoreCommand(new AddOrDeleteCommand(new List<MapEntity> { item }, false));
4873  }
4874  break;
4875  }
4876  case ItemAssemblyPrefab _:
4877  case ItemPrefab _:
4878  {
4879  // Place the item into our hands
4880  DraggedItemPrefab = (MapEntityPrefab)obj;
4881  SoundPlayer.PlayUISound(GUISoundType.PickItem);
4882  break;
4883  }
4884  }
4885  }
4886  }
4887  else
4888  {
4889  SoundPlayer.PlayUISound(GUISoundType.PickItem);
4890  MapEntityPrefab.SelectPrefab(obj);
4891  }
4892 
4893  return false;
4894  }
4895 
4896  private bool GenerateWaypoints()
4897  {
4898  if (MainSub == null) { return false; }
4899  return WayPoint.GenerateSubWaypoints(MainSub);
4900  }
4901 
4902  private void AddPreviouslyUsed(MapEntityPrefab mapEntityPrefab)
4903  {
4904  if (previouslyUsedList == null || mapEntityPrefab == null) { return; }
4905 
4906  previouslyUsedList.Deselect();
4907 
4908  if (previouslyUsedList.CountChildren == PreviouslyUsedCount)
4909  {
4910  previouslyUsedList.RemoveChild(previouslyUsedList.Content.Children.Last());
4911  }
4912 
4913  var existing = previouslyUsedList.Content.FindChild(mapEntityPrefab);
4914  if (existing != null) { previouslyUsedList.Content.RemoveChild(existing); }
4915 
4916  var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), previouslyUsedList.Content.RectTransform) { MinSize = new Point(0, 15) },
4917  ToolBox.LimitString(mapEntityPrefab.Name.Value, GUIStyle.SmallFont, previouslyUsedList.Content.Rect.Width), font: GUIStyle.SmallFont)
4918  {
4919  UserData = mapEntityPrefab
4920  };
4921  textBlock.RectTransform.SetAsFirstChild();
4922  }
4923 
4924  public void AutoHull()
4925  {
4926  for (int i = 0; i < MapEntity.MapEntityList.Count; i++)
4927  {
4929  if (h is Hull || h is Gap)
4930  {
4931  h.Remove();
4932  i--;
4933  }
4934  }
4935 
4936  List<Vector2> wallPoints = new List<Vector2>();
4937  Vector2 max;
4938 
4939  List<MapEntity> mapEntityList = new List<MapEntity>();
4940 
4941  foreach (MapEntity e in MapEntity.MapEntityList)
4942  {
4943  if (e is Item it)
4944  {
4945  Door door = it.GetComponent<Door>();
4946  if (door != null)
4947  {
4948  int halfW = it.WorldRect.Width / 2;
4949  wallPoints.Add(new Vector2(it.WorldRect.X + halfW, -it.WorldRect.Y + it.WorldRect.Height));
4950  mapEntityList.Add(it);
4951  }
4952  continue;
4953  }
4954 
4955  if (!(e is Structure)) continue;
4956  Structure s = e as Structure;
4957  if (!s.HasBody) continue;
4958  mapEntityList.Add(e);
4959 
4960  if (e.Rect.Width > e.Rect.Height)
4961  {
4962  int halfH = e.WorldRect.Height / 2;
4963  wallPoints.Add(new Vector2(e.WorldRect.X, -e.WorldRect.Y + halfH));
4964  wallPoints.Add(new Vector2(e.WorldRect.X + e.WorldRect.Width, -e.WorldRect.Y + halfH));
4965  }
4966  else
4967  {
4968  int halfW = e.WorldRect.Width / 2;
4969  wallPoints.Add(new Vector2(e.WorldRect.X + halfW, -e.WorldRect.Y));
4970  wallPoints.Add(new Vector2(e.WorldRect.X + halfW, -e.WorldRect.Y + e.WorldRect.Height));
4971  }
4972  }
4973 
4974  if (wallPoints.Count < 4)
4975  {
4976  DebugConsole.ThrowError("Generating hulls for the submarine failed. Not enough wall structures to generate hulls.");
4977  return;
4978  }
4979 
4980  var min = wallPoints[0];
4981  max = wallPoints[0];
4982  for (int i = 0; i < wallPoints.Count; i++)
4983  {
4984  min.X = Math.Min(min.X, wallPoints[i].X);
4985  min.Y = Math.Min(min.Y, wallPoints[i].Y);
4986  max.X = Math.Max(max.X, wallPoints[i].X);
4987  max.Y = Math.Max(max.Y, wallPoints[i].Y);
4988  }
4989 
4990  List<Rectangle> hullRects = new List<Rectangle>
4991  {
4992  new Rectangle((int)min.X, (int)min.Y, (int)(max.X - min.X), (int)(max.Y - min.Y))
4993  };
4994  foreach (Vector2 point in wallPoints)
4995  {
4996  MathUtils.SplitRectanglesHorizontal(hullRects, point);
4997  MathUtils.SplitRectanglesVertical(hullRects, point);
4998  }
4999 
5000  hullRects.Sort((a, b) =>
5001  {
5002  if (a.Y < b.Y) return -1;
5003  if (a.Y > b.Y) return 1;
5004  if (a.X < b.X) return -1;
5005  if (a.X > b.X) return 1;
5006  return 0;
5007  });
5008 
5009  for (int i = 0; i < hullRects.Count - 1; i++)
5010  {
5011  Rectangle rect = hullRects[i];
5012  if (hullRects[i + 1].Y > rect.Y) continue;
5013 
5014  Vector2 hullRPoint = new Vector2(rect.X + rect.Width - 8, rect.Y + rect.Height / 2);
5015  Vector2 hullLPoint = new Vector2(rect.X, rect.Y + rect.Height / 2);
5016 
5017  MapEntity container = null;
5018  foreach (MapEntity e in mapEntityList)
5019  {
5020  Rectangle entRect = e.WorldRect;
5021  entRect.Y = -entRect.Y;
5022  if (entRect.Contains(hullRPoint))
5023  {
5024  if (!entRect.Contains(hullLPoint)) container = e;
5025  break;
5026  }
5027  }
5028  if (container == null)
5029  {
5030  rect.Width += hullRects[i + 1].Width;
5031  hullRects[i] = rect;
5032  hullRects.RemoveAt(i + 1);
5033  i--;
5034  }
5035  }
5036 
5037  foreach (MapEntity e in mapEntityList)
5038  {
5039  Rectangle entRect = e.WorldRect;
5040  if (entRect.Width < entRect.Height) continue;
5041  entRect.Y = -entRect.Y - 16;
5042  for (int i = 0; i < hullRects.Count; i++)
5043  {
5044  Rectangle hullRect = hullRects[i];
5045  if (entRect.Intersects(hullRect))
5046  {
5047  if (hullRect.Y < entRect.Y)
5048  {
5049  hullRect.Height = Math.Max((entRect.Y + 16 + entRect.Height / 2) - hullRect.Y, hullRect.Height);
5050  hullRects[i] = hullRect;
5051  }
5052  else if (hullRect.Y + hullRect.Height <= entRect.Y + 16 + entRect.Height)
5053  {
5054  hullRects.RemoveAt(i);
5055  i--;
5056  }
5057  }
5058  }
5059  }
5060 
5061  foreach (MapEntity e in mapEntityList)
5062  {
5063  Rectangle entRect = e.WorldRect;
5064  if (entRect.Width < entRect.Height) continue;
5065  entRect.Y = -entRect.Y;
5066  for (int i = 0; i < hullRects.Count; i++)
5067  {
5068  Rectangle hullRect = hullRects[i];
5069  if (entRect.Intersects(hullRect))
5070  {
5071  if (hullRect.Y >= entRect.Y - 8 && hullRect.Y + hullRect.Height <= entRect.Y + entRect.Height + 8)
5072  {
5073  hullRects.RemoveAt(i);
5074  i--;
5075  }
5076  }
5077  }
5078  }
5079 
5080  for (int i = 0; i < hullRects.Count;)
5081  {
5082  Rectangle hullRect = hullRects[i];
5083  Vector2 point = new Vector2(hullRect.X+2, hullRect.Y+hullRect.Height/2);
5084  MapEntity container = null;
5085  foreach (MapEntity e in mapEntityList)
5086  {
5087  Rectangle entRect = e.WorldRect;
5088  entRect.Y = -entRect.Y;
5089  if (entRect.Contains(point))
5090  {
5091  container = e;
5092  break;
5093  }
5094  }
5095  if (container == null)
5096  {
5097  hullRects.RemoveAt(i);
5098  continue;
5099  }
5100 
5101  while (hullRects[i].Y <= hullRect.Y)
5102  {
5103  i++;
5104  if (i >= hullRects.Count) break;
5105  }
5106  }
5107 
5108  for (int i = hullRects.Count-1; i >= 0;)
5109  {
5110  Rectangle hullRect = hullRects[i];
5111  Vector2 point = new Vector2(hullRect.X+hullRect.Width-2, hullRect.Y+hullRect.Height/2);
5112  MapEntity container = null;
5113  foreach (MapEntity e in mapEntityList)
5114  {
5115  Rectangle entRect = e.WorldRect;
5116  entRect.Y = -entRect.Y;
5117  if (entRect.Contains(point))
5118  {
5119  container = e;
5120  break;
5121  }
5122  }
5123  if (container == null)
5124  {
5125  hullRects.RemoveAt(i); i--;
5126  continue;
5127  }
5128 
5129  while (hullRects[i].Y >= hullRect.Y)
5130  {
5131  i--;
5132  if (i < 0) break;
5133  }
5134  }
5135 
5136  hullRects.Sort((a, b) =>
5137  {
5138  if (a.X < b.X) return -1;
5139  if (a.X > b.X) return 1;
5140  if (a.Y < b.Y) return -1;
5141  if (a.Y > b.Y) return 1;
5142  return 0;
5143  });
5144 
5145  for (int i = 0; i < hullRects.Count - 1; i++)
5146  {
5147  Rectangle rect = hullRects[i];
5148  if (hullRects[i + 1].Width != rect.Width) continue;
5149  if (hullRects[i + 1].X > rect.X) continue;
5150 
5151  Vector2 hullBPoint = new Vector2(rect.X + rect.Width / 2, rect.Y + rect.Height - 8);
5152  Vector2 hullUPoint = new Vector2(rect.X + rect.Width / 2, rect.Y);
5153 
5154  MapEntity container = null;
5155  foreach (MapEntity e in mapEntityList)
5156  {
5157  Rectangle entRect = e.WorldRect;
5158  entRect.Y = -entRect.Y;
5159  if (entRect.Contains(hullBPoint))
5160  {
5161  if (!entRect.Contains(hullUPoint)) container = e;
5162  break;
5163  }
5164  }
5165  if (container == null)
5166  {
5167  rect.Height += hullRects[i + 1].Height;
5168  hullRects[i] = rect;
5169  hullRects.RemoveAt(i + 1);
5170  i--;
5171  }
5172  }
5173 
5174  for (int i = 0; i < hullRects.Count;i++)
5175  {
5176  Rectangle rect = hullRects[i];
5177  rect.Y -= 16;
5178  rect.Height += 32;
5179  hullRects[i] = rect;
5180  }
5181 
5182  hullRects.Sort((a, b) =>
5183  {
5184  if (a.Y < b.Y) return -1;
5185  if (a.Y > b.Y) return 1;
5186  if (a.X < b.X) return -1;
5187  if (a.X > b.X) return 1;
5188  return 0;
5189  });
5190 
5191  for (int i = 0; i < hullRects.Count; i++)
5192  {
5193  for (int j = i+1; j < hullRects.Count; j++)
5194  {
5195  if (hullRects[j].Y <= hullRects[i].Y) continue;
5196  if (hullRects[j].Intersects(hullRects[i]))
5197  {
5198  Rectangle rect = hullRects[i];
5199  rect.Height = hullRects[j].Y - rect.Y;
5200  hullRects[i] = rect;
5201  break;
5202  }
5203  }
5204  }
5205 
5206  foreach (Rectangle rect in hullRects)
5207  {
5208  Rectangle hullRect = rect;
5209  hullRect.Y = -hullRect.Y;
5210  Hull newHull = new Hull(hullRect,
5211  MainSub);
5212  }
5213 
5214  foreach (MapEntity e in mapEntityList)
5215  {
5216  if (!(e is Structure)) continue;
5217  if (!(e as Structure).IsPlatform) continue;
5218 
5219  Rectangle gapRect = e.WorldRect;
5220  gapRect.Y -= 8;
5221  gapRect.Height = 16;
5222  new Gap(gapRect);
5223  }
5224  }
5225 
5226  public override void AddToGUIUpdateList()
5227  {
5228  if (GUI.DisableHUD) { return; }
5229 
5230  MapEntity.FilteredSelectedList.FirstOrDefault()?.AddToGUIUpdateList();
5231  EntityMenu.AddToGUIUpdateList();
5232  showEntitiesPanel.AddToGUIUpdateList();
5233  previouslyUsedPanel.AddToGUIUpdateList();
5234  undoBufferPanel.AddToGUIUpdateList();
5235  entityCountPanel.AddToGUIUpdateList();
5236  layerPanel.AddToGUIUpdateList();
5237  TopPanel.AddToGUIUpdateList();
5238 
5239  if (WiringMode)
5240  {
5241  wiringToolPanel.AddToGUIUpdateList();
5242  }
5243 
5244  if (MapEntity.HighlightedListBox != null)
5245  {
5247  }
5248 
5249  if (dummyCharacter != null)
5250  {
5251  CharacterHUD.AddToGUIUpdateList(dummyCharacter);
5252  if (dummyCharacter.SelectedItem != null)
5253  {
5254  dummyCharacter.SelectedItem.AddToGUIUpdateList();
5255  }
5256  else if (WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item && item.GetComponent<Wire>() != null)
5257  {
5258  MapEntity.SelectedList.FirstOrDefault()?.AddToGUIUpdateList();
5259  }
5260  }
5261  if (loadFrame != null)
5262  {
5263  loadFrame.AddToGUIUpdateList();
5264  }
5265  else
5266  {
5267  saveFrame?.AddToGUIUpdateList();
5268  }
5269  }
5270 
5274  public bool IsMouseOnEditorGUI()
5275  {
5276  if (GUI.MouseOn == null) { return false; }
5277 
5278  return (EntityMenu?.MouseRect.Contains(PlayerInput.MousePosition) ?? false)
5279  || (entityCountPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false)
5280  || (MapEntity.EditingHUD?.MouseRect.Contains(PlayerInput.MousePosition) ?? false)
5281  || (TopPanel?.MouseRect.Contains(PlayerInput.MousePosition) ?? false);
5282  }
5283 
5284  private static void Redo(int amount)
5285  {
5286  for (int i = 0; i < amount; i++)
5287  {
5288  if (commandIndex < Commands.Count)
5289  {
5290  Command command = Commands[commandIndex++];
5291  command.Execute();
5292  }
5293  }
5294  GameMain.SubEditorScreen.UpdateUndoHistoryPanel();
5295  }
5296 
5297  private static void Undo(int amount)
5298  {
5299  for (int i = 0; i < amount; i++)
5300  {
5301  if (commandIndex > 0)
5302  {
5303  Command command = Commands[--commandIndex];
5304  command.UnExecute();
5305  }
5306  }
5307  GameMain.SubEditorScreen.UpdateUndoHistoryPanel();
5308  }
5309 
5310  private static void ClearUndoBuffer()
5311  {
5312  SerializableEntityEditor.PropertyChangesActive = false;
5313  SerializableEntityEditor.CommandBuffer = null;
5314  Commands.ForEach(cmd => cmd.Cleanup());
5315  Commands.Clear();
5316  commandIndex = 0;
5317  GameMain.SubEditorScreen.UpdateUndoHistoryPanel();
5318  }
5319 
5320  public static void StoreCommand(Command command)
5321  {
5322  if (commandIndex != Commands.Count)
5323  {
5324  Commands.RemoveRange(commandIndex, Commands.Count - commandIndex);
5325  }
5326  Commands.Add(command);
5327  commandIndex++;
5328 
5329  // Start removing old commands
5330  if (Commands.Count > Math.Clamp(GameSettings.CurrentConfig.SubEditorUndoBuffer, 1, 10240))
5331  {
5332  Commands.First()?.Cleanup();
5333  Commands.RemoveRange(0, 1);
5334  commandIndex = Commands.Count;
5335  }
5336 
5338 
5339  if (command is AddOrDeleteCommand addOrDelete)
5340  {
5341  GameMain.SubEditorScreen.EntityAddedOrDeleted(addOrDelete.Receivers);
5342  }
5343  }
5344 
5345  private string prevSelectedLayer;
5346  private void EntityAddedOrDeleted(IEnumerable<MapEntity> entities)
5347  {
5348  if (layerList?.SelectedData is string selectedLayer)
5349  {
5350  //add the created entities to the selected layer
5351  foreach (var entity in entities)
5352  {
5353  if (!entity.Removed)
5354  {
5355  entity.Layer = selectedLayer;
5356  }
5357  }
5358  var layerElement = layerList.Content.FindChild(selectedLayer);
5359  layerElement?.Flash(GUIStyle.Green);
5360  }
5361  }
5362 
5363  private void UpdateLayerPanel()
5364  {
5365  if (layerPanel is null || layerList is null) { return; }
5366 
5367  layerList.Content.ClearChildren();
5368 
5369  layerList.Deselect();
5370  layerSpecificButtons.ForEach(btn => btn.Enabled = false);
5371  GUILayoutGroup buttonHeaders = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), layerList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.BottomLeft);
5372 
5373  new GUIButton(new RectTransform(new Vector2(0.25f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headervisible"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes };
5374  new GUIButton(new RectTransform(new Vector2(0.15f, 1f), buttonHeaders.RectTransform), TextManager.Get("editor.layer.headerlink"), style: "GUIButtonSmallFreeScale")
5375  {
5377  ToolTip = TextManager.Get("editor.layer.headerlink.tooltip")
5378  };
5379  new GUIButton(new RectTransform(new Vector2(0.6f, 1f), buttonHeaders.RectTransform), TextManager.Get("name"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes };
5380 
5381  foreach ((string layer, (bool isVisible, bool isGrouped)) in Layers)
5382  {
5383  GUIFrame parent = new GUIFrame(new RectTransform(new Vector2(1f, 0.1f), layerList.Content.RectTransform), style: "ListBoxElement")
5384  {
5385  UserData = layer
5386  };
5387 
5388  GUILayoutGroup layerGroup = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft);
5389 
5390  GUILayoutGroup layerVisibilityLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center);
5391  GUITickBox layerVisibleButton = new GUITickBox(new RectTransform(Vector2.One, layerVisibilityLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
5392  {
5393  Selected = isVisible,
5394  OnSelected = box =>
5395  {
5396  if (!Layers.TryGetValue(layer, out LayerData data))
5397  {
5398  UpdateLayerPanel();
5399  return false;
5400  }
5401  if (!box.Selected && layerList.SelectedData as string == layer)
5402  {
5403  //hiding a layer automatically deselects it (can't edit a hidden layer)
5404  if (!box.Selected)
5405  {
5406  layerList.Deselect();
5407  }
5408  }
5409  Layers[layer] = data with { IsVisible = box.Selected };
5410  return true;
5411  }
5412  };
5413 
5414  GUILayoutGroup layerChainLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.15f, 1f), layerGroup.RectTransform), childAnchor: Anchor.Center);
5415  GUITickBox layerChainButton = new GUITickBox(new RectTransform(Vector2.One, layerChainLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), string.Empty)
5416  {
5417  Selected = isGrouped,
5418  OnSelected = box =>
5419  {
5420  if (!Layers.TryGetValue(layer, out LayerData data))
5421  {
5422  UpdateLayerPanel();
5423  return false;
5424  }
5425 
5426  Layers[layer] = data with { IsGrouped = box.Selected };
5427  return true;
5428  }
5429  };
5430 
5431  layerGroup.Recalculate();
5432 
5433  var textBlock = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), layerGroup.RectTransform), layer, textAlignment: Alignment.CenterLeft);
5434  if (textBlock.TextSize.X > textBlock.Rect.Width)
5435  {
5436  textBlock.ToolTip = textBlock.Text;
5437  textBlock.Text = ToolBox.LimitString(textBlock.Text, textBlock.Font, textBlock.Rect.Width);
5438  }
5439 
5440  layerGroup.Recalculate();
5441  layerChainLayout.Recalculate();
5442  layerVisibilityLayout.Recalculate();
5443  }
5444 
5445  layerList.RecalculateChildren();
5446  buttonHeaders.Recalculate();
5447  foreach (var child in buttonHeaders.Children)
5448  {
5449  var btn = child as GUIButton;
5450  string originalBtnText = btn.Text.Value;
5451  btn.Text = ToolBox.LimitString(btn.Text, btn.Font, btn.Rect.Width);
5452  if (originalBtnText != btn.Text && btn.ToolTip.IsNullOrEmpty())
5453  {
5454  btn.ToolTip = originalBtnText;
5455  }
5456  }
5457  }
5458 
5460  {
5461  if (undoBufferPanel is null) { return; }
5462 
5463  undoBufferDisclaimer.Visible = mode == Mode.Wiring;
5464 
5465  undoBufferList.Content.Children.ForEachMod(component =>
5466  {
5467  undoBufferList.Content.RemoveChild(component);
5468  });
5469 
5470  for (int i = 0; i < Commands.Count; i++)
5471  {
5472  Command command = Commands[i];
5473  LocalizedString description = command.GetDescription();
5474  CreateTextBlock(description, description, i + 1, command).RectTransform.SetAsFirstChild();
5475  }
5476 
5477  CreateTextBlock(TextManager.Get("undo.beginning"), TextManager.Get("undo.beginningtooltip"), 0, null);
5478 
5479  GUITextBlock CreateTextBlock(LocalizedString name, LocalizedString description, int index, Command command)
5480  {
5481  return new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), undoBufferList.Content.RectTransform) { MinSize = new Point(0, 15) },
5482  ToolBox.LimitString(name.Value, GUIStyle.SmallFont, undoBufferList.Content.Rect.Width), font: GUIStyle.SmallFont, textColor: index == commandIndex ? GUIStyle.Green : (Color?) null)
5483  {
5484  UserData = command,
5485  ToolTip = description
5486  };
5487  }
5488  }
5489 
5490  private static void CommitBulkItemBuffer()
5491  {
5492  if (BulkItemBuffer.Any())
5493  {
5494  AddOrDeleteCommand master = BulkItemBuffer[0];
5495  for (int i = 1; i < BulkItemBuffer.Count; i++)
5496  {
5497  AddOrDeleteCommand command = BulkItemBuffer[i];
5498  command.MergeInto(master);
5499  }
5500 
5501  StoreCommand(master);
5502  BulkItemBuffer.Clear();
5503  }
5504 
5505  bulkItemBufferinUse = null;
5506  }
5507 
5512  public override void Update(double deltaTime)
5513  {
5514  SkipInventorySlotUpdate = false;
5515  ImageManager.Update((float)deltaTime);
5516 
5517  Hull.UpdateCheats((float)deltaTime, cam);
5518 
5519  if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y)
5520  {
5521  saveFrame = null;
5522  loadFrame = null;
5523  saveAssemblyFrame = null;
5524  snapToGridFrame = null;
5525  CreateUI();
5526  UpdateEntityList();
5527  }
5528 
5529  if (OpenedItem != null && OpenedItem.Removed)
5530  {
5531  OpenedItem = null;
5532  }
5533 
5534  if (WiringMode && dummyCharacter != null)
5535  {
5536  Wire equippedWire =
5537  Character.Controlled?.HeldItems.FirstOrDefault(it => it.GetComponent<Wire>() != null)?.GetComponent<Wire>() ??
5539 
5540  if (equippedWire == null)
5541  {
5542  // Highlight wires when hovering over the entity selection box
5543  if (MapEntity.HighlightedListBox != null)
5544  {
5545  var lBox = MapEntity.HighlightedListBox;
5546  foreach (var child in lBox.Content.Children)
5547  {
5548  if (child.UserData is Item item)
5549  {
5550  item.ExternalHighlight = GUI.IsMouseOn(child);
5551  }
5552  }
5553  }
5554 
5555  var highlightedEntities = new List<MapEntity>();
5556 
5557  // ReSharper disable once LoopCanBeConvertedToQuery
5558  foreach (Item item in MapEntity.MapEntityList.Where(entity => entity is Item).Cast<Item>())
5559  {
5560  var wire = item.GetComponent<Wire>();
5561  if (wire == null || !wire.IsMouseOn()) { continue; }
5562  highlightedEntities.Add(item);
5563  }
5564 
5565  MapEntity.UpdateHighlighting(highlightedEntities, true);
5566  }
5567  }
5568 
5569  hullVolumeFrame.Visible = MapEntity.SelectedList.Any(s => s is Hull);
5570  hullVolumeFrame.RectTransform.AbsoluteOffset = new Point(Math.Max(showEntitiesPanel.Rect.Right, previouslyUsedPanel.Rect.Right), 0);
5571  bool isCircuitBoxOpened = dummyCharacter?.SelectedItem?.GetComponent<CircuitBox>() is not null;
5572  saveAssemblyFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened;
5573  snapToGridFrame.Visible = MapEntity.SelectedList.Count > 0 && !WiringMode && !isCircuitBoxOpened;
5574 
5575  var offset = cam.WorldView.Top - cam.ScreenToWorld(new Vector2(0, GameMain.GraphicsHeight - EntityMenu.Rect.Top)).Y;
5576 
5577  // Move the camera towards to the focus point
5578  if (camTargetFocus != Vector2.Zero)
5579  {
5580  if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Up].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Down].IsDown() ||
5581  GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Left].IsDown() || GameSettings.CurrentConfig.KeyMap.Bindings[InputType.Right].IsDown())
5582  {
5583  camTargetFocus = Vector2.Zero;
5584  }
5585  else
5586  {
5587  var targetWithOffset = new Vector2(camTargetFocus.X, camTargetFocus.Y - offset / 2);
5588  if (Math.Abs(cam.Position.X - targetWithOffset.X) < 1.0f &&
5589  Math.Abs(cam.Position.Y - targetWithOffset.Y) < 1.0f)
5590  {
5591  camTargetFocus = Vector2.Zero;
5592  }
5593  else
5594  {
5595  cam.Position += (targetWithOffset - cam.Position) / cam.MoveSmoothness;
5596  }
5597  }
5598  }
5599 
5600  if (undoBufferPanel.Visible)
5601  {
5602  undoBufferList.Deselect();
5603  }
5604 
5605  if (GUI.KeyboardDispatcher.Subscriber == null
5606  || MapEntity.EditingHUD != null
5607  && GUI.KeyboardDispatcher.Subscriber is GUIComponent sub
5608  && MapEntity.EditingHUD.Children.Contains(sub))
5609  {
5610  if (PlayerInput.IsCtrlDown() && !WiringMode)
5611  {
5612  if (PlayerInput.KeyHit(Keys.Z))
5613  {
5614  // Ctrl+Shift+Z redos while Ctrl+Z undos
5615  if (PlayerInput.IsShiftDown()) { Redo(1); } else { Undo(1); }
5616  }
5617 
5618  // ctrl+Y redo
5619  if (PlayerInput.KeyHit(Keys.Y))
5620  {
5621  Redo(1);
5622  }
5623  }
5624  }
5625 
5626  if (GUI.KeyboardDispatcher.Subscriber == null)
5627  {
5628  if (WiringMode && dummyCharacter != null)
5629  {
5630  if (wiringToolPanel.GetChild<GUIListBox>() is { } listBox)
5631  {
5632  if (!dummyCharacter.HeldItems.Any(it => it.HasTag(Tags.WireItem)))
5633  {
5634  listBox.Deselect();
5635  }
5636 
5637  List<Keys> numberKeys = PlayerInput.NumberKeys;
5638  if (numberKeys.Find(PlayerInput.KeyHit) is { } key && key != Keys.None)
5639  {
5640  // treat 0 as the last key instead of first
5641  int index = key == Keys.D0 ? numberKeys.Count : numberKeys.IndexOf(key) - 1;
5642  if (index > -1 && index < listBox.Content.CountChildren)
5643  {
5644  listBox.Select(index);
5645  SkipInventorySlotUpdate = true;
5646  }
5647  }
5648  }
5649  }
5650 
5651  if (mode == Mode.Default)
5652  {
5653  if (PlayerInput.KeyHit(InputType.Use))
5654  {
5655  if (dummyCharacter != null)
5656  {
5657  if (dummyCharacter.SelectedItem == null)
5658  {
5659  foreach (var entity in MapEntity.HighlightedEntities)
5660  {
5661  if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null))
5662  {
5663  var container = item.GetComponents<ItemContainer>().ToList();
5664  if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false))
5665  {
5666  OpenItem(item);
5667  break;
5668  }
5669  }
5670  }
5671  }
5672  else
5673  {
5674  CloseItem();
5675  }
5676  }
5677  }
5678 
5679  // Focus to selection
5680  if (PlayerInput.KeyHit(Keys.F))
5681  {
5682  // content warning: contains coordinate system workarounds
5683  var selected = MapEntity.SelectedList;
5684  if (selected.Count > 0)
5685  {
5686  var dRect = selected.First().Rect;
5687  var rect = new Rectangle(dRect.Left, dRect.Top, dRect.Width, dRect.Height * -1);
5688  if (selected.Count > 1)
5689  {
5690  // Create one big rect out of our selection
5691  selected.Skip(1).ForEach(me =>
5692  {
5693  var wRect = me.Rect;
5694  rect = Rectangle.Union(rect, new Rectangle(wRect.Left, wRect.Top, wRect.Width, wRect.Height * -1));
5695  });
5696  }
5697  camTargetFocus = rect.Center.ToVector2();
5698  }
5699  }
5700  if (PlayerInput.KeyHit(Keys.Tab))
5701  {
5702  entityFilterBox.Select();
5703  }
5704  }
5705 
5706  if (toggleEntityListBind != GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory])
5707  {
5708  toggleEntityMenuButton.ToolTip = RichString.Rich($"{TextManager.Get("EntityMenuToggleTooltip")}\n‖color:125,125,125‖{GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].Name}‖color:end‖");
5709  toggleEntityListBind = GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory];
5710  }
5711  if (GameSettings.CurrentConfig.KeyMap.Bindings[InputType.ToggleInventory].IsHit() && mode == Mode.Default)
5712  {
5713  toggleEntityMenuButton.OnClicked?.Invoke(toggleEntityMenuButton, toggleEntityMenuButton.UserData);
5714  }
5715 
5716  if (PlayerInput.IsCtrlDown() && MapEntity.StartMovingPos == Vector2.Zero)
5717  {
5718  cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null);
5719  // Save menu
5720  if (PlayerInput.KeyHit(Keys.S))
5721  {
5722  if (PlayerInput.IsShiftDown())
5723  {
5724  // Quick-save, but only when we've set a custom name for our sub
5725  CreateSaveScreen(subNameLabel != null && subNameLabel.Text != TextManager.Get("unspecifiedsubfilename"));
5726  }
5727  else
5728  {
5729  // Save menu
5730  CreateSaveScreen();
5731  }
5732  }
5733 
5734  // Select or deselect everything
5735  if (PlayerInput.KeyHit(Keys.A) && mode == Mode.Default)
5736  {
5737  if (MapEntity.SelectedList.Any())
5738  {
5740  }
5741  else
5742  {
5743  var selectables = MapEntity.MapEntityList.Where(entity => entity.SelectableInEditor).ToList();
5744  foreach (var item in Item.ItemList)
5745  {
5746  //attached wires are not normally selectable (by clicking),
5747  //but let's select them manually when selecting all
5748  var wire = item.GetComponent<Wire>();
5749  if (wire != null && wire.Connections.None(c => c == null) && !selectables.Contains(item))
5750  {
5751  selectables.Add(item);
5752  }
5753  }
5754  lock (selectables)
5755  {
5756  selectables.ForEach(MapEntity.AddSelection);
5757  }
5758  }
5759  }
5760 
5761  // 1-2 keys on the keyboard for switching modes
5762  if (PlayerInput.KeyHit(Keys.D1)) { SetMode(Mode.Default); }
5763  if (PlayerInput.KeyHit(Keys.D2)) { SetMode(Mode.Wiring); }
5764  }
5765  else
5766  {
5767  cam.MoveCamera((float) deltaTime, allowMove: !CircuitBox.IsCircuitBoxSelected(dummyCharacter), allowZoom: GUI.MouseOn == null);
5768  }
5769  }
5770  else
5771  {
5772  cam.MoveCamera((float) deltaTime, allowMove: false, allowZoom: GUI.MouseOn == null);
5773  }
5774 
5775  if (PlayerInput.MidButtonHeld() && !CircuitBox.IsCircuitBoxSelected(dummyCharacter))
5776  {
5777  Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / cam.Zoom;
5778  moveSpeed.X = -moveSpeed.X;
5779  cam.Position += moveSpeed;
5780  // break out of trying to focus
5781  camTargetFocus = Vector2.Zero;
5782  }
5783 
5784  if (PlayerInput.KeyHit(Keys.Escape) && dummyCharacter != null)
5785  {
5786  CloseItem();
5787  }
5788 
5789  if (lightingEnabled)
5790  {
5791  //turn off lights that are inside containers
5792  foreach (Item item in Item.ItemList)
5793  {
5794  foreach (LightComponent lightComponent in item.GetComponents<LightComponent>())
5795  {
5796  lightComponent.Light.Color =
5797  (item.body == null || item.body.Enabled || item.ParentInventory is ItemInventory { Container.HideItems: true }) &&
5798  /*the light is only visible when worn -> can't be visible in the editor*/
5799  lightComponent.Parent is not Wearable ?
5800  lightComponent.LightColor :
5801  Color.Transparent;
5802  lightComponent.Light.LightSpriteEffect = lightComponent.Item.SpriteEffects;
5803  }
5804  }
5805  GameMain.LightManager?.Update((float)deltaTime);
5806  }
5807 
5808  if (dummyCharacter != null && Entity.FindEntityByID(dummyCharacter.ID) == dummyCharacter)
5809  {
5810  if (WiringMode)
5811  {
5813 
5814  if (dummyCharacter.SelectedItem == null)
5815  {
5816  List<Wire> wires = new List<Wire>();
5817  foreach (Item item in Item.ItemList)
5818  {
5819  var wire = item.GetComponent<Wire>();
5820  if (wire != null) wires.Add(wire);
5821  }
5822  Wire.UpdateEditing(wires);
5823  }
5824  }
5825 
5826  if (!WiringMode)
5827  {
5828  // Move all of our slots on top center of the entity list
5829  // We use the slots to open item inventories and we want the position of them to be consisent
5830  dummyCharacter.Inventory.visualSlots.ForEach(slot =>
5831  {
5832  slot.Rect.Y = EntityMenu.Rect.Top;
5833  slot.Rect.X = EntityMenu.Rect.X + (EntityMenu.Rect.Width / 2) - (slot.Rect.Width /2);
5834  });
5835  }
5836 
5837  if (dummyCharacter.SelectedItem == null ||
5838  dummyCharacter.SelectedItem.GetComponent<Pickable>() != null)
5839  {
5840  if (WiringMode && PlayerInput.IsShiftDown())
5841  {
5842  Wire equippedWire = Character.Controlled?.HeldItems.FirstOrDefault(i => i.GetComponent<Wire>() != null)?.GetComponent<Wire>();
5843  if (equippedWire != null && equippedWire.GetNodes().Count > 0)
5844  {
5845  Vector2 lastNode = equippedWire.GetNodes().Last();
5846  if (equippedWire.Item.Submarine != null)
5847  {
5848  lastNode += equippedWire.Item.Submarine.HiddenSubPosition + equippedWire.Item.Submarine.Position;
5849  }
5850 
5851  var (cursorX, cursorY) = dummyCharacter.CursorPosition;
5852 
5853  bool isHorizontal = Math.Abs(cursorX - lastNode.X) < Math.Abs(cursorY - lastNode.Y);
5854 
5855  float roundedY = MathUtils.Round(cursorY, Submarine.GridSize.Y / 2.0f);
5856  float roundedX = MathUtils.Round(cursorX, Submarine.GridSize.X / 2.0f);
5857 
5858  dummyCharacter.CursorPosition = isHorizontal
5859  ? new Vector2(lastNode.X, roundedY)
5860  : new Vector2(roundedX, lastNode.Y);
5861  }
5862  }
5863 
5864  // Keep teleporting the dummy character to the opened item to make it look like the container didn't go anywhere
5865  if (OpenedItem != null)
5866  {
5867  TeleportDummyCharacter(oldItemPosition);
5868  }
5869 
5870  if (WiringMode && dummyCharacter?.SelectedItem == null)
5871  {
5872  TeleportDummyCharacter(FarseerPhysics.ConvertUnits.ToSimUnits(dummyCharacter.CursorPosition));
5873  }
5874  }
5875 
5876  if (WiringMode)
5877  {
5878  dummyCharacter.ControlLocalPlayer((float)deltaTime, cam, false);
5879  dummyCharacter.Control((float)deltaTime, cam);
5880  }
5881 
5882  cam.TargetPos = Vector2.Zero;
5883  dummyCharacter.Submarine = MainSub;
5884  }
5885 
5886  if (dummyCharacter?.SelectedItem != null)
5887  {
5888  // Deposit item from our "infinite stack" into inventory slots
5889  TryDragItemsToItem(dummyCharacter.SelectedItem);
5890  foreach (Item linkedItem in dummyCharacter.SelectedItem.linkedTo.OfType<Item>())
5891  {
5892  TryDragItemsToItem(linkedItem);
5893  }
5894  }
5895 
5896  void TryDragItemsToItem(Item item)
5897  {
5898  foreach (ItemContainer ic in item.GetComponents<ItemContainer>())
5899  {
5900  if (ic.Inventory?.visualSlots != null)
5901  {
5902  TryDragItemsToInventory(ic.Inventory);
5903  }
5904  }
5905  }
5906 
5907  void TryDragItemsToInventory(Inventory inv)
5908  {
5909  if (PlayerInput.IsCtrlDown()) { return; }
5910 
5911  var draggingMouse = MouseDragStart != Vector2.Zero && Vector2.Distance(PlayerInput.MousePosition, MouseDragStart) >= GUI.Scale * 20;
5912 
5913  // So we don't accidentally drag inventory items while doing this
5914  if (DraggedItemPrefab != null) { Inventory.DraggingItems.Clear(); }
5915 
5916  switch (DraggedItemPrefab)
5917  {
5918  // regular item prefabs
5919  case ItemPrefab itemPrefab when PlayerInput.PrimaryMouseButtonClicked() || draggingMouse:
5920  {
5921  bool spawnedItem = false;
5922  for (var i = 0; i < inv.Capacity; i++)
5923  {
5924  var slot = inv.visualSlots[i];
5925  var itemContainer = inv.GetItemAt(i)?.GetComponent<ItemContainer>();
5926 
5927  // check if the slot is empty or if we can place the item into a container, for example an oxygen tank into a diving suit
5928  if (Inventory.IsMouseOnSlot(slot))
5929  {
5930  var newItem = new Item(itemPrefab, Vector2.Zero, MainSub);
5931 
5932  if (inv.CanBePutInSlot(itemPrefab, i, condition: null))
5933  {
5934  bool placedItem = inv.TryPutItem(newItem, i, false, true, dummyCharacter);
5935  spawnedItem |= placedItem;
5936 
5937  if (!placedItem)
5938  {
5939  newItem.Remove();
5940  }
5941  }
5942  else if (itemContainer != null && itemContainer.Inventory.CanBePut(itemPrefab))
5943  {
5944  bool placedItem = itemContainer.Inventory.TryPutItem(newItem, dummyCharacter);
5945  spawnedItem |= placedItem;
5946 
5947  // try to place the item into the inventory of the item we are hovering over
5948  if (!placedItem)
5949  {
5950  newItem.Remove();
5951  }
5952  else
5953  {
5954  slot.ShowBorderHighlight(GUIStyle.Green, 0.1f, 0.4f);
5955  }
5956  }
5957  else
5958  {
5959  newItem.Remove();
5960  slot.ShowBorderHighlight(GUIStyle.Red, 0.1f, 0.4f);
5961  }
5962 
5963  if (!newItem.Removed)
5964  {
5965  BulkItemBufferInUse = ItemAddMutex;
5966  BulkItemBuffer.Add(new AddOrDeleteCommand(new List<MapEntity> { newItem }, false));
5967  }
5968 
5969  if (!draggingMouse)
5970  {
5971  SoundPlayer.PlayUISound(spawnedItem ? GUISoundType.PickItem : GUISoundType.PickItemFail);
5972  }
5973  }
5974  }
5975  break;
5976  }
5977  // item assemblies
5978  case ItemAssemblyPrefab assemblyPrefab when PlayerInput.PrimaryMouseButtonClicked():
5979  {
5980  bool spawnedItems = false;
5981  for (var i = 0; i < inv.visualSlots.Length; i++)
5982  {
5983  var slot = inv.visualSlots[i];
5984  var item = inv?.GetItemAt(i);
5985  var itemContainer = item?.GetComponent<ItemContainer>();
5986  if (item == null && Inventory.IsMouseOnSlot(slot))
5987  {
5988  // load the items
5989  var itemInstance = LoadItemAssemblyInventorySafe(assemblyPrefab);
5990 
5991  // counter for items that failed so we so we known that slot remained empty
5992  var failedCount = 0;
5993 
5994  for (var j = 0; j < itemInstance.Count; j++)
5995  {
5996  var newItem = itemInstance[j];
5997  var newSpot = i + j - failedCount;
5998 
5999  // try to find a valid slot to put the items
6000  while (inv.visualSlots.Length > newSpot)
6001  {
6002  if (inv.GetItemAt(newSpot) == null) { break; }
6003  newSpot++;
6004  }
6005 
6006  // valid slot found
6007  if (inv.visualSlots.Length > newSpot)
6008  {
6009  var placedItem = inv.TryPutItem(newItem, newSpot, false, true, dummyCharacter);
6010  spawnedItems |= placedItem;
6011 
6012  if (!placedItem)
6013  {
6014  failedCount++;
6015  // delete the included items too so we don't get a popup asking if we want to keep them
6016  newItem?.OwnInventory?.DeleteAllItems();
6017  newItem.Remove();
6018  }
6019  }
6020  else
6021  {
6022  var placedItem = inv.TryPutItem(newItem, dummyCharacter);
6023  spawnedItems |= placedItem;
6024 
6025  // if our while loop didn't find a valid slot then let the inventory decide where to put it as a last resort
6026  if (!placedItem)
6027  {
6028  // delete the included items too so we don't get a popup asking if we want to keep them
6029  newItem?.OwnInventory?.DeleteAllItems();
6030  newItem.Remove();
6031  }
6032  }
6033  }
6034 
6035  List<MapEntity> placedEntities = itemInstance.Where(it => !it.Removed).Cast<MapEntity>().ToList();
6036  if (placedEntities.Any())
6037  {
6038  BulkItemBufferInUse = ItemAddMutex;
6039  BulkItemBuffer.Add(new AddOrDeleteCommand(placedEntities, false));
6040  }
6041  }
6042  }
6043 
6044  SoundPlayer.PlayUISound(spawnedItems ? GUISoundType.PickItem : GUISoundType.PickItemFail);
6045  break;
6046  }
6047  }
6048  }
6049 
6050  if (PlayerInput.PrimaryMouseButtonReleased() && BulkItemBufferInUse != null)
6051  {
6052  CommitBulkItemBuffer();
6053  }
6054 
6056  {
6058  }
6059 
6060  // Update our mouse dragging state so we can easily slide thru slots while holding the mouse button down to place lots of items
6062  {
6063  if (MouseDragStart == Vector2.Zero)
6064  {
6065  MouseDragStart = PlayerInput.MousePosition;
6066  }
6067  }
6068  else
6069  {
6070  MouseDragStart = Vector2.Zero;
6071  }
6072 
6073  if ((GUI.MouseOn == null || !GUI.MouseOn.IsChildOf(TopPanel))
6074  && dummyCharacter?.SelectedItem == null && !WiringMode
6075  && (GUI.MouseOn == null || MapEntity.SelectedAny || MapEntity.SelectionPos != Vector2.Zero))
6076  {
6077  if (layerList is { Visible: true } && GUI.KeyboardDispatcher.Subscriber == layerList)
6078  {
6079  GUI.KeyboardDispatcher.Subscriber = null;
6080  }
6081 
6083  }
6084 
6086  {
6087  MeasurePositionStart = Vector2.Zero;
6088  }
6089 
6090  if (PlayerInput.KeyDown(Keys.LeftAlt) || PlayerInput.KeyDown(Keys.RightAlt))
6091  {
6093  {
6094  MeasurePositionStart = cam.ScreenToWorld(PlayerInput.MousePosition);
6095  }
6096  }
6097 
6098  if (!WiringMode)
6099  {
6100  bool shouldCloseHud = dummyCharacter?.SelectedItem != null && HUD.CloseHUD(dummyCharacter.SelectedItem.Rect) && DraggedItemPrefab == null;
6101 
6102  if (MapEntityPrefab.Selected != null)
6103  {
6105  }
6106  else
6107  {
6108  if (PlayerInput.SecondaryMouseButtonClicked() && !shouldCloseHud)
6109  {
6110  if (GUI.IsMouseOn(entityFilterBox))
6111  {
6112  ClearFilter();
6113  }
6114  else
6115  {
6116  if (dummyCharacter?.SelectedItem == null)
6117  {
6118  CreateContextMenu();
6119  }
6120  DraggedItemPrefab = null;
6121  }
6122  }
6123 
6124  if (shouldCloseHud)
6125  {
6126  CloseItem();
6127  }
6128  }
6129  MapEntity.UpdateEditor(cam, (float)deltaTime);
6130  }
6131 
6132  entityMenuOpenState = entityMenuOpen && !WiringMode ?
6133  (float)Math.Min(entityMenuOpenState + deltaTime * 5.0f, 1.0f) :
6134  (float)Math.Max(entityMenuOpenState - deltaTime * 5.0f, 0.0f);
6135 
6136  EntityMenu.RectTransform.ScreenSpaceOffset = Vector2.Lerp(new Vector2(0.0f, EntityMenu.Rect.Height - 10), Vector2.Zero, entityMenuOpenState).ToPoint();
6137 
6138  if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(entityFilterBox))
6139  {
6140  entityFilterBox.Deselect();
6141  }
6142 
6143  if (loadFrame != null)
6144  {
6146  {
6147  loadFrame = null;
6148  }
6149  }
6150  else if (saveFrame != null)
6151  {
6153  {
6154  saveFrame = null;
6155  }
6156  }
6157 
6158  if (dummyCharacter != null)
6159  {
6160  dummyCharacter.AnimController.FindHull(dummyCharacter.CursorWorldPosition, setSubmarine: false);
6161 
6162  foreach (Item item in dummyCharacter.Inventory.AllItems)
6163  {
6164  item.SetTransform(dummyCharacter.SimPosition, 0.0f);
6165  item.UpdateTransform();
6166  if (item.body != null)
6167  {
6168  item.SetTransform(item.body.SimPosition, 0.0f);
6169  }
6170 
6171  //wires need to be updated for the last node to follow the player during rewiring
6172  Wire wire = item.GetComponent<Wire>();
6173  wire?.Update((float)deltaTime, cam);
6174  }
6175 
6176  if (dummyCharacter.SelectedItem != null)
6177  {
6178  if (MapEntity.SelectedList.Contains(dummyCharacter.SelectedItem) || WiringMode)
6179  {
6180  dummyCharacter.SelectedItem?.UpdateHUD(cam, dummyCharacter, (float)deltaTime);
6181  }
6182  else
6183  {
6184  // We somehow managed to unfocus the item, close it so our framerate doesn't go to 5 because the
6185  // UpdateHUD() method keeps re-creating the editing HUD
6186  CloseItem();
6187  }
6188  }
6189  else if (MapEntity.SelectedList.Count == 1 && WiringMode && MapEntity.SelectedList.FirstOrDefault() is Item item)
6190  {
6191  item.UpdateHUD(cam, dummyCharacter, (float)deltaTime);
6192  }
6193 
6194  CharacterHUD.Update((float)deltaTime, dummyCharacter, cam);
6195  }
6196  }
6197 
6201  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
6202  {
6203  cam.UpdateTransform();
6204  if (lightingEnabled)
6205  {
6206  GameMain.LightManager.RenderLightMap(graphics, spriteBatch, cam);
6207  }
6208 
6209  foreach (Submarine sub in Submarine.Loaded)
6210  {
6211  sub.UpdateTransform();
6212  }
6213 
6214  graphics.Clear(BackgroundColor);
6215 
6216  ImageManager.Draw(spriteBatch, cam);
6217 
6218  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform);
6219 
6220  if (GameMain.DebugDraw)
6221  {
6222  GUI.DrawLine(spriteBatch, new Vector2(MainSub.HiddenSubPosition.X, -cam.WorldView.Y), new Vector2(MainSub.HiddenSubPosition.X, -(cam.WorldView.Y - cam.WorldView.Height)), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom));
6223  GUI.DrawLine(spriteBatch, new Vector2(cam.WorldView.X, -MainSub.HiddenSubPosition.Y), new Vector2(cam.WorldView.Right, -MainSub.HiddenSubPosition.Y), Color.White * 0.5f, 1.0f, (int)(2.0f / cam.Zoom));
6224  }
6225  Submarine.DrawBack(spriteBatch, true, e =>
6226  e is Structure s &&
6227  !IsSubcategoryHidden(e.Prefab?.Subcategory) &&
6228  (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null));
6229  Submarine.DrawPaintedColors(spriteBatch, true);
6230  spriteBatch.End();
6231 
6232  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform);
6233 
6234  // When we "open" a wearable item with inventory it won't get rendered because the dummy character is invisible
6235  // So we are drawing a clone of it on the same position
6236  if (OpenedItem?.GetComponent<Wearable>() != null)
6237  {
6238  OpenedItem.Sprite.Draw(spriteBatch, new Vector2(OpenedItem.DrawPosition.X, -(OpenedItem.DrawPosition.Y)),
6239  scale: OpenedItem.Scale, color: OpenedItem.SpriteColor, depth: OpenedItem.SpriteDepth);
6240  GUI.DrawRectangle(spriteBatch,
6241  new Vector2(OpenedItem.WorldRect.X, -OpenedItem.WorldRect.Y),
6242  new Vector2(OpenedItem.Rect.Width, OpenedItem.Rect.Height),
6243  Color.White, false, 0, (int)Math.Max(2.0f / cam.Zoom, 1.0f));
6244  }
6245 
6246  Submarine.DrawBack(spriteBatch, true, e =>
6247  (!(e is Structure) || e.SpriteDepth < 0.9f) &&
6248  !IsSubcategoryHidden(e.Prefab?.Subcategory));
6249  spriteBatch.End();
6250 
6251  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform);
6252  Submarine.DrawDamageable(spriteBatch, null, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory));
6253  spriteBatch.End();
6254 
6255  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, transformMatrix: cam.Transform);
6256  Submarine.DrawFront(spriteBatch, editing: true, e => !IsSubcategoryHidden(e.Prefab?.Subcategory));
6257  if (!WiringMode)
6258  {
6259  MapEntityPrefab.Selected?.DrawPlacing(spriteBatch, cam);
6260  MapEntity.DrawSelecting(spriteBatch, cam);
6261  }
6262  if (dummyCharacter != null && WiringMode)
6263  {
6264  foreach (Item heldItem in dummyCharacter.HeldItems)
6265  {
6266  heldItem.Draw(spriteBatch, editing: false, back: true);
6267  }
6268  }
6269 
6270  DrawGrid(spriteBatch);
6271  spriteBatch.End();
6272 
6273  ImageManager.DrawEditing(spriteBatch, cam);
6274 
6275  if (GameMain.LightManager.LightingEnabled && lightingEnabled)
6276  {
6277  spriteBatch.Begin(SpriteSortMode.Deferred, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None);
6278  spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White);
6279  spriteBatch.End();
6280  }
6281 
6282  if (GameMain.LightManager.DebugLos)
6283  {
6284  GameMain.LightManager.DebugDrawLos(spriteBatch, cam);
6285  }
6286 
6287  //-------------------- HUD -----------------------------
6288 
6289  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState);
6290 
6291  if (MainSub != null && cam.Zoom < 5f)
6292  {
6293  Vector2 position = MainSub.SubBody != null ? MainSub.WorldPosition : MainSub.HiddenSubPosition;
6294 
6295  GUI.DrawIndicator(
6296  spriteBatch, position, cam,
6297  cam.WorldView.Width,
6298  GUIStyle.SubmarineLocationIcon.Value.Sprite, Color.LightBlue * 0.5f);
6299  }
6300 
6301  var notificationIcon = GUIStyle.GetComponentStyle("GUINotificationButton");
6302  var tooltipStyle = GUIStyle.GetComponentStyle("GUIToolTip");
6303  foreach (Gap gap in Gap.GapList)
6304  {
6305  if (gap.linkedTo.Count == 2 && gap.linkedTo[0] == gap.linkedTo[1])
6306  {
6307  Vector2 screenPos = Cam.WorldToScreen(gap.WorldPosition);
6308  Rectangle rect = new Rectangle(screenPos.ToPoint() - new Point(20), new Point(40));
6309  tooltipStyle.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, Color.White);
6310  notificationIcon.Sprites[GUIComponent.ComponentState.None][0].Draw(spriteBatch, rect, GUIStyle.Orange);
6311  if (Vector2.Distance(PlayerInput.MousePosition, screenPos) < 30 * Cam.Zoom)
6312  {
6313  GUIComponent.DrawToolTip(spriteBatch, TextManager.Get("gapinsidehullwarning"), new Rectangle(screenPos.ToPoint(), new Point(10)));
6314  }
6315  }
6316  }
6317 
6318  if (DrawCharacterInventory)
6319  {
6320  dummyCharacter.DrawHUD(spriteBatch, cam, false);
6321  wiringToolPanel.DrawManually(spriteBatch);
6322  }
6323  MapEntity.DrawEditor(spriteBatch, cam);
6324 
6325  GUI.Draw(Cam, spriteBatch);
6326 
6327  if (MeasurePositionStart != Vector2.Zero)
6328  {
6329  Vector2 startPos = MeasurePositionStart;
6330  Vector2 mouseWorldPos = cam.ScreenToWorld(PlayerInput.MousePosition);
6331  if (PlayerInput.IsShiftDown())
6332  {
6333  startPos = RoundToGrid(startPos);
6334  mouseWorldPos = RoundToGrid(mouseWorldPos);
6335 
6336  static Vector2 RoundToGrid(Vector2 position)
6337  {
6338  position.X = (float) Math.Round(position.X / Submarine.GridSize.X) * Submarine.GridSize.X;
6339  position.Y = (float) Math.Round(position.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y;
6340  return position;
6341  }
6342  }
6343 
6344  GUI.DrawLine(spriteBatch, cam.WorldToScreen(startPos), cam.WorldToScreen(mouseWorldPos), GUIStyle.Green, width: 4);
6345 
6346  decimal realWorldDistance = decimal.Round((decimal) (Vector2.Distance(startPos, mouseWorldPos) * Physics.DisplayToRealWorldRatio), 2);
6347 
6348  Vector2 offset = new Vector2(GUI.IntScale(24));
6349  GUI.DrawString(spriteBatch, PlayerInput.MousePosition + offset, $"{realWorldDistance} m", GUIStyle.TextColorNormal, font: GUIStyle.Font, backgroundColor: Color.Black, backgroundPadding: 4);
6350  }
6351 
6352  spriteBatch.End();
6353  }
6354 
6355  private void CreateImage(int width, int height, System.IO.Stream stream)
6356  {
6357  MapEntity.SelectedList.Clear();
6359 
6360  var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle;
6361 
6362  Rectangle subDimensions = Submarine.MainSub.CalculateDimensions(onlyHulls: false);
6363  Vector2 viewPos = subDimensions.Center.ToVector2();
6364  float scale = Math.Min(width / (float)subDimensions.Width, height / (float)subDimensions.Height);
6365 
6366  var viewMatrix = Matrix.CreateTranslation(new Vector3(width / 2.0f, height / 2.0f, 0));
6367  var transform = Matrix.CreateTranslation(
6368  new Vector3(-viewPos.X, viewPos.Y, 0)) *
6369  Matrix.CreateScale(new Vector3(scale, scale, 1)) *
6370  viewMatrix;
6371 
6372  using (RenderTarget2D rt = new RenderTarget2D(
6373  GameMain.Instance.GraphicsDevice,
6374  width, height, false, SurfaceFormat.Color, DepthFormat.None))
6375  using (SpriteBatch spriteBatch = new SpriteBatch(GameMain.Instance.GraphicsDevice))
6376  {
6377  GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
6378  GameMain.Instance.GraphicsDevice.Clear(new Color(8, 13, 19));
6379 
6380  spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform);
6381  Submarine.Draw(spriteBatch);
6382  Submarine.DrawFront(spriteBatch);
6383  Submarine.DrawDamageable(spriteBatch, null);
6384  spriteBatch.End();
6385 
6386 
6387  GameMain.Instance.GraphicsDevice.SetRenderTarget(null);
6388  rt.SaveAsPng(stream, width, height);
6389  }
6390 
6391  //for some reason setting the rendertarget changes the size of the viewport
6392  //but it doesn't change back to default when setting it back to null
6393  GameMain.Instance.ResetViewPort();
6394  }
6395 
6396  private static readonly Color gridBaseColor = Color.White * 0.1f;
6397 
6398  private void DrawGrid(SpriteBatch spriteBatch)
6399  {
6400  // don't render at high zoom levels because it would just turn the screen white
6401  if (!ShouldDrawGrid) { return; }
6402 
6403  var (gridX, gridY) = Submarine.GridSize;
6404  DrawGrid(spriteBatch, cam, gridX, gridY, zoomTreshold: true);
6405  }
6406 
6407  public static void DrawGrid(SpriteBatch spriteBatch, Camera cam, float sizeX, float sizeY, bool zoomTreshold)
6408  {
6409  if (zoomTreshold && cam.Zoom < 0.5f) { return; }
6410  int scale = Math.Max(1, GUI.IntScale(1));
6411  float zoom = cam.Zoom / 2f; // Don't ask
6412  float lineThickness = Math.Max(1, scale / zoom);
6413 
6414  Color gridColor = gridBaseColor;
6415  if (zoomTreshold && cam.Zoom < 1.0f)
6416  {
6417  // fade the grid when zooming out
6418  gridColor *= Math.Max(0, (cam.Zoom - 0.5f) * 2f);
6419  }
6420 
6421  Rectangle camRect = cam.WorldView;
6422 
6423  for (float x = snapX(camRect.X); x < snapX(camRect.X + camRect.Width) + sizeX; x += sizeX)
6424  {
6425  spriteBatch.DrawLine(new Vector2(x, -camRect.Y), new Vector2(x, -(camRect.Y - camRect.Height)), gridColor, thickness: lineThickness);
6426  }
6427 
6428  for (float y = snapY(camRect.Y); y >= snapY(camRect.Y - camRect.Height) - sizeY; y -= sizeY)
6429  {
6430  spriteBatch.DrawLine(new Vector2(camRect.X, -y), new Vector2(camRect.Right, -y), gridColor, thickness: lineThickness);
6431  }
6432 
6433  float snapX(int x) => (float) Math.Floor(x / sizeX) * sizeX;
6434  float snapY(int y) => (float) Math.Ceiling(y / sizeY) * sizeY;
6435  }
6436 
6437  public static void DrawOutOfBoundsArea(SpriteBatch spriteBatch, Camera cam, float playableAreaSize, Color color)
6438  {
6439  Rectangle camRect = cam.WorldView;
6440 
6441  RectangleF playableArea = new RectangleF(
6442  -playableAreaSize / 2f,
6443  -playableAreaSize / 2f,
6444  playableAreaSize,
6445  playableAreaSize
6446  );
6447 
6448  RectangleF topRect = new(
6449  camRect.Left,
6450  -camRect.Top,
6451  camRect.Width,
6452  playableArea.Top + camRect.Top
6453  );
6454 
6455  // idk why camRect.Bottom doesn't work here
6456  float camRectBottom = -camRect.Top + camRect.Height;
6457 
6458  RectangleF bottomRect = new(
6459  camRect.Left,
6460  playableArea.Bottom,
6461  camRect.Width,
6462  camRectBottom + playableArea.Bottom
6463  );
6464 
6465  RectangleF rightRect = new(
6466  playableArea.Right,
6467  playableArea.Top,
6468  camRect.Right - playableArea.Right,
6469  playableArea.Height
6470  );
6471 
6472  RectangleF leftRect = new(
6473  playableArea.Left,
6474  playableArea.Top,
6475  camRect.Left - playableArea.Left,
6476  playableArea.Height
6477  );
6478 
6479  GUI.DrawFilledRectangle(spriteBatch, topRect, color);
6480  GUI.DrawFilledRectangle(spriteBatch, leftRect, color);
6481  GUI.DrawFilledRectangle(spriteBatch, rightRect, color);
6482  GUI.DrawFilledRectangle(spriteBatch, bottomRect, color);
6483  }
6484 
6485  public void SaveScreenShot(int width, int height, string filePath)
6486  {
6487  System.IO.Stream stream = File.OpenWrite(filePath);
6488  CreateImage(width, height, stream);
6489  stream.Dispose();
6490  }
6491 
6492  public bool IsSubcategoryHidden(string subcategory)
6493  {
6494  if (string.IsNullOrEmpty(subcategory) || !hiddenSubCategories.ContainsKey(subcategory))
6495  {
6496  return false;
6497  }
6498  return hiddenSubCategories[subcategory];
6499  }
6500 
6503 
6504  public static bool IsLayerVisible(MapEntity entity)
6505  {
6506  if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return true; }
6507 
6508  if (!Layers.TryGetValue(entity.Layer, out LayerData data))
6509  {
6510  Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
6511  return true;
6512  }
6513 
6514  return data.IsVisible;
6515  }
6516 
6517  public static bool IsLayerLinked(MapEntity entity)
6518  {
6519  if (!IsSubEditor() || string.IsNullOrWhiteSpace(entity.Layer)) { return false; }
6520 
6521  if (!Layers.TryGetValue(entity.Layer, out LayerData data))
6522  {
6523  Layers.TryAdd(entity.Layer, new LayerData(!entity.IsLayerHidden));
6524  return true;
6525  }
6526 
6527  return data.IsGrouped;
6528  }
6529 
6530  public static ImmutableHashSet<MapEntity> GetEntitiesInSameLayer(MapEntity entity)
6531  {
6532  if (string.IsNullOrWhiteSpace(entity.Layer)) { return ImmutableHashSet<MapEntity>.Empty; }
6533  return MapEntity.MapEntityList.Where(me => me.Layer == entity.Layer).ToImmutableHashSet();
6534  }
6535  }
6536 }
Vector2 WorldToScreen(Vector2 coords)
Definition: Camera.cs:416
float? Zoom
Definition: Camera.cs:78
Rectangle WorldView
Definition: Camera.cs:123
static void Update(float deltaTime, Character character, Camera cam)
Item????????? SelectedItem
The primary selected item. It can be any device that character interacts with. This excludes items li...
void ControlLocalPlayer(float deltaTime, Camera cam, bool moveCam=true)
Control the Character according to player input
IEnumerable< Item >?? HeldItems
Items the character has in their hand slots. Doesn't return nulls and only returns items held in both...
void DrawHUD(SpriteBatch spriteBatch, Camera cam, bool drawHealth=true)
override bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
InvSlotType[] SlotTypes
Slot type for each inventory slot. Vanilla package has one type for each slot, although it is technic...
ImmutableArray< ContentFile > Files
virtual Vector2 WorldPosition
Definition: Entity.cs:49
virtual Vector2 DrawPosition
Definition: Entity.cs:51
Submarine Submarine
Definition: Entity.cs:53
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
static Entity FindEntityByID(ushort ID)
Find an entity based on the ID
Definition: Entity.cs:204
OnClickedHandler OnClicked
Definition: GUIButton.cs:16
override void ApplyStyle(GUIComponentStyle style)
Definition: GUIButton.cs:232
override RichString ToolTip
Definition: GUIButton.cs:150
OnColorSelectedHandler? OnColorSelected
GUIComponent GetChild(int index)
Definition: GUIComponent.cs:54
virtual void RemoveChild(GUIComponent child)
Definition: GUIComponent.cs:87
virtual void Flash(Color? color=null, float flashDuration=1.5f, bool useRectangleFlash=false, bool useCircularFlash=false, Vector2? flashRectInflate=null)
virtual void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
virtual Rectangle? MouseRect
GUIComponent GetChildByUserData(object obj)
Definition: GUIComponent.cs:66
virtual void ClearChildren()
GUIComponent FindChild(Func< GUIComponent, bool > predicate, bool recursive=false)
Definition: GUIComponent.cs:95
virtual void DrawManually(SpriteBatch spriteBatch, bool alsoChildren=false, bool recursive=true)
By default, all the gui elements are drawn automatically in the same order they appear on the update ...
virtual Rectangle Rect
IEnumerable< GUIComponent > GetAllChildren()
Returns all child elements in the hierarchy.
Definition: GUIComponent.cs:49
void DrawToolTip(SpriteBatch spriteBatch)
Creates and draws a tooltip.
RectTransform RectTransform
SpriteEffects SpriteEffects
IEnumerable< GUIComponent > Children
Definition: GUIComponent.cs:29
void FadeOut(float duration, bool removeAfter, float wait=0.0f, Action onRemove=null, bool alsoChildren=false)
IEnumerable< GUIComponent > FindChildren(object userData)
GUIComponent that can be used to render custom content on the UI
GUIComponent AddItem(LocalizedString text, object userData=null, LocalizedString toolTip=null, Color? color=null, Color? textColor=null)
Definition: GUIDropDown.cs:270
OnSelectedHandler OnSelected
Triggers when some item is cliecked from the dropdown. Note that SelectedData is not set yet when thi...
Definition: GUIDropDown.cs:20
OnSelectedHandler OnDropped
Definition: GUIDropDown.cs:27
override void ClearChildren()
Definition: GUIDropDown.cs:329
Sprite?? Sprite
Definition: GUIImage.cs:84
override void RemoveChild(GUIComponent child)
Definition: GUIListBox.cs:1270
override void AddToGUIUpdateList(bool ignoreChildren=false, int order=0)
Definition: GUIListBox.cs:823
delegate bool OnSelectedHandler(GUIComponent component, object obj)
GUIFrame Content
A frame that contains the contents of the listbox. The frame itself is not rendered.
Definition: GUIListBox.cs:42
static readonly List< GUIComponent > MessageBoxes
GUILayoutGroup Content
OnValueChangedHandler OnValueChanged
OnMovedHandler OnMoved
Definition: GUIScrollBar.cs:26
override GUIFont Font
Definition: GUITextBlock.cs:66
TextBoxEvent OnDeselected
Definition: GUITextBox.cs:17
OnTextChangedHandler OnTextChanged
Don't set the Text property on delegates that register to this event, because modifying the Text will...
Definition: GUITextBox.cs:38
void Select(int forcedCaretIndex=-1, bool ignoreSelectSound=false)
Definition: GUITextBox.cs:377
OnEnterHandler OnEnterPressed
Definition: GUITextBox.cs:29
override void Flash(Color? color=null, float flashDuration=1.5f, bool useRectangleFlash=false, bool useCircularFlash=false, Vector2? flashRectOffset=null)
Definition: GUITextBox.cs:411
override bool Selected
Definition: GUITickBox.cs:18
static int GraphicsWidth
Definition: GameMain.cs:162
void ResetViewPort()
Definition: GameMain.cs:384
static SubEditorScreen SubEditorScreen
Definition: GameMain.cs:68
static int GraphicsHeight
Definition: GameMain.cs:168
static Lights.LightManager LightManager
Definition: GameMain.cs:78
static World World
Definition: GameMain.cs:105
static bool DebugDraw
Definition: GameMain.cs:29
static GameMain Instance
Definition: GameMain.cs:144
static LuaCsSetup LuaCs
Definition: GameMain.cs:26
static Sounds.SoundManager SoundManager
Definition: GameMain.cs:80
static void UpdateCheats(float deltaTime, Camera cam)
virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition=false)
Can the item be put in the specified slot.
virtual bool TryPutItem(Item item, Character user, IEnumerable< InvSlotType > allowedSlots=null, bool createNetworkEvent=true, bool ignoreCondition=false)
If there is room, puts the item in the inventory and returns true, otherwise returns false
static bool IsMouseOnSlot(VisualSlot slot)
Check if the mouse is hovering on top of the slot
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
Item GetItemAt(int index)
Get the item stored in the specified inventory slot. If the slot contains a stack of items,...
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
override void AddToGUIUpdateList(int order=0)
void SetTransform(Vector2 simPosition, float rotation, bool findNewHull=true, bool setPrevTransform=true)
override void Draw(SpriteBatch spriteBatch, bool editing, bool back=true)
void UpdateHUD(Camera cam, Character character, float deltaTime)
static readonly List< Item > ItemList
static readonly PrefabCollection< ItemPrefab > Prefabs
override ImmutableHashSet< Identifier > Tags
The base class for components holding the different functionalities of the item
override void Move(Vector2 amount, bool ignoreContacts=false)
static IEnumerable< MapEntity > HighlightedEntities
static readonly List< MapEntity > MapEntityList
static void DrawEditor(SpriteBatch spriteBatch, Camera cam)
static void UpdateEditor(Camera cam, float deltaTime)
bool IsLayerHidden
Is the layer this entity is in currently hidden? If it is, the entity is not updated and should do no...
static void UpdateHighlighting(List< MapEntity > highlightedEntities, bool wiringMode=false)
Updates the logic that runs the highlight box when the mouse is sitting still.
static void UpdateSelecting(Camera cam)
Update the selection logic in submarine editor
static void DrawSelecting(SpriteBatch spriteBatch, Camera cam)
Draw the "selection rectangle" and outlines of entities that are being dragged (if any)
virtual void DrawPlacing(SpriteBatch spriteBatch, Camera cam)
static MapEntityPrefab Find(string name, string identifier=null, bool showErrorMessages=true)
Find a matching map entity prefab
bool SetTransform(Vector2 simPosition, float rotation, bool setPrevTransform=true)
static bool KeyDown(InputType inputType)
void FindHull(Vector2? worldPosition=null, bool setSubmarine=true)
Point ScreenSpaceOffset
Screen space offset. From top left corner. In pixels.
Point AbsoluteOffset
Absolute in pixels but relative to the anchor point. Calculated away from the anchor point,...
Vector2 RelativeSize
Relative to the parent rect.
IEnumerable< RectTransform > Children
void SortChildren(Comparison< RectTransform > comparison)
Point?? MinSize
Min size in pixels. Does not affect scaling.
Point NonScaledSize
Size before scale multiplications.
Point?? MaxSize
Max size in pixels. Does not affect scaling.
static RichString Rich(LocalizedString str, Func< string, string >? postProcess=null)
Definition: RichString.cs:67
Dictionary< Identifier, GUIComponent[]> Fields
Holds the references to the input fields.
void UpdateValue(SerializableProperty property, object newValue, bool flash=true)
const string SoundCategoryWaterAmbience
Definition: SoundManager.cs:17
void SetCategoryGainMultiplier(string category, float gain, int index=0)
void Draw(ISpriteBatch spriteBatch, Vector2 pos, float rotate=0.0f, float scale=1.0f, SpriteEffects spriteEffect=SpriteEffects.None)
static List< WarningType > SuppressedWarnings
override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
This is called when the game should draw itself.
void SetMode(Mode newMode)
void SaveScreenShot(int width, int height, string filePath)
override void Update(double deltaTime)
Allows the game to run logic such as updating the world, checking for collisions, gathering input,...
void Select(bool enableAutoSave=true)
static bool IsLayerVisible(MapEntity entity)
static XDocument AutoSaveInfo
static readonly List< Command > Commands
Global undo/redo state for the sub editor and a selector index for it Command
static GUIMessageBox CreatePropertyColorPicker(Color originalColor, SerializableProperty property, ISerializableEntity entity)
static List< AddOrDeleteCommand > BulkItemBuffer
static void DrawOutOfBoundsArea(SpriteBatch spriteBatch, Camera cam, float playableAreaSize, Color color)
override void AddToGUIUpdateList()
By default, submits the screen's main GUIFrame and, if requested upon construction,...
bool IsMouseOnEditorGUI()
GUI.MouseOn doesn't get updated while holding primary mouse and we need it to
bool IsSubcategoryHidden(string subcategory)
static bool IsWiringMode()
void LoadSub(SubmarineInfo info, bool checkIdConflicts=true)
static readonly EditorImageManager ImageManager
static void StoreCommand(Command command)
static readonly object ItemAddMutex
override void DeselectEditorSpecific()
static GUIFrame CreateWiringPanel(Point offset, GUIListBox.OnSelectedHandler onWireSelected)
static ImmutableHashSet< MapEntity > GetEntitiesInSameLayer(MapEntity entity)
static bool IsLayerLinked(MapEntity entity)
override void OnFileDropped(string filePath, string extension)
static Type DetermineSubFileType(SubmarineType type)
static void DrawGrid(SpriteBatch spriteBatch, Camera cam, float sizeX, float sizeY, bool zoomTreshold)
static MapEntityPrefab DraggedItemPrefab
Prefab used for dragging from the item catalog into inventories GUI.Draw
static void DrawFront(SpriteBatch spriteBatch, bool editing=false, Predicate< MapEntity > predicate=null)
static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing=false, Predicate< MapEntity > predicate=null)
static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing=false, Predicate< MapEntity > predicate=null)
static void Draw(SpriteBatch spriteBatch, bool editing=false)
Rectangle CalculateDimensions(bool onlyHulls=true)
static Submarine MainSub
Note that this can be null in some situations, e.g. editors and missions that don't load a submarine.
static void DrawBack(SpriteBatch spriteBatch, bool editing=false, Predicate< MapEntity > predicate=null)
NumberType
Definition: Enums.cs:741
GUISoundType
Definition: GUI.cs:21
@ Character
Characters only
@ Structure
Structures and hulls, but also items (for backwards support)!