From bd6cdeb97bb4abce43700ab28c6549d7cec56118 Mon Sep 17 00:00:00 2001 From: Nicola Date: Thu, 14 May 2026 09:58:35 +0200 Subject: [PATCH] Finished new node approach --- Scripts/Core/FileHandler.cs | 2 +- Scripts/Gameplay/Robots/Robot.cs | 1 - Scripts/Tests/TestRunner.cs | 74 +++++++- Scripts/UI/DSL/CodingWindow.cs | 293 ++++++++++++++++++++++++++----- Scripts/UI/DSL/NodeDisplay.cs | 10 +- 5 files changed, 332 insertions(+), 48 deletions(-) diff --git a/Scripts/Core/FileHandler.cs b/Scripts/Core/FileHandler.cs index 1e4d54a..acb0b62 100644 --- a/Scripts/Core/FileHandler.cs +++ b/Scripts/Core/FileHandler.cs @@ -4,7 +4,7 @@ using Godot; public class FileHandler { private const string ScriptDirectory = "user://scripts"; - private const string ScriptExtension = ".dsl"; + private const string ScriptExtension = ".json"; public static void CreateScriptDirectory() { diff --git a/Scripts/Gameplay/Robots/Robot.cs b/Scripts/Gameplay/Robots/Robot.cs index 4429a11..824bf0f 100644 --- a/Scripts/Gameplay/Robots/Robot.cs +++ b/Scripts/Gameplay/Robots/Robot.cs @@ -35,7 +35,6 @@ public partial class Robot : Node3D { if (CanExecute(delta)) { - GD.Print(currentNode.DisplayText); switch (currentNode.Execute(this, delta)) { case NodeResult.SUCCESS: diff --git a/Scripts/Tests/TestRunner.cs b/Scripts/Tests/TestRunner.cs index 8f7330c..c38c684 100644 --- a/Scripts/Tests/TestRunner.cs +++ b/Scripts/Tests/TestRunner.cs @@ -36,6 +36,8 @@ public partial class TestRunner : Node Run("While node reports false conditions", TestWhileNodeReportsFalseConditions); Run("For node stops after configured amount", TestForNodeStopsAfterConfiguredAmount); Run("Start node succeeds immediately", TestStartNodeSucceedsImmediately); + Run("Split node connections restore both branches", TestSplitNodeConnectionsRestoreBothBranches); + Run("Linear node connection restores next node", TestLinearNodeConnectionRestoresNextNode); Run("Paused world does not drain survival", TestPausedWorldDoesNotDrainSurvival); Run("Open gate hides gate content", TestOpenGateHidesGateContent); Run("Layer generation succeeds above threshold", TestLayerGenerationSuccessRate); @@ -372,7 +374,7 @@ public partial class TestRunner : Node private void TestSavedScriptsCanBeDeleted() { string scriptName = "delete_test_script"; - FileHandler.SaveProgram(scriptName, "Name: Explore;"); + FileHandler.SaveProgram(scriptName, "{\"Nodes\":[],\"Connections\":[]}"); AssertTrue(FileHandler.LoadProgramNames().Contains(scriptName), "script should be listed"); AssertTrue(FileHandler.DeleteProgram(scriptName), "delete should succeed"); @@ -485,6 +487,61 @@ public partial class TestRunner : Node AssertEqual("Name: Start", node.Save(), "start save"); } + private void TestSplitNodeConnectionsRestoreBothBranches() + { + MoveNode successNode = new MoveNode(); + MoveNode negativeNode = new MoveNode(); + System.Collections.Generic.Dictionary availableNodes = + new System.Collections.Generic.Dictionary + { + { new StringName("success"), successNode }, + { new StringName("negative"), negativeNode } + }; + List connections = new List + { + CreateConnection("split", 0, "success", 0), + CreateConnection("split", 1, "negative", 0) + }; + + IfNode ifNode = new IfNode(); + ifNode.SetNextNode(connections, availableNodes); + + AssertTrue(ReferenceEquals(successNode, ifNode.nextNode), "if success branch"); + AssertTrue(ReferenceEquals(negativeNode, ifNode.NegativeNode), "if negative branch"); + + WhileNode whileNode = new WhileNode(); + whileNode.SetNextNode(connections, availableNodes); + + AssertTrue(ReferenceEquals(successNode, whileNode.nextNode), "while success branch"); + AssertTrue(ReferenceEquals(negativeNode, whileNode.NegativeNode), "while negative branch"); + + ForNode forNode = new ForNode(); + forNode.SetNextNode(connections, availableNodes); + + AssertTrue(ReferenceEquals(successNode, forNode.nextNode), "for success branch"); + AssertTrue(ReferenceEquals(negativeNode, forNode.NegativeNode), "for negative branch"); + } + + private void TestLinearNodeConnectionRestoresNextNode() + { + MoveNode targetNode = new MoveNode(); + StartNode startNode = new StartNode(); + System.Collections.Generic.Dictionary availableNodes = + new System.Collections.Generic.Dictionary + { + { new StringName("target"), targetNode } + }; + List connections = new List + { + CreateConnection("start", 0, "target", 0) + }; + + startNode.SetNextNode(connections, availableNodes); + + AssertTrue(ReferenceEquals(targetNode, startNode.nextNode), "linear next node"); + AssertTrue(startNode.NegativeNode == null, "linear node should not have negative branch"); + } + private void TestPausedWorldDoesNotDrainSurvival() { GameData.isPaused = true; @@ -620,6 +677,21 @@ public partial class TestRunner : Node return WFC.IsMapConnected(layer.tiles, 1f); } + private Godot.Collections.Dictionary CreateConnection( + string fromNode, + int fromPort, + string toNode, + int toPort + ) + { + Godot.Collections.Dictionary connection = new Godot.Collections.Dictionary(); + connection["from_node"] = new StringName(fromNode); + connection["from_port"] = fromPort; + connection["to_node"] = new StringName(toNode); + connection["to_port"] = toPort; + return connection; + } + private void AssertTrue(bool value, string message) { if (!value) diff --git a/Scripts/UI/DSL/CodingWindow.cs b/Scripts/UI/DSL/CodingWindow.cs index b322a6f..4e6f981 100644 --- a/Scripts/UI/DSL/CodingWindow.cs +++ b/Scripts/UI/DSL/CodingWindow.cs @@ -15,6 +15,14 @@ public partial class CodingWindow : PanelContainer public System.Collections.Generic.Dictionary DSLNodes; private System.Collections.Generic.Dictionary availableNodes; + private class ScriptConnection + { + public string FromNodeId; + public int FromPort; + public string ToNodeId; + public int ToPort; + } + public override void _Ready() { DSLNodes = ResourceLoader.LoadDSLNodes(); @@ -140,16 +148,19 @@ public partial class CodingWindow : PanelContainer return; } - availableNodes = BuildAvailableNodeLookup(); - List nodes = BuildScriptOrder(startNode, new List()); + BuildAvailableNodeLookup(); + List nodes = BuildScriptOrder( + startNode, + new List(), + new HashSet() + ); if (nodes.Count > 0) robot.SetupExecution(nodes); robot.currentProgram = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text; } - private System.Collections.Generic.Dictionary BuildAvailableNodeLookup() + private void BuildAvailableNodeLookup() { - System.Collections.Generic.Dictionary availableNodes = - new System.Collections.Generic.Dictionary(); + availableNodes = new System.Collections.Generic.Dictionary(); for (int i = 0; i < editorWindow.GetChildCount(); i++) { @@ -159,12 +170,18 @@ public partial class CodingWindow : PanelContainer nodeDisplay.ReadParameters(); availableNodes.Add(nodeDisplay.Name, nodeDisplay.node); } - - return availableNodes; } - private List BuildScriptOrder(NodeDisplay node, List program) + private List BuildScriptOrder( + NodeDisplay node, + List program, + HashSet visitedNodes + ) { + if (node == null) return program; + if (visitedNodes.Contains(node.Name)) return program; + + visitedNodes.Add(node.Name); program.Add(node.node); if (editorWindow.GetConnectionListFromNode(node.Name).Count <= 0) return program; List nextConnections = CheckNodeConnections(node); @@ -172,9 +189,13 @@ public partial class CodingWindow : PanelContainer node.node.SetNextNode(nextConnections, availableNodes); foreach (Dictionary connection in nextConnections) { + NodeDisplay nextNode = editorWindow.GetNodeOrNull( + new NodePath(connection["to_node"].AsStringName()) + ); program = BuildScriptOrder( - editorWindow.GetNode(new NodePath(connection["to_node"].AsStringName())), - program + nextNode, + program, + visitedNodes ); } return program; @@ -220,44 +241,197 @@ public partial class CodingWindow : PanelContainer ClearWindow(); string scriptContent = FileHandler.LoadProgram(availableScripts.GetItemText(index)); - string[] nodes = scriptContent.Split(";"); - foreach (string node in nodes) - { - NodeDisplay nodeDisplay = NodeDisplay.Load(node, DSLNodes); - if (nodeDisplay != null) - { - editorWindow.AddChild(nodeDisplay); - RegisterEditorNode(nodeDisplay); - } - } + LoadStructuredProgram(scriptContent); scriptName.Text = availableScripts.GetItemText(index); availableScripts.Select(0); } + private void LoadStructuredProgram(string scriptContent) + { + Variant parsedScript = Json.ParseString(scriptContent); + if (parsedScript.VariantType != Variant.Type.Dictionary) return; + + Dictionary scriptData = parsedScript.AsGodotDictionary(); + if (!scriptData.ContainsKey("Nodes")) return; + + System.Collections.Generic.Dictionary loadedNodes = + new System.Collections.Generic.Dictionary(); + Array nodes = scriptData["Nodes"].AsGodotArray(); + for (int i = 0; i < nodes.Count; i++) + { + Dictionary nodeData = nodes[i].AsGodotDictionary(); + NodeDisplay nodeDisplay = LoadStructuredNode(nodeData); + if (nodeDisplay == null) continue; + + editorWindow.AddChild(nodeDisplay); + RegisterEditorNode(nodeDisplay); + RegisterLoadedNode(nodeData, nodeDisplay, loadedNodes); + } + + LoadStructuredConnections(scriptData, loadedNodes); + } + + private void RegisterLoadedNode( + Dictionary nodeData, + NodeDisplay nodeDisplay, + System.Collections.Generic.Dictionary loadedNodes + ) + { + string nodeId = nodeDisplay.Name.ToString(); + if (nodeData.ContainsKey("Id")) + { + nodeId = nodeData["Id"].AsString(); + } + + if (!loadedNodes.ContainsKey(nodeId)) + { + loadedNodes.Add(nodeId, nodeDisplay); + } + } + + private NodeDisplay LoadStructuredNode(Dictionary nodeData) + { + if (!nodeData.ContainsKey("Type")) return null; + if (!nodeData.ContainsKey("Content")) return null; + + string type = nodeData["Type"].AsString(); + string content = nodeData["Content"].AsString(); + NodeDisplay nodeDisplay = NodeDisplay.Load(type, content, DSLNodes); + if (nodeDisplay == null) return null; + + if (nodeData.ContainsKey("Id")) + { + nodeDisplay.Name = nodeData["Id"].AsString(); + } + + if (nodeData.ContainsKey("PositionX") && nodeData.ContainsKey("PositionY")) + { + float positionX = (float)nodeData["PositionX"].AsDouble(); + float positionY = (float)nodeData["PositionY"].AsDouble(); + nodeDisplay.PositionOffset = new Vector2(positionX, positionY); + } + + return nodeDisplay; + } + + private void LoadStructuredConnections( + Dictionary scriptData, + System.Collections.Generic.Dictionary loadedNodes + ) + { + if (!scriptData.ContainsKey("Connections")) return; + + Array connectionData = scriptData["Connections"].AsGodotArray(); + for (int i = 0; i < connectionData.Count; i++) + { + Dictionary savedConnection = connectionData[i].AsGodotDictionary(); + if (!savedConnection.ContainsKey("From")) continue; + if (!savedConnection.ContainsKey("To")) continue; + if (!savedConnection.ContainsKey("FromPort")) continue; + if (!savedConnection.ContainsKey("ToPort")) continue; + + ScriptConnection connection = new ScriptConnection + { + FromNodeId = savedConnection["From"].AsString(), + FromPort = savedConnection["FromPort"].AsInt32(), + ToNodeId = savedConnection["To"].AsString(), + ToPort = savedConnection["ToPort"].AsInt32() + }; + if (!loadedNodes.ContainsKey(connection.FromNodeId)) continue; + if (!loadedNodes.ContainsKey(connection.ToNodeId)) continue; + + NodeDisplay fromDisplay = loadedNodes[connection.FromNodeId]; + NodeDisplay toDisplay = loadedNodes[connection.ToNodeId]; + if (ConnectionExists(fromDisplay.Name, connection.FromPort, toDisplay.Name, connection.ToPort)) continue; + + editorWindow.ConnectNode( + fromDisplay.Name, + connection.FromPort, + toDisplay.Name, + connection.ToPort + ); + } + } + + private bool ConnectionExists(StringName fromNode, int fromPort, StringName toNode, int toPort) + { + foreach (Dictionary connection in editorWindow.GetConnectionList()) + { + if (connection["from_node"].AsStringName() != fromNode) continue; + if ((int)connection["from_port"] != fromPort) continue; + if (connection["to_node"].AsStringName() != toNode) continue; + if ((int)connection["to_port"] != toPort) continue; + + return true; + } + + return false; + } + public void LoadTemporaryProgram() { if (robot == null) return; if (robot.currentNode == null) return; - HashSet loadedNodes = new HashSet(); - ProgramNode nodeToLoad = robot.currentNode; - while (nodeToLoad != null && !loadedNodes.Contains(nodeToLoad)) - { - loadedNodes.Add(nodeToLoad); - - NodeDisplay nodeDisplay = NodeDisplay.Load(nodeToLoad.Save(), DSLNodes); - if (nodeDisplay != null) - { - editorWindow.AddChild(nodeDisplay); - RegisterEditorNode(nodeDisplay); - } - - nodeToLoad = nodeToLoad.nextNode; - } + System.Collections.Generic.Dictionary loadedNodes = + new System.Collections.Generic.Dictionary(); + LoadTemporaryNode(robot.currentNode, loadedNodes); scriptName.Text = robot.currentProgram ?? ""; } + private NodeDisplay LoadTemporaryNode( + ProgramNode programNode, + System.Collections.Generic.Dictionary loadedNodes + ) + { + if (programNode == null) return null; + if (loadedNodes.ContainsKey(programNode)) return loadedNodes[programNode]; + + NodeDisplay nodeDisplay = NodeDisplay.Load( + programNode.DisplayText, + programNode.Save(), + DSLNodes + ); + if (nodeDisplay == null) return null; + + editorWindow.AddChild(nodeDisplay); + RegisterEditorNode(nodeDisplay); + loadedNodes.Add(programNode, nodeDisplay); + + ConnectTemporaryNode(nodeDisplay, 0, programNode.nextNode, loadedNodes); + ConnectTemporaryNode(nodeDisplay, 1, programNode.NegativeNode, loadedNodes); + + return nodeDisplay; + } + + private void ConnectTemporaryNode( + NodeDisplay fromDisplay, + int fromPort, + ProgramNode targetNode, + System.Collections.Generic.Dictionary loadedNodes + ) + { + NodeDisplay toDisplay = LoadTemporaryNode(targetNode, loadedNodes); + if (toDisplay == null) return; + + ScriptConnection connection = new ScriptConnection + { + FromNodeId = fromDisplay.Name.ToString(), + FromPort = fromPort, + ToNodeId = toDisplay.Name.ToString(), + ToPort = 0 + }; + if (ConnectionExists(fromDisplay.Name, connection.FromPort, toDisplay.Name, connection.ToPort)) return; + + editorWindow.ConnectNode( + fromDisplay.Name, + connection.FromPort, + toDisplay.Name, + connection.ToPort + ); + } + public void DeleteProgram() { string filename = scriptName.Text; @@ -276,20 +450,55 @@ public partial class CodingWindow : PanelContainer public void SaveProgram() { - string result = ""; + Array savedNodes = BuildSavedNodes(); + if (savedNodes.Count <= 0) return; + + Dictionary scriptData = new Dictionary(); + scriptData["Nodes"] = savedNodes; + scriptData["Connections"] = BuildSavedConnections(); + + string result = Json.Stringify(scriptData); + if (result.Length <= 0) return; + string filename = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text; + FileHandler.SaveProgram(filename, result); + SetupScriptOptions(); + } + + private Array BuildSavedNodes() + { + Array savedNodes = new Array(); for (int i = 0; i < editorWindow.GetChildCount(); i++) { NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay; if (nodeDisplay == null) continue; nodeDisplay.ReadParameters(); - result += nodeDisplay.node.Save(); - result += ";\r\n"; + Dictionary savedNode = new Dictionary(); + savedNode["Id"] = nodeDisplay.Name.ToString(); + savedNode["Type"] = nodeDisplay.node.DisplayText.ToLower(); + savedNode["Content"] = nodeDisplay.node.Save(); + savedNode["PositionX"] = nodeDisplay.PositionOffset.X; + savedNode["PositionY"] = nodeDisplay.PositionOffset.Y; + savedNodes.Add(savedNode); } - if (result.Length <= 0) return; - string filename = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text; - FileHandler.SaveProgram(filename, result); - SetupScriptOptions(); + + return savedNodes; + } + + private Array BuildSavedConnections() + { + Array savedConnections = new Array(); + foreach (Dictionary connection in editorWindow.GetConnectionList()) + { + Dictionary savedConnection = new Dictionary(); + savedConnection["From"] = connection["from_node"].AsStringName().ToString(); + savedConnection["FromPort"] = (int)connection["from_port"]; + savedConnection["To"] = connection["to_node"].AsStringName().ToString(); + savedConnection["ToPort"] = (int)connection["to_port"]; + savedConnections.Add(savedConnection); + } + + return savedConnections; } public void OnNodeConnect(StringName from, int fromPort, StringName to, int toPort) diff --git a/Scripts/UI/DSL/NodeDisplay.cs b/Scripts/UI/DSL/NodeDisplay.cs index 95b016c..4234c37 100644 --- a/Scripts/UI/DSL/NodeDisplay.cs +++ b/Scripts/UI/DSL/NodeDisplay.cs @@ -25,13 +25,17 @@ public partial class NodeDisplay : GraphNode EmitSignal(SignalName.OnDeleteNode); } - public static NodeDisplay Load(string content, Dictionary DSLNodes) + public static NodeDisplay Load( + string nodeName, + string content, + Dictionary DSLNodes + ) { string nodeSanitized = content.Replace("\r\n", "").Trim(); if (nodeSanitized.Length <= 0) return null; + string normalizedNodeName = nodeName.Trim().ToLower(); - string nodeName = nodeSanitized.Split(",")[0].Replace("Name: ", "").ToLower(); - PackedScene prefab = GetPrefab(nodeName, DSLNodes); + PackedScene prefab = GetPrefab(normalizedNodeName, DSLNodes); if (prefab == null) return null; NodeDisplay result = prefab.Instantiate();