Fixed robot save mechanic to include nodes and their states.
This commit is contained in:
@@ -96,4 +96,28 @@ public class RobotSaveData
|
||||
public float Maintenance { get; set; }
|
||||
public bool IsCoolingDown { get; set; }
|
||||
public bool IsBroken { get; set; }
|
||||
public List<NodeSaveData> RunningProgramNodes { get; set; }
|
||||
public string ProgramStartNode { get; set; }
|
||||
public string CurrentNode { get; set; }
|
||||
public bool IsExecuting { get; set; }
|
||||
}
|
||||
|
||||
public class NodeSaveData
|
||||
{
|
||||
public string NodeType { get; set; }
|
||||
public string EditorNodeId { get; set; }
|
||||
public string NextEditorNodeId { get; set; }
|
||||
public string NegativeEditorNodeId { get; set; }
|
||||
public string NodeContent { get; set; }
|
||||
public string LastExecutionMessage { get; set; }
|
||||
public string ItemId { get; set; }
|
||||
public string Comparator { get; set; }
|
||||
public int Amount { get; set; }
|
||||
public int AmountExecuted { get; set; }
|
||||
public double ElapsedCraftTime { get; set; }
|
||||
public int AmountCrafted { get; set; }
|
||||
public bool HasTargetPosition { get; set; }
|
||||
public int TargetX { get; set; }
|
||||
public int TargetY { get; set; }
|
||||
public int TargetZ { get; set; }
|
||||
}
|
||||
|
||||
@@ -55,4 +55,32 @@ public class CraftNode : ProgramNode
|
||||
{
|
||||
return $"Name: {DisplayText}, Item: {(selectedItem == null ? "Empty" : selectedItem.data.Id)}, Amount: {amount}";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.ItemId = selectedItem?.data.Id;
|
||||
saveData.Amount = amount;
|
||||
if (selectedItem != null)
|
||||
{
|
||||
saveData.ElapsedCraftTime = selectedItem.elapsedCraftTime;
|
||||
saveData.AmountCrafted = selectedItem.amountCrafted;
|
||||
}
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
amount = saveData.Amount;
|
||||
if (string.IsNullOrEmpty(saveData.ItemId)) return;
|
||||
if (!GameData.availableItems.ContainsKey(saveData.ItemId)) return;
|
||||
|
||||
selectedItem = new Item
|
||||
{
|
||||
data = GameData.availableItems[saveData.ItemId],
|
||||
elapsedCraftTime = saveData.ElapsedCraftTime,
|
||||
amountCrafted = saveData.AmountCrafted
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ public class ExploreNode : ProgramNode
|
||||
public Vector3 startPosition;
|
||||
public Vector3I targetPosition;
|
||||
public List<Vector3> pathPoints;
|
||||
public bool hasTargetPosition;
|
||||
|
||||
public ExploreNode()
|
||||
{
|
||||
@@ -16,7 +17,7 @@ public class ExploreNode : ProgramNode
|
||||
|
||||
public override NodeResult Execute(Robot robot, double delta)
|
||||
{
|
||||
if (pathPoints == null && !TrySelectTarget())
|
||||
if (pathPoints == null && !hasTargetPosition && !TrySelectTarget())
|
||||
{
|
||||
lastExecutionMessage = "No tiles left to explore";
|
||||
return NodeResult.SUCCESS;
|
||||
@@ -52,6 +53,7 @@ public class ExploreNode : ProgramNode
|
||||
);
|
||||
if (!GameData.map[targetPosition.Y].tiles[targetPosition.X, targetPosition.Z].wasVisited)
|
||||
{
|
||||
hasTargetPosition = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,6 +96,7 @@ public class ExploreNode : ProgramNode
|
||||
|
||||
lastExecutionMessage = "Current exploration finished";
|
||||
pathPoints = null;
|
||||
hasTargetPosition = false;
|
||||
return NodeResult.RUNNING;
|
||||
}
|
||||
|
||||
@@ -119,7 +122,8 @@ public class ExploreNode : ProgramNode
|
||||
{
|
||||
return new ExploreNode
|
||||
{
|
||||
targetPosition = targetPosition
|
||||
targetPosition = targetPosition,
|
||||
hasTargetPosition = hasTargetPosition
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,4 +131,27 @@ public class ExploreNode : ProgramNode
|
||||
{
|
||||
return $"Name: {DisplayText}";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.TargetX = targetPosition.X;
|
||||
saveData.TargetY = targetPosition.Y;
|
||||
saveData.TargetZ = targetPosition.Z;
|
||||
saveData.HasTargetPosition = hasTargetPosition || pathPoints != null;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
hasTargetPosition = saveData.HasTargetPosition;
|
||||
if (!hasTargetPosition) return;
|
||||
|
||||
targetPosition = new Vector3I(
|
||||
saveData.TargetX,
|
||||
saveData.TargetY,
|
||||
saveData.TargetZ
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,21 @@ public class ForNode : ProgramNode
|
||||
return $"Name: {DisplayText}, AmountExecuted: {amountExecuted}, Amount: {amount}";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.Amount = amount;
|
||||
saveData.AmountExecuted = amountExecuted;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
amount = saveData.Amount;
|
||||
amountExecuted = saveData.AmountExecuted;
|
||||
}
|
||||
|
||||
public override void SetNextNode(
|
||||
List<Godot.Collections.Dictionary> connections,
|
||||
Dictionary<StringName, ProgramNode> availableNodes
|
||||
|
||||
@@ -60,6 +60,26 @@ public class IfNode : ProgramNode
|
||||
return $"Name: {DisplayText}, Item: {(selectedItem == null ? "Empty" : selectedItem.data.Id)}, Comparator: {comparator}, Amount: {amount}";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.ItemId = selectedItem?.data.Id;
|
||||
saveData.Comparator = comparator;
|
||||
saveData.Amount = amount;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
amount = saveData.Amount;
|
||||
comparator = saveData.Comparator;
|
||||
if (string.IsNullOrEmpty(saveData.ItemId)) return;
|
||||
if (!GameData.availableItems.ContainsKey(saveData.ItemId)) return;
|
||||
|
||||
selectedItem = new Item { data = GameData.availableItems[saveData.ItemId] };
|
||||
}
|
||||
|
||||
public override void SetNextNode(
|
||||
List<Godot.Collections.Dictionary> connections,
|
||||
Dictionary<StringName, ProgramNode> availableNodes
|
||||
|
||||
@@ -98,4 +98,26 @@ public class MoveNode : ProgramNode
|
||||
{
|
||||
return $"Name: {DisplayText}, Position: ({targetPosition.X}|{targetPosition.Y}|{targetPosition.Z})";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.TargetX = targetPosition.X;
|
||||
saveData.TargetY = targetPosition.Y;
|
||||
saveData.TargetZ = targetPosition.Z;
|
||||
saveData.HasTargetPosition = true;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
if (!saveData.HasTargetPosition) return;
|
||||
|
||||
targetPosition = new Vector3I(
|
||||
saveData.TargetX,
|
||||
saveData.TargetY,
|
||||
saveData.TargetZ
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,52 @@ public abstract class ProgramNode
|
||||
public abstract ProgramNode Duplicate();
|
||||
public abstract string Save();
|
||||
|
||||
public virtual NodeSaveData CreateSaveData()
|
||||
{
|
||||
return new NodeSaveData
|
||||
{
|
||||
NodeType = DisplayText,
|
||||
EditorNodeId = EditorNodeId,
|
||||
NextEditorNodeId = nextNode?.EditorNodeId,
|
||||
NegativeEditorNodeId = NegativeNode?.EditorNodeId,
|
||||
NodeContent = Save(),
|
||||
LastExecutionMessage = lastExecutionMessage
|
||||
};
|
||||
}
|
||||
|
||||
public virtual void Load(NodeSaveData saveData)
|
||||
{
|
||||
EditorNodeId = saveData.EditorNodeId;
|
||||
lastExecutionMessage = saveData.LastExecutionMessage;
|
||||
}
|
||||
|
||||
public void RestoreConnections(
|
||||
NodeSaveData saveData,
|
||||
Dictionary<string, ProgramNode> availableNodes
|
||||
)
|
||||
{
|
||||
nextNode = GetSavedConnection(saveData.NextEditorNodeId, availableNodes);
|
||||
NegativeNode = GetSavedConnection(saveData.NegativeEditorNodeId, availableNodes);
|
||||
}
|
||||
|
||||
public static ProgramNode CreateFromSaveData(NodeSaveData saveData)
|
||||
{
|
||||
return saveData.NodeType?.ToLower() switch
|
||||
{
|
||||
"start" => new StartNode(),
|
||||
"move" => new MoveNode(),
|
||||
"explore" => new ExploreNode(),
|
||||
"harvest" => new HarvestNode(),
|
||||
"craft" => new CraftNode(),
|
||||
"if" => new IfNode(),
|
||||
"while" => new WhileNode(),
|
||||
"for" => new ForNode(),
|
||||
"maintain" => new MaintainNode(),
|
||||
"sacrifice" => new SacrificeNode(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public ProgramNode DuplicateForRuntime(string editorNodeId)
|
||||
{
|
||||
ProgramNode duplicate = Duplicate();
|
||||
@@ -65,4 +111,15 @@ public abstract class ProgramNode
|
||||
|
||||
return availableNodes[nodeName];
|
||||
}
|
||||
|
||||
private ProgramNode GetSavedConnection(
|
||||
string nodeId,
|
||||
Dictionary<string, ProgramNode> availableNodes
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId)) return null;
|
||||
if (!availableNodes.ContainsKey(nodeId)) return null;
|
||||
|
||||
return availableNodes[nodeId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,26 @@ public class WhileNode : ProgramNode
|
||||
return $"Name: {DisplayText}, Item: {(selectedItem == null ? "Empty" : selectedItem.data.Id)}, Comparator: {comparator}, Amount: {amount}";
|
||||
}
|
||||
|
||||
public override NodeSaveData CreateSaveData()
|
||||
{
|
||||
NodeSaveData saveData = base.CreateSaveData();
|
||||
saveData.ItemId = selectedItem?.data.Id;
|
||||
saveData.Comparator = comparator;
|
||||
saveData.Amount = amount;
|
||||
return saveData;
|
||||
}
|
||||
|
||||
public override void Load(NodeSaveData saveData)
|
||||
{
|
||||
base.Load(saveData);
|
||||
amount = saveData.Amount;
|
||||
comparator = saveData.Comparator;
|
||||
if (string.IsNullOrEmpty(saveData.ItemId)) return;
|
||||
if (!GameData.availableItems.ContainsKey(saveData.ItemId)) return;
|
||||
|
||||
selectedItem = new Item { data = GameData.availableItems[saveData.ItemId] };
|
||||
}
|
||||
|
||||
public override void SetNextNode(
|
||||
List<Godot.Collections.Dictionary> connections,
|
||||
Dictionary<StringName, ProgramNode> availableNodes
|
||||
|
||||
@@ -23,6 +23,7 @@ public partial class Robot : Node3D
|
||||
public bool isBroken = false;
|
||||
public string robotType = "iron_robot";
|
||||
public bool showOnMap = true;
|
||||
public List<ProgramNode> nodes = new List<ProgramNode>();
|
||||
|
||||
private RobotTypeStats TypeStats =>
|
||||
GameData.robotStats.RobotTypes.TryGetValue(robotType, out RobotTypeStats stats)
|
||||
@@ -107,6 +108,7 @@ public partial class Robot : Node3D
|
||||
programStartNode = nodes[0];
|
||||
currentNode = nodes[0];
|
||||
currentMessage = "";
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public void StopExecution(string message)
|
||||
@@ -173,7 +175,11 @@ public partial class Robot : Node3D
|
||||
Heat = heat,
|
||||
Maintenance = maintenance,
|
||||
IsCoolingDown = isCoolingDown,
|
||||
IsBroken = isBroken
|
||||
IsBroken = isBroken,
|
||||
RunningProgramNodes = CreateProgramSaveData(),
|
||||
ProgramStartNode = programStartNode?.EditorNodeId,
|
||||
CurrentNode = currentNode?.EditorNodeId,
|
||||
IsExecuting = isExecuting
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +194,7 @@ public partial class Robot : Node3D
|
||||
maintenance = saveData.Maintenance;
|
||||
isCoolingDown = saveData.IsCoolingDown;
|
||||
isBroken = saveData.IsBroken;
|
||||
LoadProgramState(saveData);
|
||||
}
|
||||
|
||||
private bool CanExecute(double delta)
|
||||
@@ -283,4 +290,78 @@ public partial class Robot : Node3D
|
||||
isCoolingDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<NodeSaveData> CreateProgramSaveData()
|
||||
{
|
||||
List<NodeSaveData> result = new List<NodeSaveData>();
|
||||
if (nodes == null) return result;
|
||||
|
||||
foreach (ProgramNode node in nodes)
|
||||
{
|
||||
if (node == null) continue;
|
||||
result.Add(node.CreateSaveData());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void LoadProgramState(RobotSaveData saveData)
|
||||
{
|
||||
nodes = new List<ProgramNode>();
|
||||
programStartNode = null;
|
||||
currentNode = null;
|
||||
isExecuting = false;
|
||||
|
||||
if (saveData.RunningProgramNodes == null || saveData.RunningProgramNodes.Count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, ProgramNode> loadedNodes = new Dictionary<string, ProgramNode>();
|
||||
foreach (NodeSaveData nodeSaveData in saveData.RunningProgramNodes)
|
||||
{
|
||||
ProgramNode node = ProgramNode.CreateFromSaveData(nodeSaveData);
|
||||
if (node == null) continue;
|
||||
|
||||
node.Load(nodeSaveData);
|
||||
nodes.Add(node);
|
||||
|
||||
if (!string.IsNullOrEmpty(node.EditorNodeId) && !loadedNodes.ContainsKey(node.EditorNodeId))
|
||||
{
|
||||
loadedNodes.Add(node.EditorNodeId, node);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (NodeSaveData nodeSaveData in saveData.RunningProgramNodes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeSaveData.EditorNodeId)) continue;
|
||||
if (!loadedNodes.ContainsKey(nodeSaveData.EditorNodeId)) continue;
|
||||
|
||||
loadedNodes[nodeSaveData.EditorNodeId].RestoreConnections(nodeSaveData, loadedNodes);
|
||||
}
|
||||
|
||||
programStartNode = GetLoadedNode(saveData.ProgramStartNode, loadedNodes);
|
||||
if (programStartNode == null && nodes.Count > 0)
|
||||
{
|
||||
programStartNode = nodes[0];
|
||||
}
|
||||
|
||||
currentNode = GetLoadedNode(saveData.CurrentNode, loadedNodes);
|
||||
if (currentNode == null)
|
||||
{
|
||||
currentNode = programStartNode;
|
||||
}
|
||||
|
||||
isExecuting = saveData.IsExecuting && currentNode != null;
|
||||
}
|
||||
|
||||
private ProgramNode GetLoadedNode(
|
||||
string nodeId,
|
||||
Dictionary<string, ProgramNode> loadedNodes
|
||||
)
|
||||
{
|
||||
if (string.IsNullOrEmpty(nodeId)) return null;
|
||||
if (!loadedNodes.ContainsKey(nodeId)) return null;
|
||||
|
||||
return loadedNodes[nodeId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public partial class TestRunner : Node
|
||||
Run("Resource extraction and save data roundtrip", TestResourceSaveRoundtrip);
|
||||
Run("Endless resources extract slower", TestEndlessResourcesExtractSlower);
|
||||
Run("Robot save data roundtrip keeps robot state", TestRobotSaveRoundtrip);
|
||||
Run("Robot save data roundtrip keeps running program", TestRobotProgramSaveRoundtrip);
|
||||
Run("Maintain node consumes matching gear", TestMaintainNodeConsumesMatchingGear);
|
||||
Run("Sacrifice node makes resource endless", TestSacrificeNodeMakesResourceEndless);
|
||||
Run("No robot recovery detects loss", TestNoRobotRecoveryDetectsLoss);
|
||||
@@ -229,6 +230,66 @@ public partial class TestRunner : Node
|
||||
AssertTrue(loadedRobot.isCoolingDown, "robot cooling state");
|
||||
}
|
||||
|
||||
private void TestRobotProgramSaveRoundtrip()
|
||||
{
|
||||
StartNode startNode = new StartNode { EditorNodeId = "start" };
|
||||
ForNode forNode = new ForNode
|
||||
{
|
||||
EditorNodeId = "loop",
|
||||
amount = 3,
|
||||
amountExecuted = 2
|
||||
};
|
||||
CraftNode craftNode = new CraftNode
|
||||
{
|
||||
EditorNodeId = "craft",
|
||||
selectedItem = new Item
|
||||
{
|
||||
data = GameData.availableItems["stone"],
|
||||
elapsedCraftTime = 0.5,
|
||||
amountCrafted = 1
|
||||
},
|
||||
amount = 2
|
||||
};
|
||||
HarvestNode harvestNode = new HarvestNode { EditorNodeId = "done" };
|
||||
|
||||
startNode.nextNode = forNode;
|
||||
forNode.nextNode = craftNode;
|
||||
forNode.NegativeNode = harvestNode;
|
||||
craftNode.nextNode = forNode;
|
||||
|
||||
Robot robot = new Robot
|
||||
{
|
||||
Name = "Loop Bot",
|
||||
currentProgram = "Looping",
|
||||
isExecuting = true,
|
||||
programStartNode = startNode,
|
||||
currentNode = craftNode,
|
||||
nodes = new List<ProgramNode> { startNode, forNode, craftNode, harvestNode }
|
||||
};
|
||||
|
||||
RobotSaveData saveData = robot.CreateSaveData();
|
||||
Robot loadedRobot = new Robot();
|
||||
loadedRobot.LoadSaveData(saveData);
|
||||
|
||||
AssertTrue(loadedRobot.isExecuting, "loaded robot should still execute");
|
||||
AssertEqual("craft", loadedRobot.currentNode.EditorNodeId, "current node id");
|
||||
AssertEqual("start", loadedRobot.programStartNode.EditorNodeId, "start node id");
|
||||
AssertEqual(4, loadedRobot.nodes.Count, "loaded node count");
|
||||
|
||||
ForNode loadedForNode = loadedRobot.nodes[1] as ForNode;
|
||||
CraftNode loadedCraftNode = loadedRobot.nodes[2] as CraftNode;
|
||||
|
||||
AssertEqual(3, loadedForNode.amount, "for amount");
|
||||
AssertEqual(2, loadedForNode.amountExecuted, "for amount executed");
|
||||
AssertTrue(ReferenceEquals(loadedCraftNode, loadedForNode.nextNode), "for success branch");
|
||||
AssertTrue(ReferenceEquals(loadedRobot.nodes[3], loadedForNode.NegativeNode), "for negative branch");
|
||||
AssertTrue(ReferenceEquals(loadedForNode, loadedCraftNode.nextNode), "craft loops back");
|
||||
AssertEqual("stone", loadedCraftNode.selectedItem.data.Id, "craft item");
|
||||
AssertEqual(2, loadedCraftNode.amount, "craft amount");
|
||||
AssertClose(0.5f, (float)loadedCraftNode.selectedItem.elapsedCraftTime, 0.001f, "craft elapsed");
|
||||
AssertEqual(1, loadedCraftNode.selectedItem.amountCrafted, "craft crafted amount");
|
||||
}
|
||||
|
||||
private void TestMaintainNodeConsumesMatchingGear()
|
||||
{
|
||||
Robot robot = new Robot
|
||||
@@ -475,9 +536,9 @@ public partial class TestRunner : Node
|
||||
amount = 2
|
||||
};
|
||||
|
||||
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "first iteration");
|
||||
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "second iteration");
|
||||
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "loop finished");
|
||||
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "first iteration");
|
||||
AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "second iteration");
|
||||
AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "loop finished");
|
||||
}
|
||||
|
||||
private void TestRuntimeNodeKeepsEditorIdWithoutSharingState()
|
||||
|
||||
Reference in New Issue
Block a user