// Copyright © Gamesmiths Guild. #if TOOLS using System.Collections.Generic; using Gamesmiths.Forge.Core; using Gamesmiths.Forge.Godot.Core; using Gamesmiths.Forge.Godot.Resources.Statescript; using Godot; using GodotCollections = Godot.Collections; namespace Gamesmiths.Forge.Godot.Editor.Statescript; public partial class StatescriptGraphEditorDock { private static bool WouldCreateLoop( StatescriptGraph graphResource, string fromNodeId, int fromPort, string toNodeId, int toPort) { var tempConnection = new StatescriptConnection { FromNode = fromNodeId, OutputPort = fromPort, ToNode = toNodeId, InputPort = toPort, }; graphResource.Connections.Add(tempConnection); try { StatescriptGraphBuilder.Build(graphResource); } catch (ValidationException) { return true; } finally { graphResource.Connections.Remove(tempConnection); } return false; } private void OnConnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null) { return; } if (WouldCreateLoop(graph, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort)) { ShowLoopWarningDialog(); return; } if (_undoRedo is not null) { _undoRedo.CreateAction("Connect Statescript Nodes", customContext: graph); _undoRedo.AddDoMethod( this, MethodName.DoConnect, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort); _undoRedo.AddUndoMethod( this, MethodName.UndoConnect, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort); _undoRedo.CommitAction(); } else { DoConnect(fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort); } } private void DoConnect(string fromNode, int fromPort, string toNode, int toPort) { _graphEdit?.ConnectNode(fromNode, fromPort, toNode, toPort); SyncConnectionsToCurrentGraph(); } private void UndoConnect(string fromNode, int fromPort, string toNode, int toPort) { _graphEdit?.DisconnectNode(fromNode, fromPort, toNode, toPort); SyncConnectionsToCurrentGraph(); } private void OnDisconnectionRequest(StringName fromNode, long fromPort, StringName toNode, long toPort) { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null) { return; } if (_undoRedo is not null) { _undoRedo.CreateAction("Disconnect Statescript Nodes", customContext: graph); _undoRedo.AddDoMethod( this, MethodName.UndoConnect, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort); _undoRedo.AddUndoMethod( this, MethodName.DoConnect, fromNode.ToString(), (int)fromPort, toNode.ToString(), (int)toPort); _undoRedo.CommitAction(); } else { _graphEdit.DisconnectNode(fromNode, (int)fromPort, toNode, (int)toPort); SyncConnectionsToCurrentGraph(); } } private void OnDeleteNodesRequest(GodotCollections.Array deletedNodes) { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null) { return; } foreach (StringName nodeName in deletedNodes) { Node? child = _graphEdit.GetNodeOrNull(nodeName.ToString()); if (child is not StatescriptGraphNode graphNode) { continue; } if (graphNode.NodeResource?.NodeType == StatescriptNodeType.Entry) { GD.PushWarning("Cannot delete the Entry statescriptNode."); continue; } if (graphNode.NodeResource is null) { continue; } var affectedConnections = new List(); foreach (GodotCollections.Dictionary connection in _graphEdit.GetConnectionList()) { StringName from = connection["from_node"].AsStringName(); StringName to = connection["to_node"].AsStringName(); if (from == nodeName || to == nodeName) { affectedConnections.Add(new StatescriptConnection { FromNode = connection["from_node"].AsString(), OutputPort = connection["from_port"].AsInt32(), ToNode = connection["to_node"].AsString(), InputPort = connection["to_port"].AsInt32(), }); } } if (_undoRedo is not null) { _undoRedo.CreateAction("Delete Statescript Node", customContext: graph); _undoRedo.AddDoMethod( this, MethodName.DoDeleteNode, graph, graphNode.NodeResource, new GodotCollections.Array(affectedConnections)); _undoRedo.AddUndoMethod( this, MethodName.UndoDeleteNode, graph, graphNode.NodeResource, new GodotCollections.Array(affectedConnections)); _undoRedo.CommitAction(); } else { DoDeleteNode( graph, graphNode.NodeResource, [.. affectedConnections]); } } } private void DoDeleteNode( StatescriptGraph graph, StatescriptNode nodeResource, GodotCollections.Array affectedConnections) { if (_graphEdit is not null && CurrentGraph == graph) { foreach (StatescriptConnection connection in affectedConnections) { _graphEdit.DisconnectNode( connection.FromNode, connection.OutputPort, connection.ToNode, connection.InputPort); } Node? child = _graphEdit.GetNodeOrNull(nodeResource.NodeId); child?.QueueFree(); } graph.Nodes.Remove(nodeResource); SyncConnectionsToCurrentGraph(); } private void UndoDeleteNode( StatescriptGraph graph, StatescriptNode nodeResource, GodotCollections.Array affectedConnections) { graph.Nodes.Add(nodeResource); graph.Connections.AddRange(affectedConnections); if (CurrentGraph == graph) { LoadGraphIntoEditor(graph); } } private void OnBeginNodeMove() { if (_graphEdit is null) { return; } _preMovePositions.Clear(); foreach (Node child in _graphEdit.GetChildren()) { if (child is StatescriptGraphNode { Selected: true } sgn) { _preMovePositions[sgn.Name] = sgn.PositionOffset; } } } private void OnEndNodeMove() { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null || _preMovePositions.Count == 0) { return; } var movedNodes = new GodotCollections.Dictionary(); var oldPositions = new GodotCollections.Dictionary(); foreach (Node child in _graphEdit.GetChildren()) { if (child is not StatescriptGraphNode sgn || !_preMovePositions.TryGetValue(sgn.Name, out Vector2 oldPos)) { continue; } Vector2 newPos = sgn.PositionOffset; if (oldPos != newPos) { movedNodes[sgn.Name] = newPos; oldPositions[sgn.Name] = oldPos; } } _preMovePositions.Clear(); if (movedNodes.Count == 0) { return; } if (_undoRedo is not null) { _undoRedo.CreateAction("Move Statescript Node(s)", customContext: graph); _undoRedo.AddDoMethod(this, MethodName.DoMoveNodes, graph, movedNodes); _undoRedo.AddUndoMethod(this, MethodName.DoMoveNodes, graph, oldPositions); _undoRedo.CommitAction(false); } SyncNodePositionsToResource(graph, movedNodes); } private void DoMoveNodes( StatescriptGraph graph, GodotCollections.Dictionary positions) { foreach (StatescriptNode node in graph.Nodes) { if (positions.TryGetValue(node.NodeId, out Vector2 pos)) { node.PositionOffset = pos; } } if (CurrentGraph == graph && _graphEdit is not null) { foreach (Node child in _graphEdit.GetChildren()) { if (child is StatescriptGraphNode sgn && positions.TryGetValue(sgn.Name, out Vector2 pos)) { sgn.PositionOffset = pos; } } } } private string AddNodeAtPosition( StatescriptNodeType nodeType, string title, string runtimeTypeName, Vector2 position) { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null) { return string.Empty; } var nodeId = $"node_{_nextNodeId++}"; var nodeResource = new StatescriptNode { NodeId = nodeId, Title = title, NodeType = nodeType, RuntimeTypeName = runtimeTypeName, PositionOffset = position, }; if (_undoRedo is not null) { _undoRedo.CreateAction("Add Statescript Node", customContext: graph); _undoRedo.AddDoMethod(this, MethodName.DoAddNode, graph, nodeResource); _undoRedo.AddUndoMethod(this, MethodName.UndoAddNode, graph, nodeResource); _undoRedo.CommitAction(); } else { DoAddNode(graph, nodeResource); } return nodeId; } private void DoAddNode(StatescriptGraph graph, StatescriptNode nodeResource) { graph.Nodes.Add(nodeResource); if (CurrentGraph == graph && _graphEdit is not null) { var graphNode = new StatescriptGraphNode(); _graphEdit.AddChild(graphNode); graphNode.Initialize(nodeResource, graph); graphNode.SetUndoRedo(_undoRedo); } } private void UndoAddNode(StatescriptGraph graph, StatescriptNode nodeResource) { graph.Nodes.Remove(nodeResource); if (CurrentGraph == graph) { LoadGraphIntoEditor(graph); } } private void DuplicateSelectedNodes() { StatescriptGraph? graph = CurrentGraph; if (graph is null || _graphEdit is null) { return; } var selectedNodes = new List(); foreach (Node child in _graphEdit.GetChildren()) { if (child is StatescriptGraphNode { Selected: true } statescriptNode && statescriptNode.NodeResource is not null && statescriptNode.NodeResource.NodeType != StatescriptNodeType.Entry) { selectedNodes.Add(statescriptNode); } } if (selectedNodes.Count == 0) { return; } foreach (StatescriptGraphNode sgn in selectedNodes) { sgn.Selected = false; } var duplicatedIds = new Dictionary(); const float offset = 40f; foreach (StatescriptGraphNode sgn in selectedNodes) { StatescriptNode original = sgn.NodeResource!; var newNodeId = $"node_{_nextNodeId++}"; duplicatedIds[original.NodeId] = newNodeId; var duplicated = new StatescriptNode { NodeId = newNodeId, Title = original.Title, NodeType = original.NodeType, RuntimeTypeName = original.RuntimeTypeName, PositionOffset = original.PositionOffset + new Vector2(offset, offset), }; foreach (KeyValuePair kvp in original.CustomData) { duplicated.CustomData[kvp.Key] = kvp.Value; } foreach (StatescriptNodeProperty binding in original.PropertyBindings) { var newBinding = new StatescriptNodeProperty { Direction = binding.Direction, PropertyIndex = binding.PropertyIndex, Resolver = binding.Resolver is not null ? (StatescriptResolverResource)binding.Resolver.Duplicate(true) : null, }; duplicated.PropertyBindings.Add(newBinding); } graph.Nodes.Add(duplicated); var graphNode = new StatescriptGraphNode(); _graphEdit.AddChild(graphNode); graphNode.Initialize(duplicated, graph); graphNode.Selected = true; } foreach (StatescriptConnection connection in graph.Connections) { if (duplicatedIds.TryGetValue(connection.FromNode, out var newFrom) && duplicatedIds.TryGetValue(connection.ToNode, out var newTo)) { _graphEdit.ConnectNode(newFrom, connection.OutputPort, newTo, connection.InputPort); } } SyncConnectionsToCurrentGraph(); } private void ShowLoopWarningDialog() { var dialog = new AcceptDialog { Title = "Connection Rejected", DialogText = "This connection would create a loop in the graph, which is not allowed.", Exclusive = true, }; dialog.Confirmed += dialog.QueueFree; dialog.Canceled += dialog.QueueFree; AddChild(dialog); dialog.PopupCentered(); } } #endif