#nullable enable using Barotrauma.IO; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Xml.Linq; using Directory = System.IO.Directory; namespace Barotrauma { internal class EventEditorScreen : EditorScreen { private GUIFrame GuiFrame = null!; public override Camera Cam { get; } public static string? DrawnTooltip { get; set; } public static bool ConversationMode { get; set; } public static readonly List nodeList = new List(); private readonly List selectedNodes = new List(); public static Vector2 DraggingPosition = Vector2.Zero; public static EventEditorNodeConnection? DraggedConnection; private EditorNode? draggedNode; private Vector2 dragOffset; private readonly Dictionary markedNodes = new Dictionary(); private static string projectName = string.Empty; private OutpostGenerationParams? lastTestParam; private LocationType? lastTestType; private GUITickBox? isTraitorEventBox; private GUITickBox? conversationModeBox; private readonly LanguageIdentifier originalLanguage; private GUIDropDown? languageDropdown; private static int CreateID() { int maxId = nodeList.Any() ? nodeList.Max(node => node.ID) : 0; return ++maxId; } private Point screenResolution; public EventEditorScreen() { Cam = new Camera(); nodeList.Clear(); originalLanguage = GameSettings.CurrentConfig.Language; CreateGUI(); } public override void Select() { GUI.PreventPauseMenuToggle = false; projectName = TextManager.Get("EventEditor.Unnamed").Value; UpdateLanguageDropdownSelection(); base.Select(); } private void UpdateLanguageDropdownSelection() { if (languageDropdown == null) { return; } languageDropdown.SelectItem(GameSettings.CurrentConfig.Language); } protected override void DeselectEditorSpecific() { // Restore the original language when leaving the editor var config = GameSettings.CurrentConfig; config.Language = originalLanguage; GameSettings.SetCurrentConfig(config); TextManager.LanguageChanged(); } private static readonly HashSet hiddenNodesInConversationMode = new HashSet(); private static bool ShouldHideNodeInConversationMode(EditorNode node) { return hiddenNodesInConversationMode.Contains(node); } private static void UpdateHiddenNodesInConversationMode() { hiddenNodesInConversationMode.Clear(); // Find all text display nodes (ConversationAction and EventLogAction) and mark their inner Text nodes (and descendants) as hidden foreach (var textDisplayNode in nodeList.Where(IsEventTextDisplayNode)) { var addConnection = textDisplayNode.Connections.FirstOrDefault(c => c.Type == NodeConnectionType.Add); if (addConnection != null && addConnection.ConnectedTo.Any()) { foreach (var connectedNode in addConnection.ConnectedTo) { if (connectedNode.Parent?.Name == "Text") { MarkNodeAndDescendantsAsHidden(connectedNode.Parent); } } } } } private static bool IsEventTextDisplayNode(EditorNode node) => node is EventTextDisplayNode; private static void MarkNodeAndDescendantsAsHidden(EditorNode node) { hiddenNodesInConversationMode.Add(node); // Recursively mark all connected child nodes as hidden foreach (var connection in node.Connections) { foreach (var connectedNode in connection.ConnectedTo) { if (connectedNode.Parent != null && !hiddenNodesInConversationMode.Contains(connectedNode.Parent)) { MarkNodeAndDescendantsAsHidden(connectedNode.Parent); } } } } private void CreateGUI() { GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.5f), GUI.Canvas) { MinSize = new Point(300, 520) }); GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true, AbsoluteSpacing = GUI.IntScale(5) }; // === BUTTONS === // GUILayoutGroup buttonLayout = new GUILayoutGroup(RectTransform(1.0f, 0.40f, layoutGroup)) { RelativeSpacing = 0.04f }; GUIButton newProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.NewProject")); GUIButton saveProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.SaveProject")); GUIButton loadProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.LoadProject")); GUIButton exportProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.Export")); // === LOAD PREFAB === // GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup)); new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUIStyle.SubHeadingFont); GUILayoutGroup loadDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, loadEventLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown loadDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, loadDropdownLayout), elementCount: 10); GUIButton loadButton = new GUIButton(RectTransform(0.2f, 1.0f, loadDropdownLayout), TextManager.Get("Load")); // === ADD ACTION === // GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup)); new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addActionDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addActionLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addActionDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addActionDropdownLayout), elementCount: 10); GUIButton addActionButton = new GUIButton(RectTransform(0.2f, 1.0f, addActionDropdownLayout), TextManager.Get("EventEditor.Add")); // === ADD VALUE === // GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup)); new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addValueDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addValueLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addValueDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addValueDropdownLayout), elementCount: 7); GUIButton addValueButton = new GUIButton(RectTransform(0.2f, 1.0f, addValueDropdownLayout), TextManager.Get("EventEditor.Add")); // === ADD SPECIAL === // GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup)); new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUIStyle.SubHeadingFont); GUILayoutGroup addSpecialDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addSpecialLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft); GUIDropDown addSpecialDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addSpecialDropdownLayout), elementCount: 1); GUIButton addSpecialButton = new GUIButton(RectTransform(0.2f, 1.0f, addSpecialDropdownLayout), TextManager.Get("EventEditor.Add")); // Add event prefabs with identifiers to the list foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs().Where(p => !p.Identifier.IsEmpty).Distinct().OrderBy(p => p.Identifier)) { if (!typeof(ScriptedEvent).IsAssignableFrom(eventPrefab.EventType)) { continue; } var textBlock = loadDropdown.AddItem(eventPrefab.Identifier.Value!, eventPrefab) as GUITextBlock; if (eventPrefab is TraitorEventPrefab && textBlock != null) { textBlock.TextColor = Color.MediumPurple; } } // Add all types that inherit the EventAction class foreach (Type type in Assembly.GetExecutingAssembly().GetTypes().Where(type => type.IsSubclassOf(typeof(EventAction))).OrderBy(t => t.Name)) { addActionDropdown.AddItem(type.Name, type); } addSpecialDropdown.AddItem("Custom", typeof(CustomNode)); addValueDropdown.AddItem(nameof(Single), typeof(float)); addValueDropdown.AddItem(nameof(Boolean), typeof(bool)); addValueDropdown.AddItem(nameof(String), typeof(string)); addValueDropdown.AddItem(nameof(SpawnType), typeof(SpawnType)); addValueDropdown.AddItem(nameof(LimbType), typeof(LimbType)); addValueDropdown.AddItem(nameof(ReputationAction.ReputationType), typeof(ReputationAction.ReputationType)); addValueDropdown.AddItem(nameof(SpawnAction.SpawnLocationType), typeof(SpawnAction.SpawnLocationType)); addValueDropdown.AddItem(nameof(CharacterTeamType), typeof(CharacterTeamType)); loadButton.OnClicked += (button, o) => Load(loadDropdown.SelectedData as EventPrefab); addActionButton.OnClicked += (button, o) => AddAction(addActionDropdown.SelectedData as Type); addValueButton.OnClicked += (button, o) => AddValue(addValueDropdown.SelectedData as Type); addSpecialButton.OnClicked += (button, o) => AddSpecial(addSpecialDropdown.SelectedData as Type); exportProjectButton.OnClicked += ExportEventToFile; saveProjectButton.OnClicked += SaveProjectToFile; newProjectButton.OnClicked += TryCreateNewProject; loadProjectButton.OnClicked += (button, o) => { FileSelection.OnFileSelected = (file) => { XDocument? document = XMLExtensions.TryLoadXml(file); if (document?.Root != null) { Load(document.Root); } }; string directory = Path.GetFullPath("EventProjects"); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("Scripted Event", "*.sevproj"); FileSelection.SelectFileTypeFilter("*.sevproj"); FileSelection.CurrentDirectory = directory; FileSelection.Open = true; return true; }; isTraitorEventBox = new GUITickBox(RectTransform(1.0f, 0.10f, layoutGroup), "Traitor event"); // === CONVERSATION MODE CHECKBOX === // conversationModeBox = new GUITickBox(RectTransform(1.0f, 0.10f, layoutGroup), "Conversation Mode"); conversationModeBox.Selected = ConversationMode; conversationModeBox.OnSelected = box => { ConversationMode = !ConversationMode; UpdateHiddenNodesInConversationMode(); return true; }; // === LANGUAGE SELECTION === // GUILayoutGroup languageLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup)); new GUITextBlock(RectTransform(1.0f, 0.5f, languageLayout), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont); var languages = TextManager.AvailableLanguages .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()); languageDropdown = new GUIDropDown(RectTransform(1.0f, 0.5f, languageLayout), elementCount: 10); foreach (var language in languages) { languageDropdown.AddItem(TextManager.GetTranslatedLanguageName(language), language); } // Select current language languageDropdown.SelectItem(GameSettings.CurrentConfig.Language); languageDropdown.OnSelected = (component, userData) => { if (userData is LanguageIdentifier selectedLanguage) { var config = GameSettings.CurrentConfig; config.Language = selectedLanguage; GameSettings.SetCurrentConfig(config); TextManager.LanguageChanged(); } return true; }; screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } private bool ExportEventToFile(GUIButton button, object o) { XElement? save = ExportXML(); if (save != null) { try { string directory = Path.GetFullPath("EventProjects"); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } string exportPath = Path.Combine(directory, "Exported"); if (!Directory.Exists(exportPath)) { Directory.CreateDirectory(exportPath); } 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)); var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName }; // Cancel button msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); return true; }; // Ok button msgBox.Buttons[1].OnClicked = delegate { foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (!nameInput.Text.Contains(illegalChar)) { continue; } GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } msgBox.Close(); string path = Path.Combine(exportPath, $"{nameInput.Text}.xml"); File.WriteAllText(path, save.ToString(), catchUnauthorizedAccessExceptions: false); AskForConfirmation(TextManager.Get("EventEditor.OpenTextHeader"), TextManager.Get("EventEditor.OpenTextBody"), () => { ToolBox.OpenFileWithShell(path); return true; }); GUI.AddMessage($"XML exported to {path}", GUIStyle.Green); return true; }; } catch (Exception e) { DebugConsole.ThrowError("Failed to export event", e); } } else { GUI.AddMessage("Unable to export because the project contains errors", GUIStyle.Red); } return true; } private bool TryCreateNewProject(GUIButton button, object o) { AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () => { nodeList.Clear(); markedNodes.Clear(); selectedNodes.Clear(); projectName = TextManager.Get("EventEditor.Unnamed").Value; return true; }); return true; } public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Func onConfirm, GUISoundType? overrideConfirmButtonSound = null) { LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") }; GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons); // Cancel button msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); return true; }; // Ok button msgBox.Buttons[0].OnClicked = delegate { onConfirm.Invoke(); msgBox.Close(); return true; }; if (overrideConfirmButtonSound.HasValue) { msgBox.Buttons[0].ClickSound = overrideConfirmButtonSound.Value; } return msgBox; } private bool SaveProjectToFile(GUIButton button, object o) { string directory = Path.GetFullPath("EventProjects"); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } 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)); var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.25f), msgBox.Content.RectTransform), isHorizontal: true); GUITextBox nameInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform)) { Text = projectName }; // Cancel button msgBox.Buttons[0].OnClicked = delegate { msgBox.Close(); return true; }; // Ok button msgBox.Buttons[1].OnClicked = delegate { foreach (var illegalChar in Path.GetInvalidFileNameCharsCrossPlatform()) { if (!nameInput.Text.Contains(illegalChar)) { continue; } GUI.AddMessage(TextManager.GetWithVariable("SubNameIllegalCharsWarning", "[illegalchar]", illegalChar.ToString()), GUIStyle.Red); return false; } msgBox.Close(); projectName = nameInput.Text; XElement save = SaveEvent(projectName); string filePath = System.IO.Path.Combine(directory, $"{projectName}.sevproj"); File.WriteAllText(Path.Combine(directory, $"{projectName}.sevproj"), save.ToString()); GUI.AddMessage($"Project saved to {filePath}", GUIStyle.Green); AskForConfirmation(TextManager.Get("EventEditor.TestPromptHeader"), TextManager.Get("EventEditor.TestPromptBody"), CreateTestSetupMenu); return true; }; return true; } private bool Load(EventPrefab? prefab) { if (prefab == null) { return false; } AskForConfirmation(TextManager.Get("EventEditor.NewProject"), TextManager.Get("EventEditor.NewProjectPrompt"), () => { nodeList.Clear(); selectedNodes.Clear(); markedNodes.Clear(); if (isTraitorEventBox != null) { isTraitorEventBox.Selected = prefab is TraitorEventPrefab; } bool hadNodes = true; CreateNodes(prefab.ConfigElement, ref hadNodes); if (!hadNodes) { GUI.NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody")); } // Update hidden nodes after loading UpdateHiddenNodesInConversationMode(); return true; }); return true; } private bool AddAction(Type? type) { if (type == null) { return false; } Vector2 spawnPos = Cam.WorldViewCenter; spawnPos.Y = -spawnPos.Y; // Create the appropriate node type based on the action type EditorNode newNode; if (EditorNode.IsInstanceOf(type, typeof(ConversationAction))) { newNode = new EventConversationNode(type, type.Name) { ID = CreateID() }; } else if (EditorNode.IsInstanceOf(type, typeof(EventLogAction))) { newNode = new EventLogNode(type, type.Name) { ID = CreateID() }; } else { newNode = new EventNode(type, type.Name) { ID = CreateID() }; } newNode.Position = spawnPos - newNode.Size / 2; nodeList.Add(newNode); return true; } private bool AddValue(Type? type) { if (type == null) { return false; } Vector2 spawnPos = Cam.WorldViewCenter; spawnPos.Y = -spawnPos.Y; ValueNode newValue = new ValueNode(type, type.Name) { ID = CreateID() }; newValue.Position = spawnPos - newValue.Size / 2; nodeList.Add(newValue); return true; } private bool AddSpecial(Type? type) { if (type == null) { return false; } Vector2 spawnPos = Cam.WorldViewCenter; spawnPos.Y = -spawnPos.Y; ConstructorInfo? constructor = type.GetConstructor(Array.Empty()); SpecialNode? newNode = null; if (constructor != null) { newNode = constructor.Invoke(Array.Empty()) as SpecialNode; } if (newNode != null) { newNode.ID = CreateID(); newNode.Position = spawnPos - newNode.Size / 2; nodeList.Add(newNode); return true; } return false; } private void CreateNodes(ContentXElement element, ref bool hadNodes, EditorNode? parent = null, int ident = 0) { EditorNode? lastNode = null; foreach (var subElement in element.Elements()) { bool skip = true; switch (subElement.Name.ToString().ToLowerInvariant()) { case "failure": case "success": case "option": CreateNodes(subElement, ref hadNodes, parent, ident); break; default: skip = false; break; } if (!skip) { Vector2 defaultNodePos = new Vector2(-16000, -16000); EditorNode newNode; Type? t = Type.GetType($"Barotrauma.{subElement.Name}"); if (t != null && EditorNode.IsInstanceOf(t, typeof(EventAction))) { // Create the appropriate node type based on the action type if (EditorNode.IsInstanceOf(t, typeof(ConversationAction))) { newNode = new EventConversationNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; } else if (EditorNode.IsInstanceOf(t, typeof(EventLogAction))) { newNode = new EventLogNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; } else { newNode = new EventNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; } } else { newNode = new CustomNode(subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() }; foreach (XAttribute attribute in subElement.Attributes().Where(attribute => !attribute.ToString().StartsWith("_"))) { newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, attribute.Name.ToString(), typeof(string))); } } Vector2 npos = subElement.GetAttributeVector2("_npos", defaultNodePos); if (npos != defaultNodePos) { newNode.Position = npos; } else { hadNodes = false; } var parentElement = subElement.Parent; foreach (var xElement in subElement.Elements()) { switch (xElement.Name.ToString().ToLowerInvariant()) { case "option": EventEditorNodeConnection optionConnection = new EventEditorNodeConnection(newNode, NodeConnectionType.Option) { OptionText = xElement.GetAttributeString("text", string.Empty), EndConversation = xElement.GetAttributeBool("endconversation", false) }; newNode.Connections.Add(optionConnection); break; } } foreach (EventEditorNodeConnection connection in newNode.Connections) { if (connection.Type == NodeConnectionType.Value) { foreach (XAttribute attribute in subElement.Attributes()) { if (string.Equals(connection.Attribute, attribute.Name.ToString(), StringComparison.InvariantCultureIgnoreCase) && connection.ValueType != null) { if (connection.ValueType.IsEnum) { Array values = Enum.GetValues(connection.ValueType); foreach (object? @enum in values) { if (string.Equals(@enum?.ToString(), attribute.Value, StringComparison.InvariantCultureIgnoreCase)) { connection.OverrideValue = @enum; } } } else { try { connection.OverrideValue = ChangeType(attribute.Value, connection.ValueType); } catch { DebugConsole.ThrowError($"Failed to convert the value {attribute.Value} of the attribute {attribute.Name} to {connection.ValueType}."); } } } } } } if (npos == defaultNodePos) { hadNodes = false; bool Predicate(EditorNode node) => Rectangle.Union(node.GetDrawRectangle(), node.HeaderRectangle).Intersects(Rectangle.Union(newNode.GetDrawRectangle(), newNode.HeaderRectangle)); while (nodeList.Any(Predicate)) { EditorNode? otherNode = nodeList.Find(Predicate); if (otherNode != null) { newNode.Position += new Vector2(128, otherNode.GetDrawRectangle().Height + otherNode.HeaderRectangle.Height + new Random().Next(128, 256)); } } } if (subElement.Name.ToString().ToLowerInvariant() is "text" or "conditional") { parent?.AddConnection(NodeConnectionType.Add); parent?.Connect(newNode, NodeConnectionType.Add); } else { if (parentElement?.FirstElement() == subElement) { switch (parentElement?.Name.ToString().ToLowerInvariant()) { case "failure": parent?.Connect(newNode, NodeConnectionType.Failure); break; case "success": parent?.Connect(newNode, NodeConnectionType.Success); break; case "onroundendaction": parent?.Connect(newNode, NodeConnectionType.Next); break; case "option": if (parent != null) { EventEditorNodeConnection? activateConnection = newNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); EventEditorNodeConnection? optionConnection = parent.Connections.FirstOrDefault(connection => connection.Type == NodeConnectionType.Option && string.Equals(connection.OptionText, parentElement.GetAttributeString("text", string.Empty), StringComparison.Ordinal)); if (activateConnection != null) { optionConnection?.ConnectedTo.Add(activateConnection); } } break; default: parent?.Connect(newNode, NodeConnectionType.Add); break; } } else { lastNode?.Connect(newNode, NodeConnectionType.Next); } } lastNode = newNode; nodeList.Add(newNode); ident += 600; CreateNodes(subElement, ref hadNodes, newNode, ident); } else { } } } private static RectTransform RectTransform(float x, float y, GUIComponent parent, Anchor anchor = Anchor.TopRight) { return new RectTransform(new Vector2(x, y), parent.RectTransform, anchor); } public override void AddToGUIUpdateList() { GuiFrame.AddToGUIUpdateList(); } public static object? ChangeType(string value, Type type) { if (type == typeof(Identifier)) { return value.ToIdentifier(); } else { return Convert.ChangeType(value, type); } } private XElement? ExportXML() { XElement mainElement = new XElement( isTraitorEventBox is { Selected: true } ? nameof(TraitorEvent) : nameof(ScriptedEvent), new XAttribute("identifier", projectName.RemoveWhitespace().ToLowerInvariant())); EditorNode? startNode = null; foreach (EditorNode eventNode in nodeList.Where(node => node is EventNode || node is SpecialNode)) { if (eventNode.GetParent() == null) { if (startNode != null) { DebugConsole.ThrowError("You have more than one start node, only one will be picked while the others will get ignored."); } startNode ??= eventNode; } } if (startNode == null) { return null; } ExportChildNodes(startNode, mainElement); return mainElement; } private void ExportChildNodes(EditorNode startNode, XElement parent) { XElement? newElement = startNode.ToXML(); if (newElement == null) { return; } parent.Add(newElement); EditorNode? success = startNode.GetNext(NodeConnectionType.Success); EditorNode? failure = startNode.GetNext(NodeConnectionType.Failure); EditorNode? add = startNode.GetNext(NodeConnectionType.Add); Tuple[] options = startNode is EventNode eNode ? eNode.GetOptions() : new Tuple[0]; if (success != null) { XElement successElement = new XElement("Success"); ExportChildNodes(success, successElement); newElement.Add(successElement); } if (failure != null) { XElement failureElement = new XElement("Failure"); ExportChildNodes(failure, failureElement); newElement.Add(failureElement); } if (add is CustomNode custom) { ExportChildNodes(custom, newElement); } foreach (var (node, text, end) in options) { XElement optionElement = new XElement("Option"); optionElement.Add(new XAttribute("text", text ?? "")); if (end) { optionElement.Add(new XAttribute("endconversation", true)); } if (node is EventNode eventNode) { ExportChildNodes(eventNode, optionElement); } newElement.Add(optionElement); } EditorNode? next = startNode.GetNext(); if (next != null) { ExportChildNodes(next, parent); } } private static XElement SaveEvent(string name) { XElement mainElement = new XElement("SavedEvent", new XAttribute("name", name)); XElement nodes = new XElement("Nodes"); foreach (var editorNode in nodeList) { nodes.Add(editorNode.Save()); } mainElement.Add(nodes); XElement connections = new XElement("AllConnections"); foreach (var editorNode in nodeList) { connections.Add(editorNode.SaveConnections()); } mainElement.Add(connections); return mainElement; } private static void Load(XElement saveElement) { nodeList.Clear(); hiddenNodesInConversationMode.Clear(); projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed").Value); foreach (XElement element in saveElement.Elements()) { switch (element.Name.ToString().ToLowerInvariant()) { case "nodes": { foreach (var subElement in element.Elements()) { EditorNode? node = EditorNode.Load(subElement); if (node != null) { nodeList.Add(node); } } break; } case "allconnections": { foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); EditorNode? node = nodeList.Find(editorNode => editorNode.ID == id); node?.LoadConnections(subElement); } break; } } } // Update hidden nodes after loading UpdateHiddenNodesInConversationMode(); } private static void CreateContextMenu(EditorNode node, EventEditorNodeConnection? connection = null) { if (GUIContextMenu.CurrentContextMenu != null) { return; } GUIContextMenu.CreateContextMenu( new ContextMenuOption("EventEditor.Edit", isEnabled: node is ValueNode || connection?.Type == NodeConnectionType.Value || connection?.Type == NodeConnectionType.Option, onSelected: delegate { CreateEditMenu(node as ValueNode, connection); }), new ContextMenuOption("EventEditor.MarkEnding", isEnabled: connection != null && connection.Type == NodeConnectionType.Option, onSelected: delegate { if (connection == null) { return; } connection.EndConversation = !connection.EndConversation; }), new ContextMenuOption("EventEditor.RemoveConnection", isEnabled: connection != null, onSelected: delegate { if (connection == null) { return; } connection.ClearConnections(); connection.OverrideValue = null; connection.OptionText = connection.OptionText; }), new ContextMenuOption("EventEditor.AddOption", isEnabled: node.CanAddConnections, onSelected: node.AddOption), new ContextMenuOption("EventEditor.RemoveOption", isEnabled: connection != null && node.RemovableTypes.Contains(connection.Type), onSelected: delegate { connection?.Parent.RemoveOption(connection); }), new ContextMenuOption("EventEditor.Delete", isEnabled: true, onSelected: delegate { nodeList.Remove(node); node.ClearConnections(); })); } private bool CreateTestSetupMenu() { var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.TestPromptHeader"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, relativeSize: new Vector2(0.2f, 0.3f), minSize: new Point(300, 175)); var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), msgBox.Content.RectTransform)); new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.OutpostGenParams"), font: GUIStyle.SubHeadingFont); GUIDropDown paramInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, OutpostGenerationParams.OutpostParams.Count()); foreach (OutpostGenerationParams param in OutpostGenerationParams.OutpostParams) { paramInput.AddItem(param.Identifier.Value!, param); } paramInput.OnSelected = (_, param) => { lastTestParam = param as OutpostGenerationParams; return true; }; paramInput.SelectItem(lastTestParam ?? OutpostGenerationParams.OutpostParams.FirstOrDefault()); new GUITextBlock(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), TextManager.Get("EventEditor.LocationType"), font: GUIStyle.SubHeadingFont); GUIDropDown typeInput = new GUIDropDown(new RectTransform(new Vector2(1, 0.25f), layout.RectTransform), string.Empty, LocationType.Prefabs.Count()); foreach (LocationType type in LocationType.Prefabs) { typeInput.AddItem(type.Identifier.Value!, type); } typeInput.OnSelected = (_, type) => { lastTestType = type as LocationType; return true; }; typeInput.SelectItem(lastTestType ?? LocationType.Prefabs.FirstOrDefault()); // Cancel button msgBox.Buttons[0].OnClicked = (button, o) => { msgBox.Close(); return true; }; // Ok button msgBox.Buttons[1].OnClicked = (button, o) => { TestEvent(lastTestParam, lastTestType); msgBox.Close(); return true; }; return true; } private static void CreateEditMenu(ValueNode? node, EventEditorNodeConnection? connection = null) { object? newValue; Type? type; if (node != null) { newValue = node.Value; type = node.Type; } else if (connection != null) { newValue = connection.OverrideValue; type = connection.ValueType; } else { return; } if (connection?.Type == NodeConnectionType.Option) { newValue = connection.OptionText; type = typeof(string); } if (type == null) { return; } Vector2 size = type == typeof(string) ? new Vector2(0.2f, 0.3f) : new Vector2(0.2f, 0.175f); var msgBox = new GUIMessageBox(TextManager.Get("EventEditor.Edit"), "", new[] { TextManager.Get("Cancel"), TextManager.Get("OK") }, size, minSize: new Point(300, 175)); Vector2 layoutSize = type == typeof(string) ? new Vector2(1f, 0.5f) : new Vector2(1f, 0.25f); var layout = new GUILayoutGroup(new RectTransform(layoutSize, msgBox.Content.RectTransform), isHorizontal: true); if (type.IsEnum) { Array enums = Enum.GetValues(type); GUIDropDown valueInput = new GUIDropDown(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? "", enums.Length); foreach (object? @enum in enums) { valueInput.AddItem(@enum?.ToString() ?? "", @enum); } valueInput.OnSelected += (component, o) => { newValue = o; return true; }; } else { if (type == typeof(string)) { GUIListBox listBox = new GUIListBox(new RectTransform(Vector2.One, layout.RectTransform)) { CanBeFocused = false }; GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, listBox.Content.RectTransform, Anchor.TopRight), wrap: true, style: "GUITextBoxNoBorder"); valueInput.OnTextChanged += (component, o) => { Vector2 textSize = valueInput.Font.MeasureString(valueInput.WrappedText); valueInput.RectTransform.NonScaledSize = new Point(valueInput.RectTransform.NonScaledSize.X, (int)textSize.Y + 10); listBox.UpdateScrollBarSize(); listBox.BarScroll = 1.0f; newValue = o; return true; }; valueInput.Text = newValue?.ToString() ?? ""; } else if (type == typeof(Identifier)) { GUITextBox valueInput = new GUITextBox(new RectTransform(Vector2.One, layout.RectTransform), newValue?.ToString() ?? string.Empty); valueInput.OnTextChanged += (component, o) => { newValue = new Identifier(o); return true; }; } else if (type == typeof(float)) { GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Float); if (newValue is float floatVal) { valueInput.FloatValue = floatVal; } valueInput.OnValueChanged += component => { newValue = component.FloatValue; }; } else if (type == typeof(int)) { GUINumberInput valueInput = new GUINumberInput(new RectTransform(Vector2.One, layout.RectTransform), NumberType.Int); if (newValue is int intVal) { valueInput.IntValue = intVal; } valueInput.OnValueChanged += component => { newValue = component.IntValue; }; } else if (type == typeof(bool)) { GUITickBox valueInput = new GUITickBox(new RectTransform(Vector2.One, layout.RectTransform), "Value"); if (newValue is bool val) { valueInput.Selected = val; } valueInput.OnSelected += component => { newValue = component.Selected; return true; }; } } // Cancel button msgBox.Buttons[0].OnClicked = (button, o) => { msgBox.Close(); return true; }; // Ok button msgBox.Buttons[1].OnClicked = (button, o) => { if (node != null) { node.Value = newValue; } else if (connection != null) { if (connection.Type == NodeConnectionType.Option) { connection.OptionText = newValue?.ToString(); } else { connection.ClearConnections(); connection.OverrideValue = newValue; } } msgBox.Close(); return true; }; } private bool TestEvent(OutpostGenerationParams? param, LocationType? type) { SubmarineInfo subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(info => info.HasTag(SubmarineTag.Shuttle)) ?? throw new NullReferenceException("Could not test event: There are no shuttles available."); XElement? eventXml = ExportXML(); EventPrefab? prefab; if (eventXml != null) { prefab = EventPrefab.Create(eventXml.FromPackage(null), file: null); } else { GUI.AddMessage("Unable to open test enviroment because the event contains errors.", GUIStyle.Red); return false; } GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); TestGameMode gameMode = ((TestGameMode?)gameSession.GameMode) ?? throw new InvalidCastException(); gameMode.SpawnOutpost = true; gameMode.OutpostParams = param; gameMode.OutpostType = type; gameMode.TriggeredEvent = prefab; gameMode.OnRoundEnd = () => { Submarine.Unload(); GameMain.EventEditorScreen.Select(); }; GameMain.GameScreen.Select(); gameSession.StartRound(null, false); return true; } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { DrawnTooltip = string.Empty; Cam.UpdateTransform(); // "world" space spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: Cam.Transform); graphics.Clear(new Color(0.2f, 0.2f, 0.2f, 1.0f)); foreach (EditorNode node in nodeList.Where(node => node is SpecialNode)) { if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; } node.Draw(spriteBatch); } // Render value nodes below event nodes foreach (EditorNode node in nodeList.Where(node => node is ValueNode)) { if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; } node.Draw(spriteBatch); } foreach (EditorNode node in nodeList.Where(node => node is EventNode)) { if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; } node.Draw(spriteBatch); } draggedNode?.Draw(spriteBatch); foreach (var (node, _) in markedNodes) { node.Draw(spriteBatch); } spriteBatch.End(); // GUI spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); GUI.Draw(Cam, spriteBatch); if (!string.IsNullOrWhiteSpace(DrawnTooltip) && GUIStyle.SmallFont.Value != null) { string tooltip = ToolBox.WrapText(DrawnTooltip, 256.0f, GUIStyle.SmallFont.Value); GUI.DrawString(spriteBatch, PlayerInput.MousePosition + new Vector2(32, 32), tooltip, Color.White, Color.Black * 0.8f, 4, GUIStyle.SmallFont); } spriteBatch.End(); } public override void Update(double deltaTime) { if (GameMain.GraphicsWidth != screenResolution.X || GameMain.GraphicsHeight != screenResolution.Y) { CreateGUI(); } if (PlayerInput.KeyHit(Keys.R) && PlayerInput.KeyDown(Keys.LeftShift)) { CreateGUI(); } Cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null); Vector2 mousePos = Cam.ScreenToWorld(PlayerInput.MousePosition); mousePos.Y = -mousePos.Y; foreach (EditorNode node in nodeList) { if (PlayerInput.PrimaryMouseButtonDown()) { EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (connection != null && connection.Type.NodeSide == NodeConnectionType.Side.Right) { if (connection.Type != NodeConnectionType.Out) { if (connection.ConnectedTo.Any()) { return; } } DraggedConnection = connection; } } // ReSharper disable once AssignmentInConditionalExpression if (node.IsHighlighted = node.HeaderRectangle.Contains(mousePos)) { if (PlayerInput.PrimaryMouseButtonDown()) { // Ctrl + clicking the headers add them to the "selection" that allows us to drag multiple nodes at once if (PlayerInput.IsCtrlDown()) { if (selectedNodes.Contains(node)) { selectedNodes.Remove(node); } else { selectedNodes.Add(node); } node.IsSelected = selectedNodes.Contains(node); break; } draggedNode = node; dragOffset = draggedNode.Position - mousePos; foreach (EditorNode selectedNode in selectedNodes) { if (!markedNodes.ContainsKey(selectedNode)) { markedNodes.Add(selectedNode, selectedNode.Position - mousePos); } } } } if (PlayerInput.SecondaryMouseButtonClicked()) { EventEditorNodeConnection? connection = node.GetConnectionOnMouse(mousePos); if (node.GetDrawRectangle().Contains(mousePos) || connection != null) { CreateContextMenu(node, node.GetConnectionOnMouse(mousePos)); break; } } } if (PlayerInput.SecondaryMouseButtonClicked()) { foreach (var selectedNode in selectedNodes) { selectedNode.IsSelected = false; } selectedNodes.Clear(); } if (draggedNode != null) { if (!PlayerInput.PrimaryMouseButtonHeld()) { draggedNode = null; markedNodes.Clear(); } else { Vector2 offsetChange = Vector2.Zero; draggedNode.IsHighlighted = true; draggedNode.Position = mousePos + dragOffset; if (PlayerInput.KeyHit(Keys.Up)) { offsetChange.Y--; } if (PlayerInput.KeyHit(Keys.Down)) { offsetChange.Y++; } if (PlayerInput.KeyHit(Keys.Left)) { offsetChange.X--; } if (PlayerInput.KeyHit(Keys.Right)) { offsetChange.X++; } dragOffset += offsetChange; foreach (var (editorNode, offset) in markedNodes.Where(pair => pair.Key != draggedNode)) { editorNode.Position = mousePos + offset; } if (offsetChange != Vector2.Zero) { foreach (var (key, value) in markedNodes.ToList()) { markedNodes[key] = value + offsetChange; } } } } if (DraggedConnection != null) { if (!PlayerInput.PrimaryMouseButtonHeld()) { foreach (EditorNode node in nodeList) { var nodeOnMouse = node.GetConnectionOnMouse(mousePos); if (nodeOnMouse != null && nodeOnMouse != DraggedConnection && nodeOnMouse.Type.NodeSide == NodeConnectionType.Side.Left) { if (!DraggedConnection.CanConnect(nodeOnMouse)) { continue; } nodeOnMouse.ClearConnections(); EditorNode.Connect(DraggedConnection, nodeOnMouse); break; } } DraggedConnection = null; } else { DraggingPosition = mousePos; } } else { DraggingPosition = Vector2.Zero; } if (PlayerInput.MidButtonHeld()) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float) deltaTime * 60.0f / Cam.Zoom; moveSpeed.X = -moveSpeed.X; Cam.Position += moveSpeed; } base.Update(deltaTime); } } }