Client LuaCsForBarotrauma
EventEditorScreen.cs
1 #nullable enable
2 using Barotrauma.IO;
3 using Microsoft.Xna.Framework;
4 using Microsoft.Xna.Framework.Graphics;
5 using Microsoft.Xna.Framework.Input;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Reflection;
10 using System.Xml.Linq;
11 using Directory = System.IO.Directory;
12 
13 namespace Barotrauma
14 {
15  internal class EventEditorScreen : EditorScreen
16  {
17  private GUIFrame GuiFrame = null!;
18 
19  public override Camera Cam { get; }
20  public static string? DrawnTooltip { get; set; }
21 
22  public static readonly List<EditorNode> nodeList = new List<EditorNode>();
23 
24  private readonly List<EditorNode> selectedNodes = new List<EditorNode>();
25 
26  public static Vector2 DraggingPosition = Vector2.Zero;
27  public static EventEditorNodeConnection? DraggedConnection;
28 
29  private EditorNode? draggedNode;
30  private Vector2 dragOffset;
31 
32  private readonly Dictionary<EditorNode, Vector2> markedNodes = new Dictionary<EditorNode, Vector2>();
33 
34  private static string projectName = string.Empty;
35 
36  private OutpostGenerationParams? lastTestParam;
37  private LocationType? lastTestType;
38 
39  private GUITickBox? isTraitorEventBox;
40 
41  private static int CreateID()
42  {
43  int maxId = nodeList.Any() ? nodeList.Max(node => node.ID) : 0;
44  return ++maxId;
45  }
46 
47  private Point screenResolution;
48 
49  public EventEditorScreen()
50  {
51  Cam = new Camera();
52  nodeList.Clear();
53  CreateGUI();
54  }
55 
56  private void CreateGUI()
57  {
58  GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUI.Canvas) { MinSize = new Point(300, 420) });
59  GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true, AbsoluteSpacing = GUI.IntScale(5) };
60 
61  // === BUTTONS === //
62  GUILayoutGroup buttonLayout = new GUILayoutGroup(RectTransform(1.0f, 0.50f, layoutGroup)) { RelativeSpacing = 0.04f };
63  GUIButton newProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.NewProject"));
64  GUIButton saveProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.SaveProject"));
65  GUIButton loadProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.LoadProject"));
66  GUIButton exportProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.Export"));
67 
68 
69  // === LOAD PREFAB === //
70 
71  GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
72  new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUIStyle.SubHeadingFont);
73 
74  GUILayoutGroup loadDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, loadEventLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
75  GUIDropDown loadDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, loadDropdownLayout), elementCount: 10);
76  GUIButton loadButton = new GUIButton(RectTransform(0.2f, 1.0f, loadDropdownLayout), TextManager.Get("Load"));
77 
78  // === ADD ACTION === //
79 
80  GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
81  new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUIStyle.SubHeadingFont);
82 
83  GUILayoutGroup addActionDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addActionLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
84  GUIDropDown addActionDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addActionDropdownLayout), elementCount: 10);
85  GUIButton addActionButton = new GUIButton(RectTransform(0.2f, 1.0f, addActionDropdownLayout), TextManager.Get("EventEditor.Add"));
86 
87  // === ADD VALUE === //
88  GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
89  new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUIStyle.SubHeadingFont);
90 
91  GUILayoutGroup addValueDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addValueLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
92  GUIDropDown addValueDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addValueDropdownLayout), elementCount: 7);
93  GUIButton addValueButton = new GUIButton(RectTransform(0.2f, 1.0f, addValueDropdownLayout), TextManager.Get("EventEditor.Add"));
94 
95  // === ADD SPECIAL === //
96  GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
97  new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUIStyle.SubHeadingFont);
98  GUILayoutGroup addSpecialDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addSpecialLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
99  GUIDropDown addSpecialDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addSpecialDropdownLayout), elementCount: 1);
100  GUIButton addSpecialButton = new GUIButton(RectTransform(0.2f, 1.0f, addSpecialDropdownLayout), TextManager.Get("EventEditor.Add"));
101 
102  // Add event prefabs with identifiers to the list
103  foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(p => !p.Identifier.IsEmpty).Distinct().OrderBy(p => p.Identifier))
104  {
105  if (!typeof(ScriptedEvent).IsAssignableFrom(eventPrefab.EventType)) { continue; }
106  var textBlock = loadDropdown.AddItem(eventPrefab.Identifier.Value!, eventPrefab) as GUITextBlock;
107  if (eventPrefab is TraitorEventPrefab && textBlock != null)
108  {
109  textBlock.TextColor = Color.MediumPurple;
110  }
111  }
112 
113  // Add all types that inherit the EventAction class
114  foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(type => type.IsSubclassOf(typeof(EventAction))).OrderBy(t => t.Name))
115  {
116  addActionDropdown.AddItem(type.Name, type);
117  }
118 
119  addSpecialDropdown.AddItem("Custom", typeof(CustomNode));
120 
121  addValueDropdown.AddItem(nameof(Single), typeof(float));
122  addValueDropdown.AddItem(nameof(Boolean), typeof(bool));
123  addValueDropdown.AddItem(nameof(String), typeof(string));
124  addValueDropdown.AddItem(nameof(SpawnType), typeof(SpawnType));
125  addValueDropdown.AddItem(nameof(LimbType), typeof(LimbType));
126  addValueDropdown.AddItem(nameof(ReputationAction.ReputationType), typeof(ReputationAction.ReputationType));
127  addValueDropdown.AddItem(nameof(SpawnAction.SpawnLocationType), typeof(SpawnAction.SpawnLocationType));
128  addValueDropdown.AddItem(nameof(CharacterTeamType), typeof(CharacterTeamType));
129 
130  loadButton.OnClicked += (button, o) => Load(loadDropdown.SelectedData as EventPrefab);
131  addActionButton.OnClicked += (button, o) => AddAction(addActionDropdown.SelectedData as Type);
132  addValueButton.OnClicked += (button, o) => AddValue(addValueDropdown.SelectedData as Type);
133  addSpecialButton.OnClicked += (button, o) => AddSpecial(addSpecialDropdown.SelectedData as Type);
134  exportProjectButton.OnClicked += ExportEventToFile;
135  saveProjectButton.OnClicked += SaveProjectToFile;
136  newProjectButton.OnClicked += TryCreateNewProject;
137  loadProjectButton.OnClicked += (button, o) =>
138  {
139  FileSelection.OnFileSelected = (file) =>
140  {
141  XDocument? document = XMLExtensions.TryLoadXml(file);
142  if (document?.Root != null)
143  {
144  Load(document.Root);
145  }
146  };
147 
148  string directory = Path.GetFullPath("EventProjects");
149  if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); }
150 
151  FileSelection.ClearFileTypeFilters();
152  FileSelection.AddFileTypeFilter("Scripted Event", "*.sevproj");
153  FileSelection.SelectFileTypeFilter("*.sevproj");
154  FileSelection.CurrentDirectory = directory;
155  FileSelection.Open = true;
156  return true;
157  };
158 
159  isTraitorEventBox = new GUITickBox(RectTransform(1.0f, 0.125f, layoutGroup), "Traitor event");
160 
161  screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
162  }
163 
164  private bool ExportEventToFile(GUIButton button, object o)
165  {
166  XElement? save = ExportXML();
167  if (save != null)
168  {
169  try
170  {
171  string directory = Path.GetFullPath("EventProjects");
172  if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); }
173 
174  string exportPath = Path.Combine(directory, "Exported");
175  if (!Directory.Exists(exportPath)) { Directory.CreateDirectory(exportPath); }
176 
177  var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.ExportProjectPrompt"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("EventEditor.Export") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
178  var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true);
179  GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName };
180 
181  // Cancel button
182  msgBox.Buttons[0].OnClicked = delegate
183  {
184  msgBox.Close();
185  return true;
186  };
187 
188  // Ok button
189  msgBox.Buttons[1].OnClicked = delegate
190  {
191  foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform())
192  {
193  if (!nameInput.Text.Contains(illegalChar)) { continue; }
194 
195  GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red);
196  return false;
197  }
198 
199  msgBox.Close();
200  string path = Path.Combine(exportPath, $"{nameInput.Text}.xml");
201  File.WriteAllText(path, save.ToString(), catchUnauthorizedAccessExceptions: false);
202  AskForConfirmation(TextManager.Get("EventEditor.OpenTextHeader"), TextManager.Get("EventEditor.OpenTextBody"), () =>
203  {
204  ToolBox.OpenFileWithShell(path);
205  return true;
206  });
207  GUI.AddMessage($"XML exported to {path}", GUIStyle.Green);
208  return true;
209  };
210  }
211  catch (Exception e)
212  {
213  DebugConsole.ThrowError("Failed to export event", e);
214  }
215  }
216  else
217  {
218  GUI.AddMessage("Unable to export because the project contains errors", GUIStyle.Red);
219  }
220 
221  return true;
222  }
223 
224  private bool TryCreateNewProject(GUIButton button, object o)
225  {
226  AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () =>
227  {
228  nodeList.Clear();
229  markedNodes.Clear();
230  selectedNodes.Clear();
231  projectName = TextManager.Get("EventEditor.Unnamed").Value;
232  return true;
233  });
234  return true;
235  }
236 
237  public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func<bool> onConfirm, GUISoundType? overrideConfirmButtonSound = null)
238  {
239  LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
240  GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons);
241 
242  // Cancel button
243  msgBox.Buttons[1].OnClicked = delegate
244  {
245  msgBox.Close();
246  return true;
247  };
248 
249  // Ok button
250  msgBox.Buttons[0].OnClicked = delegate
251  {
252  onConfirm.Invoke();
253  msgBox.Close();
254  return true;
255  };
256  if (overrideConfirmButtonSound.HasValue)
257  {
258  msgBox.Buttons[0].ClickSound = overrideConfirmButtonSound.Value;
259  }
260  return msgBox;
261  }
262 
263  private bool SaveProjectToFile(GUIButton button, object o)
264  {
265  string directory = Path.GetFullPath("EventProjects");
266 
267  if (!Directory.Exists(directory))
268  {
269  Directory.CreateDirectory(directory);
270  }
271 
272  var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.NameFilePrompt"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("Save") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
273  var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true);
274  GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName };
275 
276  // Cancel button
277  msgBox.Buttons[0].OnClicked = delegate
278  {
279  msgBox.Close();
280  return true;
281  };
282 
283  // Ok button
284  msgBox.Buttons[1].OnClicked = delegate
285  {
286  foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform())
287  {
288  if (!nameInput.Text.Contains(illegalChar)) { continue; }
289 
290  GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red);
291  return false;
292  }
293 
294  msgBox.Close();
295  projectName = nameInput.Text;
296  XElement save = SaveEvent(projectName);
297  string filePath = System.IO.Path.Combine(directory, $"{projectName}.sevproj");
298  File.WriteAllText(Path.Combine(directory, $"{projectName}.sevproj"), save.ToString());
299  GUI.AddMessage($"Project saved to {filePath}", GUIStyle.Green);
300 
301  AskForConfirmation(TextManager.Get("EventEditor.TestPromptHeader"), TextManager.Get("EventEditor.TestPromptBody"), CreateTestSetupMenu);
302  return true;
303  };
304  return true;
305  }
306 
307  private bool Load(EventPrefab? prefab)
308  {
309  if (prefab == null) { return false; }
310 
311  AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () =>
312  {
313  nodeList.Clear();
314  selectedNodes.Clear();
315  markedNodes.Clear();
316  if (isTraitorEventBox != null)
317  {
318  isTraitorEventBox.Selected = prefab is TraitorEventPrefab;
319  }
320  bool hadNodes = true;
321  CreateNodes(prefab.ConfigElement, ref hadNodes);
322  if (!hadNodes)
323  {
324  GUI.NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody"));
325  }
326  return true;
327  });
328  return true;
329  }
330 
331  private bool AddAction(Type? type)
332  {
333  if (type == null) { return false; }
334 
335  Vector2 spawnPos = Cam.WorldViewCenter;
336  spawnPos.Y = -spawnPos.Y;
337  EventNode newNode = new EventNode(type, type.Name) { ID = CreateID() };
338  newNode.Position = spawnPos - newNode.Size / 2;
339  nodeList.Add(newNode);
340  return true;
341  }
342 
343  private bool AddValue(Type? type)
344  {
345  if (type == null) { return false; }
346 
347  Vector2 spawnPos = Cam.WorldViewCenter;
348  spawnPos.Y = -spawnPos.Y;
349  ValueNode newValue = new ValueNode(type, type.Name) { ID = CreateID() };
350  newValue.Position = spawnPos - newValue.Size / 2;
351  nodeList.Add(newValue);
352  return true;
353  }
354 
355  private bool AddSpecial(Type? type)
356  {
357  if (type == null) { return false; }
358  Vector2 spawnPos = Cam.WorldViewCenter;
359  spawnPos.Y = -spawnPos.Y;
360 
361  ConstructorInfo? constructor = type.GetConstructor(Array.Empty<Type>());
362  SpecialNode? newNode = null;
363  if (constructor != null)
364  {
365  newNode = constructor.Invoke(Array.Empty<object>()) as SpecialNode;
366  }
367  if (newNode != null)
368  {
369  newNode.ID = CreateID();
370  newNode.Position = spawnPos - newNode.Size / 2;
371  nodeList.Add(newNode);
372  return true;
373  }
374  return false;
375  }
376 
377  private void CreateNodes(ContentXElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0)
378  {
379  EditorNode? lastNode = null;
380  foreach (var subElement in element.Elements())
381  {
382  bool skip = true;
383  switch (subElement.Name.ToString().ToLowerInvariant())
384  {
385  case "failure":
386  case "success":
387  case "option":
388  CreateNodes(subElement, ref hadNodes, parent, ident);
389  break;
390  default:
391  skip = false;
392  break;
393  }
394 
395  if (!skip)
396  {
397  Vector2 defaultNodePos = new Vector2(-16000, -16000);
398  EditorNode newNode;
399  Type? t = Type.GetType($"Barotrauma.{subElement.Name}");
400  if (t != null && EditorNode.IsInstanceOf(t, typeof(EventAction)))
401  {
402  newNode = new EventNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
403  }
404  else
405  {
406  newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
407  foreach (XAttribute attribute in subElement.Attributes().Where(attribute => !attribute.ToString().StartsWith("_")))
408  {
409  newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string)));
410  }
411  }
412 
413  Vector2 npos = subElement.GetAttributeVector2("_npos", defaultNodePos);
414  if (npos != defaultNodePos)
415  {
416  newNode.Position = npos;
417  }
418  else
419  {
420  hadNodes = false;
421  }
422 
423  var parentElement = subElement.Parent;
424  foreach (var xElement in subElement.Elements())
425  {
426  switch (xElement.Name.ToString().ToLowerInvariant())
427  {
428  case "option":
429  EventEditorNodeConnection optionConnection = new EventEditorNodeConnection(newNode, NodeConnectionType.Option)
430  {
431  OptionText = xElement.GetAttributeString("text", string.Empty),
432  EndConversation = xElement.GetAttributeBool("endconversation", false)
433  };
434  newNode.Connections.Add(optionConnection);
435  break;
436  }
437  }
438 
439  foreach (EventEditorNodeConnection connection in newNode.Connections)
440  {
441  if (connection.Type == NodeConnectionType.Value)
442  {
443  foreach (XAttribute attribute in subElement.Attributes())
444  {
445  if (string.Equals(connection.Attribute, attribute.Name.ToString(), StringComparison.InvariantCultureIgnoreCase) && connection.ValueType != null)
446  {
447  if (connection.ValueType.IsEnum)
448  {
449  Array values = Enum.GetValues(connection.ValueType);
450  foreach (object? @enum in values)
451  {
452  if (string.Equals(@enum?.ToString(), attribute.Value, StringComparison.InvariantCultureIgnoreCase))
453  {
454  connection.OverrideValue = @enum;
455  }
456  }
457  }
458  else
459  {
460  try
461  {
462  connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType);
463  }
464  catch
465  {
466  DebugConsole.ThrowError($"Failed to convert the value {attribute.Value} of the attribute {attribute.Name} to {connection.ValueType}.");
467  }
468  }
469  }
470  }
471  }
472  }
473 
474  if (npos == defaultNodePos)
475  {
476  hadNodes = false;
477  bool Predicate(EditorNode node) => Rectangle.Union(node.GetDrawRectangle(), node.HeaderRectangle).Intersects(Rectangle.Union(newNode.GetDrawRectangle(), newNode.HeaderRectangle));
478 
479  while (nodeList.Any(Predicate))
480  {
481  EditorNode? otherNode = nodeList.Find(Predicate);
482  if (otherNode != null)
483  {
484  newNode.Position += new Vector2(128, otherNode.GetDrawRectangle().Height + otherNode.HeaderRectangle.Height + new Random().Next(128, 256));
485  }
486  }
487  }
488 
489  if (subElement.Name.ToString().ToLowerInvariant() is "text" or "conditional")
490  {
491  parent?.AddConnection(NodeConnectionType.Add);
492  parent?.Connect(newNode, NodeConnectionType.Add);
493  }
494  else
495  {
496  if (parentElement?.FirstElement() == subElement)
497  {
498  switch (parentElement?.Name.ToString().ToLowerInvariant())
499  {
500  case "failure":
501  parent?.Connect(newNode, NodeConnectionType.Failure);
502  break;
503  case "success":
504  parent?.Connect(newNode, NodeConnectionType.Success);
505  break;
506  case "onroundendaction":
507  parent?.Connect(newNode, NodeConnectionType.Next);
508  break;
509  case "option":
510  if (parent != null)
511  {
512  EventEditorNodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate);
513  EventEditorNodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection =>
514  connection.Type == NodeConnectionType.Option && string.Equals(connection.OptionText, parentElement.GetAttributeString("text", string.Empty), StringComparison.Ordinal));
515 
516  if (activateConnection != null)
517  {
518  optionConnection?.ConnectedTo.Add(activateConnection);
519  }
520  }
521  break;
522  default:
523  parent?.Connect(newNode, NodeConnectionType.Add);
524  break;
525  }
526  }
527  else
528  {
529  lastNode?.Connect(newNode, NodeConnectionType.Next);
530  }
531  }
532 
533  lastNode = newNode;
534  nodeList.Add(newNode);
535  ident += 600;
536  CreateNodes(subElement, ref hadNodes, newNode, ident);
537  }
538  else
539  {
540 
541  }
542  }
543  }
544 
545  private static RectTransform RectTransform(float x, float y, GUIComponent parent, Anchor anchor = Anchor.TopRight)
546  {
547  return new RectTransform(new Vector2(x, y), parent.RectTransform, anchor);
548  }
549 
550  public override void Select()
551  {
552  GUI.PreventPauseMenuToggle = false;
553  projectName = TextManager.Get("EventEditor.Unnamed").Value;
554  base.Select();
555  }
556 
557  public override void AddToGUIUpdateList()
558  {
559  GuiFrame.AddToGUIUpdateList();
560  }
561 
562  public static object? ChangeType(string value, Type type)
563  {
564  if (type == typeof(Identifier))
565  {
566  return value.ToIdentifier();
567  }
568  else
569  {
570  return Convert.ChangeType(value, type);
571  }
572  }
573 
574  private XElement? ExportXML()
575  {
576  XElement mainElement = new XElement(
577  isTraitorEventBox is { Selected: true } ? nameof(TraitorEvent) : nameof(ScriptedEvent),
578  new XAttribute("identifier", projectName.RemoveWhitespace().ToLowerInvariant()));
579  EditorNode? startNode = null;
580  foreach (EditorNode eventNode in nodeList.Where(node => node is EventNode || node is SpecialNode))
581  {
582  if (eventNode.GetParent() == null)
583  {
584  if (startNode != null)
585  {
586  DebugConsole.ThrowError("You have more than one start node, only one will be picked while the others will get ignored.");
587  }
588  startNode ??= eventNode;
589  }
590  }
591 
592  if (startNode == null) { return null; }
593 
594  ExportChildNodes(startNode, mainElement);
595 
596  return mainElement;
597  }
598 
599  private void ExportChildNodes(EditorNode startNode, XElement parent)
600  {
601  XElement? newElement = startNode.ToXML();
602  if (newElement == null) { return; }
603  parent.Add(newElement);
604 
605  EditorNode? success = startNode.GetNext(NodeConnectionType.Success);
606  EditorNode? failure = startNode.GetNext(NodeConnectionType.Failure);
607  EditorNode? add = startNode.GetNext(NodeConnectionType.Add);
608  Tuple<EditorNode?, string?, bool>[] options = startNode is EventNode eNode ? eNode.GetOptions() : new Tuple<EditorNode?, string?, bool>[0];
609 
610  if (success != null)
611  {
612  XElement successElement = new XElement("Success");
613  ExportChildNodes(success, successElement);
614  newElement.Add(successElement);
615  }
616 
617  if (failure != null)
618  {
619  XElement failureElement = new XElement("Failure");
620  ExportChildNodes(failure, failureElement);
621  newElement.Add(failureElement);
622  }
623 
624  if (add is CustomNode custom)
625  {
626  ExportChildNodes(custom, newElement);
627  }
628 
629  foreach (var (node, text, end) in options)
630  {
631  XElement optionElement = new XElement("Option");
632  optionElement.Add(new XAttribute("text", text ?? ""));
633  if (end) { optionElement.Add(new XAttribute("endconversation", true)); }
634 
635  if (node is EventNode eventNode)
636  {
637  ExportChildNodes(eventNode, optionElement);
638  }
639 
640  newElement.Add(optionElement);
641  }
642 
643  EditorNode? next = startNode.GetNext();
644  if (next != null)
645  {
646  ExportChildNodes(next, parent);
647  }
648  }
649 
650  private static XElement SaveEvent(string name)
651  {
652  XElement mainElement = new XElement("SavedEvent", new XAttribute("name", name));
653  XElement nodes = new XElement("Nodes");
654  foreach (var editorNode in nodeList)
655  {
656  nodes.Add(editorNode.Save());
657  }
658 
659  mainElement.Add(nodes);
660 
661  XElement connections = new XElement("AllConnections");
662  foreach (var editorNode in nodeList)
663  {
664  connections.Add(editorNode.SaveConnections());
665  }
666 
667  mainElement.Add(connections);
668  return mainElement;
669  }
670 
671  private static void Load(XElement saveElement)
672  {
673  nodeList.Clear();
674  projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed").Value);
675  foreach (XElement element in saveElement.Elements())
676  {
677  switch (element.Name.ToString().ToLowerInvariant())
678  {
679  case "nodes":
680  {
681  foreach (var subElement in element.Elements())
682  {
683  EditorNode? node = EditorNode.Load(subElement);
684  if (node != null)
685  {
686  nodeList.Add(node);
687  }
688  }
689 
690  break;
691  }
692  case "allconnections":
693  {
694  foreach (var subElement in element.Elements())
695  {
696  int id = subElement.GetAttributeInt("i", -1);
697  EditorNode? node = nodeList.Find(editorNode => editorNode.ID == id);
698  node?.LoadConnections(subElement);
699  }
700 
701  break;
702  }
703  }
704  }
705  }
706 
707  private static void CreateContextMenu(EditorNode node, EventEditorNodeConnection? connection = null)
708  {
709  if (GUIContextMenu.CurrentContextMenu != null) { return; }
710 
711  GUIContextMenu.CreateContextMenu(
712  new ContextMenuOption("EventEditor.Edit", isEnabled: node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option, onSelected: delegate
713  {
714  CreateEditMenu(node as ValueNode, connection);
715  }),
716  new ContextMenuOption("EventEditor.MarkEnding", isEnabled: connection != null && connection.Type == NodeConnectionType.Option, onSelected: delegate
717  {
718  if (connection == null) { return; }
719 
720  connection.EndConversation = !connection.EndConversation;
721  }),
722  new ContextMenuOption("EventEditor.RemoveConnection", isEnabled: connection != null, onSelected: delegate
723  {
724  if (connection == null) { return; }
725 
726  connection.ClearConnections();
727  connection.OverrideValue = null;
728  connection.OptionText = connection.OptionText;
729  }),
730  new ContextMenuOption("EventEditor.AddOption", isEnabled: node.CanAddConnections, onSelected: node.AddOption),
731  new ContextMenuOption("EventEditor.RemoveOption", isEnabled: connection != null && node.RemovableTypes.Contains(connection.Type), onSelected: delegate
732  {
733  connection?.Parent.RemoveOption(connection);
734  }),
735  new ContextMenuOption("EventEditor.Delete", isEnabled: true, onSelected: delegate
736  {
737  nodeList.Remove(node);
738  node.ClearConnections();
739  }));
740  }
741 
742  private bool CreateTestSetupMenu()
743  {
744  var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.TestPromptHeader"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") },
745  relativeSize: new Vector2(0.2f, 0.3f), minSize: new Point(300, 175));
746 
747  var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), msgBox.Content.RectTransform));
748 
749  new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUIStyle.SubHeadingFont);
750  GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.OutpostParams.Count());
751  foreach (OutpostGenerationParams param in OutpostGenerationParams.OutpostParams)
752  {
753  paramInput.AddItem(param.Identifier.Value!, param);
754  }
755  paramInput.OnSelected = (_, param) =>
756  {
757  lastTestParam = param as OutpostGenerationParams;
758  return true;
759  };
760  paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.OutpostParams.FirstOrDefault());
761 
762  new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUIStyle.SubHeadingFont);
763  GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.Prefabs.Count());
764  foreach (LocationType type in LocationType.Prefabs)
765  {
766  typeInput.AddItem(type.Identifier.Value!, type);
767  }
768  typeInput.OnSelected = (_, type) =>
769  {
770  lastTestType = type as LocationType;
771  return true;
772  };
773  typeInput.SelectItem(lastTestType ?? LocationType.Prefabs.FirstOrDefault());
774 
775  // Cancel button
776  msgBox.Buttons[0].OnClicked = (button, o) =>
777  {
778  msgBox.Close();
779  return true;
780  };
781 
782  // Ok button
783  msgBox.Buttons[1].OnClicked = (button, o) =>
784  {
785  TestEvent(lastTestParam, lastTestType);
786  msgBox.Close();
787  return true;
788  };
789 
790  return true;
791  }
792 
793  private static void CreateEditMenu(ValueNode? node, EventEditorNodeConnection? connection = null)
794  {
795  object? newValue;
796  Type? type;
797  if (node != null)
798  {
799  newValue = node.Value;
800  type = node.Type;
801  }
802  else if (connection != null)
803  {
804  newValue = connection.OverrideValue;
805  type = connection.ValueType;
806  }
807  else
808  {
809  return;
810  }
811 
812  if (connection?.Type == NodeConnectionType.Option)
813  {
814  newValue = connection.OptionText;
815  type = typeof(string);
816  }
817 
818  if (type == null) { return; }
819 
820  Vector2 size = type == typeof(string) ? new Vector2(0.2f, 0.3f) : new Vector2(0.2f, 0.175f);
821  var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.Edit"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, size, minSize: new Point(300, 175));
822 
823  Vector2 layoutSize = type == typeof(string) ? new Vector2(1f, 0.5f) : new Vector2(1f, 0.25f);
824  var layout = new GUILayoutGroup(new RectTransform(layoutSize, msgBox.Content.RectTransform), isHorizontal: true);
825 
826  if (type.IsEnum)
827  {
828  Array enums = Enum.GetValues(type);
829  GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? "", enums.Length);
830  foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString() ?? "", @enum); }
831 
832  valueInput.OnSelected += (component, o) =>
833  {
834  newValue = o;
835  return true;
836  };
837  }
838  else
839  {
840  if (type == typeof(string))
841  {
842  GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, layout.RectTransform)) { CanBeFocused = false };
843  GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, listBox.Content.RectTransform, Anchor.TopRight), wrap: true, style: "GUITextBoxNoBorder");
844  valueInput.OnTextChanged += (component, o) =>
845  {
846  Vector2 textSize = valueInput.Font.MeasureString(valueInput.WrappedText);
847  valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int)textSize.Y + 10);
848  listBox.UpdateScrollBarSize();
849  listBox.BarScroll = 1.0f;
850  newValue = o;
851  return true;
852  };
853  valueInput.Text = newValue?.ToString() ?? "<type here>";
854  }
855  else if (type == typeof(Identifier))
856  {
857  GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? string.Empty);
858  valueInput.OnTextChanged += (component, o) =>
859  {
860  newValue = new Identifier(o);
861  return true;
862  };
863  }
864  else if (type == typeof(float))
865  {
866  GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float);
867  if (newValue is float floatVal)
868  {
869  valueInput.FloatValue = floatVal;
870  }
871  valueInput.OnValueChanged += component => { newValue = component.FloatValue; };
872  }
873  else if (type == typeof(int))
874  {
875  GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Int);
876  if (newValue is int intVal)
877  {
878  valueInput.IntValue = intVal;
879  }
880  valueInput.OnValueChanged += component => { newValue = component.IntValue; };
881  }
882  else if (type == typeof(bool))
883  {
884  GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value");
885  if (newValue is bool val)
886  {
887  valueInput.Selected = val;
888  }
889  valueInput.OnSelected += component =>
890  {
891  newValue = component.Selected;
892  return true;
893  };
894  }
895  }
896 
897  // Cancel button
898  msgBox.Buttons[0].OnClicked = (button, o) =>
899  {
900  msgBox.Close();
901  return true;
902  };
903 
904  // Ok button
905  msgBox.Buttons[1].OnClicked = (button, o) =>
906  {
907  if (node != null)
908  {
909  node.Value = newValue;
910  }
911  else if (connection != null)
912  {
913  if (connection.Type == NodeConnectionType.Option)
914  {
915  connection.OptionText = newValue?.ToString();
916  }
917  else
918  {
919  connection.ClearConnections();
920  connection.OverrideValue = newValue;
921  }
922  }
923 
924  msgBox.Close();
925  return true;
926  };
927  }
928 
929  private bool TestEvent(OutpostGenerationParams? param, LocationType? type)
930  {
931  SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)) ?? throw new NullReferenceException("Could not test event: There are no shuttles available.");
932 
933  XElement? eventXml = ExportXML();
934  EventPrefab? prefab;
935  if (eventXml != null)
936  {
937  prefab = EventPrefab.Create(eventXml.FromPackage(null), file: null);
938  }
939  else
940  {
941  GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUIStyle.Red);
942  return false;
943  }
944 
945  GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null);
946  TestGameMode gameMode = ((TestGameMode?)gameSession.GameMode) ?? throw new InvalidCastException();
947 
948  gameMode.SpawnOutpost = true;
949  gameMode.OutpostParams = param;
950  gameMode.OutpostType = type;
951  gameMode.TriggeredEvent = prefab;
952  gameMode.OnRoundEnd = () =>
953  {
954  Submarine.Unload();
955  GameMain.EventEditorScreen.Select();
956  };
957 
958  GameMain.GameScreen.Select();
959  gameSession.StartRound(null, false);
960  return true;
961  }
962 
963  public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch)
964  {
965  DrawnTooltip = string.Empty;
966  Cam.UpdateTransform();
967 
968  // "world" space
969  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: Cam.Transform);
970  graphics.Clear(new Color(0.2f, 0.2f, 0.2f, 1.0f));
971 
972  foreach (EditorNode node in nodeList.Where(node => node is SpecialNode))
973  {
974  node.Draw(spriteBatch);
975  }
976 
977  // Render value nodes below event nodes
978  foreach (EditorNode node in nodeList.Where(node => node is ValueNode))
979  {
980  node.Draw(spriteBatch);
981  }
982 
983  foreach (EditorNode node in nodeList.Where(node => node is EventNode))
984  {
985  node.Draw(spriteBatch);
986  }
987 
988  draggedNode?.Draw(spriteBatch);
989  foreach (var (node, _) in markedNodes)
990  {
991  node.Draw(spriteBatch);
992  }
993 
994  spriteBatch.End();
995 
996  // GUI
997  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState);
998  GUI.Draw(Cam, spriteBatch);
999 
1000  if (!string.IsNullOrWhiteSpace(DrawnTooltip) && GUIStyle.SmallFont.Value != null)
1001  {
1002  string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUIStyle.SmallFont.Value);
1003  GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUIStyle.SmallFont);
1004  }
1005 
1006  spriteBatch.End();
1007  }
1008 
1009  public override void Update(double deltaTime)
1010  {
1011  if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y)
1012  {
1013  CreateGUI();
1014  }
1015 
1016  Cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null);
1017  Vector2 mousePos = Cam.ScreenToWorld(PlayerInput.MousePosition);
1018  mousePos.Y = -mousePos.Y;
1019 
1020  foreach (EditorNode node in nodeList)
1021  {
1022  if (PlayerInput.PrimaryMouseButtonDown())
1023  {
1024  EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos);
1025  if (connection != null && connection.Type.NodeSide == NodeConnectionType.Side.Right)
1026  {
1027  if (connection.Type != NodeConnectionType.Out)
1028  {
1029  if (connection.ConnectedTo.Any()) { return; }
1030  }
1031 
1032  DraggedConnection = connection;
1033  }
1034  }
1035 
1036  // ReSharper disable once AssignmentInConditionalExpression
1037  if (node.IsHighlighted = node.HeaderRectangle.Contains(mousePos))
1038  {
1039  if (PlayerInput.PrimaryMouseButtonDown())
1040  {
1041  // Ctrl + clicking the headers add them to the "selection" that allows us to drag multiple nodes at once
1042  if (PlayerInput.IsCtrlDown())
1043  {
1044  if (selectedNodes.Contains(node))
1045  {
1046  selectedNodes.Remove(node);
1047  }
1048  else
1049  {
1050  selectedNodes.Add(node);
1051  }
1052 
1053  node.IsSelected = selectedNodes.Contains(node);
1054  break;
1055  }
1056 
1057  draggedNode = node;
1058  dragOffset = draggedNode.Position - mousePos;
1059  foreach (EditorNode selectedNode in selectedNodes)
1060  {
1061  if (!markedNodes.ContainsKey(selectedNode))
1062  {
1063  markedNodes.Add(selectedNode, selectedNode.Position - mousePos);
1064  }
1065  }
1066  }
1067  }
1068 
1069  if (PlayerInput.SecondaryMouseButtonClicked())
1070  {
1071  EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos);
1072  if (node.GetDrawRectangle().Contains(mousePos) || connection != null)
1073  {
1074  CreateContextMenu(node, node.GetConnectionOnMouse(mousePos));
1075  break;
1076  }
1077  }
1078  }
1079 
1080  if (PlayerInput.SecondaryMouseButtonClicked())
1081  {
1082  foreach (var selectedNode in selectedNodes)
1083  {
1084  selectedNode.IsSelected = false;
1085  }
1086 
1087  selectedNodes.Clear();
1088  }
1089 
1090  if (draggedNode != null)
1091  {
1092  if (!PlayerInput.PrimaryMouseButtonHeld())
1093  {
1094  draggedNode = null;
1095  markedNodes.Clear();
1096  }
1097  else
1098  {
1099  Vector2 offsetChange = Vector2.Zero;
1100  draggedNode.IsHighlighted = true;
1101  draggedNode.Position = mousePos + dragOffset;
1102 
1103  if (PlayerInput.KeyHit(Keys.Up)) { offsetChange.Y--; }
1104 
1105  if (PlayerInput.KeyHit(Keys.Down)) { offsetChange.Y++; }
1106 
1107  if (PlayerInput.KeyHit(Keys.Left)) { offsetChange.X--; }
1108 
1109  if (PlayerInput.KeyHit(Keys.Right)) { offsetChange.X++; }
1110 
1111  dragOffset += offsetChange;
1112 
1113  foreach (var (editorNode, offset) in markedNodes.Where(pair => pair.Key != draggedNode))
1114  {
1115  editorNode.Position = mousePos + offset;
1116  }
1117 
1118  if (offsetChange != Vector2.Zero)
1119  {
1120  foreach (var (key, value) in markedNodes.ToList())
1121  {
1122  markedNodes[key] = value + offsetChange;
1123  }
1124  }
1125  }
1126  }
1127 
1128  if (DraggedConnection != null)
1129  {
1130  if (!PlayerInput.PrimaryMouseButtonHeld())
1131  {
1132  foreach (EditorNode node in nodeList)
1133  {
1134  var nodeOnMouse = node.GetConnectionOnMouse(mousePos);
1135  if (nodeOnMouse != null && nodeOnMouse != DraggedConnection && nodeOnMouse.Type.NodeSide == NodeConnectionType.Side.Left)
1136  {
1137  if (!DraggedConnection.CanConnect(nodeOnMouse)) { continue; }
1138 
1139  nodeOnMouse.ClearConnections();
1140  EditorNode.Connect(DraggedConnection, nodeOnMouse);
1141  break;
1142  }
1143  }
1144 
1145  DraggedConnection = null;
1146  }
1147  else
1148  {
1149  DraggingPosition = mousePos;
1150  }
1151  }
1152  else
1153  {
1154  DraggingPosition = Vector2.Zero;
1155  }
1156 
1157  if (PlayerInput.MidButtonHeld())
1158  {
1159  Vector2 moveSpeed = PlayerInput.MouseSpeed * (float) deltaTime * 60.0f / Cam.Zoom;
1160  moveSpeed.X = -moveSpeed.X;
1161  Cam.Position += moveSpeed;
1162  }
1163 
1164  base.Update(deltaTime);
1165  }
1166  }
1167 }
NumberType
Definition: Enums.cs:741
GUISoundType
Definition: GUI.cs:21