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