#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Xml.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Barotrauma { internal class EditorNode { public Vector2 Position { get; set; } public Vector2 Size { get; set; } public int ID; private const int HeaderSize = 32; public Rectangle HeaderRectangle => new Rectangle(Position.ToPoint(), new Point((int) Size.X, HeaderSize)); public Rectangle Rectangle => new Rectangle(new Point((int) Position.X, (int) Position.Y + HeaderSize), Size.ToPoint()); public string Name { get; protected set; } public bool CanAddConnections { get; set; } public readonly List Connections = new List(); public readonly List RemovableTypes = new List(); public bool IsHighlighted; public bool IsSelected; protected EditorNode(string name) { Name = name; Position = Vector2.Zero; } public virtual XElement Save() { throw new NotImplementedException(); } public XElement SaveConnections() { XElement allConnections = new XElement("Connections", new XAttribute("i", ID)); foreach (EventEditorNodeConnection connection in Connections) { XElement connectionElement = new XElement("Connection"); connectionElement.Add(new XAttribute("i", connection.ID)); connectionElement.Add(new XAttribute("type", connection.Type.Label)); if (connection.EndConversation) { connectionElement.Add(new XAttribute("endconversation", connection.EndConversation)); } if (!string.IsNullOrWhiteSpace(connection.OptionText)) { connectionElement.Add(new XAttribute("optiontext", connection.OptionText)); } if (connection.OverrideValue is { } overrideValue && !string.IsNullOrWhiteSpace(connection.OverrideValue?.ToString())) { connectionElement.Add(new XAttribute("overridevalue", overrideValue.ToString() ?? string.Empty)); connectionElement.Add(new XAttribute("valuetype", overrideValue.GetType().ToString())); } foreach (var nodeConnection in connection.ConnectedTo) { XElement connectedTo = new XElement("ConnectedTo", new XAttribute("i", nodeConnection.ID), new XAttribute("node", nodeConnection.Parent.ID)); connectionElement.Add(connectedTo); } allConnections.Add(connectionElement); } return allConnections; } public void LoadConnections(XElement element) { foreach (var subElement in element.Elements()) { int id = subElement.GetAttributeInt("i", -1); string? connectionType = subElement.GetAttributeString("type", null); bool endConversation = subElement.GetAttributeBool("endconversation", false); if (id < 0) { continue; } EventEditorNodeConnection? connection = Connections.Find(c => c.ID == id); if (connection == null) { if (string.Equals(connectionType, NodeConnectionType.Option.Label, StringComparison.InvariantCultureIgnoreCase)) { connection = new EventEditorNodeConnection(this, NodeConnectionType.Option) { ID = id, EndConversation = endConversation }; Connections.Add(connection); } else { continue; } } string? optionText = subElement.GetAttributeString("optiontext", null); string? overrideValue = subElement.GetAttributeString("overridevalue", null); string? valueType = subElement.GetAttributeString("valuetype", null); if (optionText != null) { connection.OptionText = optionText; } if (overrideValue != null && valueType != null) { Type? type = Type.GetType(valueType); if (type != null) { if (type.IsEnum) { Array enums = Enum.GetValues(type); foreach (object? @enum in enums) { if (string.Equals(@enum?.ToString(), overrideValue, StringComparison.InvariantCultureIgnoreCase)) { connection.OverrideValue = @enum; } } } else { connection.OverrideValue = EventEditorScreen.ChangeType(overrideValue, type); } } } foreach (XElement connectedTo in subElement.Elements()) { int id2 = connectedTo.GetAttributeInt("i", -1); int node = connectedTo.GetAttributeInt("node", -1); if (id2 < 0 || node < 0) { continue; } EditorNode? otherNode = EventEditorScreen.nodeList.Find(editorNode => editorNode.ID == node); EventEditorNodeConnection? otherConnection = otherNode?.Connections.Find(c => c.ID == id2); if (otherConnection != null) { connection.ConnectedTo.Add(otherConnection); } } } } public static EditorNode? Load(XElement element) { return element.Name.ToString().ToLowerInvariant() switch { "eventnode" => EventNode.LoadEventNode(element), "valuenode" => ValueNode.LoadValueNode(element), "customnode" => CustomNode.LoadCustomNode(element), _ => null }; } public virtual XElement? ToXML() { XElement newElement = new XElement(Name); foreach (var connection in Connections) { if (connection.Type == NodeConnectionType.Value) { if (connection.GetValue() is { } connValue) { newElement.Add(new XAttribute(connection.Attribute.ToLowerInvariant(), connValue)); } } } newElement.Add(new XAttribute("_npos", XMLExtensions.Vector2ToString(Position))); return newElement; } public void Connect(EditorNode otherNode, NodeConnectionType type) { EventEditorNodeConnection? conn = Connections.Find(connection => connection.Type == type && !connection.ConnectedTo.Any()); EventEditorNodeConnection? found = otherNode.Connections.Find(connection => connection.Type == NodeConnectionType.Activate); if (found != null) { conn?.ConnectedTo.Add(found); } } public static void Connect(EventEditorNodeConnection connection, EventEditorNodeConnection ownConnection) { connection.ConnectedTo.Add(ownConnection); } public static void Disconnect(EventEditorNodeConnection conn) { foreach (var connection in EventEditorScreen.nodeList.SelectMany(editorNode => editorNode.Connections.Where(connection => connection.ConnectedTo.Contains(conn)))) { connection.ConnectedTo.Remove(conn); } } public void ClearConnections() { foreach (EventEditorNodeConnection conn in Connections) { conn.ClearConnections(); } } public virtual Rectangle GetDrawRectangle() { return Rectangle; } public EventEditorNodeConnection? GetConnectionOnMouse(Vector2 mousePos) { return Connections.FirstOrDefault(eventNodeConnection => eventNodeConnection.DrawRectangle.Contains(mousePos)); } public void Draw(SpriteBatch spriteBatch) { DrawBack(spriteBatch); DrawFront(spriteBatch); } protected virtual void DrawFront(SpriteBatch spriteBatch) { } protected virtual Color BackgroundColor => new Color(150, 150, 150); private void DrawBack(SpriteBatch spriteBatch) { Color outlineColor = Color.White * 0.8f; Color fontColor = Color.White; Color headerColor = IsHighlighted ? new Color(100, 100, 100) : new Color(120, 120, 120); if (IsSelected) { headerColor = new Color(80, 80, 80); } float camZoom = Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f; Rectangle bodyRect = GetDrawRectangle(); GUI.DrawRectangle(spriteBatch, HeaderRectangle, headerColor, isFilled: true, depth: 1.0f); GUI.DrawRectangle(spriteBatch, bodyRect, BackgroundColor, isFilled: true, depth: 1.0f); GUI.DrawRectangle(spriteBatch, HeaderRectangle, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom)); GUI.DrawRectangle(spriteBatch, bodyRect, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom)); int x = 0, y = 0; foreach (EventEditorNodeConnection connection in Connections) { switch (connection.Type.NodeSide) { case NodeConnectionType.Side.Left: connection.Draw(spriteBatch, Rectangle, y); y++; break; case NodeConnectionType.Side.Right: connection.Draw(spriteBatch, Rectangle, x); x++; break; } } Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name); GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor); } public void AddConnection(NodeConnectionType connectionType) { Connections.Add(new EventEditorNodeConnection(this, connectionType)); } public virtual void AddOption() { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Option)); } public void RemoveOption(EventEditorNodeConnection connection) { int index = Connections.IndexOf(connection); foreach (var nodeConnection in Connections.Skip(index)) { nodeConnection.ID--; } Connections.Remove(connection); } public EditorNode? GetNext() { var nextNode = Connections.Find(connection => connection.Type == NodeConnectionType.Next); return nextNode?.ConnectedTo.FirstOrDefault()?.Parent; } public EditorNode? GetNext(NodeConnectionType type) { var nextNode = Connections.Find(connection => connection.Type == type); return nextNode?.ConnectedTo.FirstOrDefault()?.Parent; } public static bool IsInstanceOf(Type type1, Type type2) { return type1.IsAssignableFrom(type2) || type1.IsSubclassOf(type2); } public EditorNode? GetParent() { var myNode = Connections.Find(connection => connection.Type == NodeConnectionType.Activate); if (myNode == null) { return null; } foreach (EditorNode editorNode in EventEditorScreen.nodeList) { List childConnection = editorNode.Connections.Where(connection => connection.Type == NodeConnectionType.Next || connection.Type == NodeConnectionType.Option || connection.Type == NodeConnectionType.Failure || connection.Type == NodeConnectionType.Success || connection.Type == NodeConnectionType.Add).ToList(); if (childConnection.Any(connection => connection != null && connection.ConnectedTo.Contains(myNode))) { return editorNode; } } return null; } } internal class EventNode : EditorNode { private readonly Type type; public EventNode(Type type, string name) : base(name) { this.type = type; Size = new Vector2(256, 256); PropertyInfo[] properties = type.GetProperties().Where(info => info.CustomAttributes.Any(data => data.AttributeType == typeof(Serialize))).ToArray(); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); foreach (PropertyInfo property in properties) { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Value, property.Name, property.PropertyType, property)); } if (IsInstanceOf(type, typeof(BinaryOptionAction))) { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Success)); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Failure)); } if (IsInstanceOf(type, typeof(ConversationAction))) { CanAddConnections = true; RemovableTypes.Add(NodeConnectionType.Option); } if (IsInstanceOf(type, typeof(StatusEffectAction)) || IsInstanceOf(type, typeof(MissionAction))) { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } } public override XElement Save() { XElement newElement = new XElement(nameof(EventNode), new XAttribute("i", ID), new XAttribute("type", type.ToString()), new XAttribute("name", Name), new XAttribute("xpos", Position.X), new XAttribute("ypos", Position.Y)); return newElement; } public static EditorNode? LoadEventNode(XElement element) { if (!string.Equals(element.Name.ToString(), nameof(EventNode), StringComparison.InvariantCultureIgnoreCase)) { return null; } Type? t = Type.GetType(element.GetAttributeString("type", string.Empty)); if (t == null) { return null; } EventNode newNode = new EventNode(t, element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; float posX = element.GetAttributeFloat("xpos", 0f); float posY = element.GetAttributeFloat("ypos", 0f); newNode.Position = new Vector2(posX, posY); return newNode; } public override Rectangle GetDrawRectangle() { return ScaleRectFromConnections(Connections, Rectangle); } public static Rectangle ScaleRectFromConnections(List connections, Rectangle baseRect) { // determine how big this box should get based on how many input/output nodes the sides have int y = connections.Count(connection => connection.Type.NodeSide == NodeConnectionType.Side.Left), x = connections.Count(connection => connection.Type.NodeSide == NodeConnectionType.Side.Right); int maxHeight = Math.Max(x, y); Rectangle bodyRect = baseRect; bodyRect.Height = bodyRect.Height / 8 * maxHeight; return bodyRect; } public Tuple[] GetOptions() { IEnumerable myNode = Connections.Where(connection => connection.Type == NodeConnectionType.Option).ToArray(); List> list = new List>(); if (myNode != null) { foreach (EventEditorNodeConnection connection in myNode) { if (connection.ConnectedTo.Any()) { foreach (EventEditorNodeConnection nodeConnection in connection.ConnectedTo) { list.Add(Tuple.Create((EditorNode?) nodeConnection.Parent, connection.OptionText, connection.EndConversation)); } } else { list.Add(Tuple.Create(null, connection.OptionText, connection.EndConversation)); } } } return list.ToArray(); } } internal class ValueNode : EditorNode { private object? nodeValue; public object? Value { get => nodeValue; set { nodeValue = value; if (value is string str) { WrappedText = TextManager.Get(str) is { Loaded:true } translated ? translated.Value : str; } else { WrappedText = value?.ToString() ?? string.Empty; } valueTextSize = GUIStyle.SubHeadingFont.MeasureString(WrappedText); } } private Vector2 valueTextSize = Vector2.Zero; public Type Type { get; } public ValueNode(Type type, string name) : base(name) { Type = type; Value = type.IsValueType ? Activator.CreateInstance(type) : null; Size = new Vector2(256, 32); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Out, "Output", Type)); } public override XElement Save() { XElement newElement = new XElement(nameof(ValueNode)); newElement.Add(new XAttribute("i", ID)); if (Value != null) { newElement.Add(new XAttribute("value", Value)); } newElement.Add(new XAttribute("type", Type.ToString())); newElement.Add(new XAttribute("name", Name)); newElement.Add(new XAttribute("xpos", Position.X)); newElement.Add(new XAttribute("ypos", Position.Y)); return newElement; } public override XElement? ToXML() { return null; } public static EditorNode? LoadValueNode(XElement element) { if (!string.Equals(element.Name.ToString(), nameof(ValueNode), StringComparison.InvariantCultureIgnoreCase)) { return null; } string? value = element.GetAttributeString("value", null); Type? type = Type.GetType(element.GetAttributeString("type", string.Empty)); if (type != null) { ValueNode newNode = new ValueNode(type, element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; float posX = element.GetAttributeFloat("xpos", 0f); float posY = element.GetAttributeFloat("ypos", 0f); newNode.Position = new Vector2(posX, posY); if (value != null) { if (type.IsEnum) { Array enums = Enum.GetValues(type); foreach (object? @enum in enums) { if (string.Equals(@enum?.ToString(), value, StringComparison.InvariantCultureIgnoreCase)) { newNode.Value = @enum; } } } else { newNode.Value = EventEditorScreen.ChangeType(value, type); } } return newNode; } return null; } protected override Color BackgroundColor => new Color(50, 50, 50); private string? wrappedText; private string? WrappedText { get => wrappedText; set { string valueText = value ?? "null"; int width = Rectangle.Width; if (width == 0) { wrappedText = valueText; return; } if (width > 16) { width -= 16; } if (GUIStyle.SubHeadingFont.Value != null) { valueText = ToolBox.WrapText(valueText, width, GUIStyle.SubHeadingFont.Value); } wrappedText = valueText; } } public override Rectangle GetDrawRectangle() { Rectangle drawRectangle = Rectangle; Vector2 size = GUIStyle.SubHeadingFont.MeasureString(WrappedText ?? ""); drawRectangle.Height = (int) Math.Max(size.Y + 16, drawRectangle.Height); return drawRectangle; } protected override void DrawFront(SpriteBatch spriteBatch) { base.DrawFront(spriteBatch); Vector2 pos = GetDrawRectangle().Location.ToVector2() + (GetDrawRectangle().Size.ToVector2() / 2) - (valueTextSize / 2); Rectangle drawRect = Rectangle; drawRect.Inflate(-1, -1); GUI.DrawString(spriteBatch, pos, WrappedText, EventEditorNodeConnection.GetPropertyColor(Type), font: GUIStyle.SubHeadingFont); } } class SpecialNode : EditorNode { public SpecialNode(string name) : base(name) { Size = new Vector2(256, 256); } public override Rectangle GetDrawRectangle() { return EventNode.ScaleRectFromConnections(Connections, Rectangle); } } class CustomNode : SpecialNode { public CustomNode(string name) : base(name) { CanAddConnections = true; RemovableTypes.Add(NodeConnectionType.Value); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Activate)); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Next)); Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Add)); } public CustomNode() : this("Custom") { Prompt(s => { Name = s; return true; }); } public override void AddOption() { Prompt(s => { Connections.Add(new EventEditorNodeConnection(this, NodeConnectionType.Value, s, typeof(string))); return true; }); } public override XElement Save() { XElement newElement = new XElement(nameof(CustomNode)); newElement.Add(new XAttribute("i", ID)); newElement.Add(new XAttribute("name", Name)); newElement.Add(new XAttribute("xpos", Position.X)); newElement.Add(new XAttribute("ypos", Position.Y)); foreach (EventEditorNodeConnection connection in Connections.FindAll(connection => connection.Type == NodeConnectionType.Value)) { newElement.Add(new XElement("Value", new XAttribute("name", connection.Attribute))); } return newElement; } public static EditorNode? LoadCustomNode(XElement element) { if (!string.Equals(element.Name.ToString(), nameof(CustomNode), StringComparison.OrdinalIgnoreCase)) { return null; } CustomNode newNode = new CustomNode(element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) }; float posX = element.GetAttributeFloat("xpos", 0f); float posY = element.GetAttributeFloat("ypos", 0f); newNode.Position = new Vector2(posX, posY); foreach (XElement valueElement in element.Elements()) { newNode.Connections.Add(new EventEditorNodeConnection(newNode, NodeConnectionType.Value, valueElement.GetAttributeString("name", string.Empty), typeof(string))); } return newNode; } private static void Prompt(Func OnAccepted) { var msgBox = new GUIMessageBox(TextManager.Get("Name"), "", new[] { TextManager.Get("Ok"), TextManager.Get("Cancel") }, 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)); msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); return true; }; msgBox.Buttons[0].OnClicked = delegate { OnAccepted.Invoke(nameInput.Text); msgBox.Close(); return true; }; } } }