diff --git a/Forge.Tests/Statescript/GraphProcessorTests.cs b/Forge.Tests/Statescript/GraphProcessorTests.cs index 5b8487f..1ba8a04 100644 --- a/Forge.Tests/Statescript/GraphProcessorTests.cs +++ b/Forge.Tests/Statescript/GraphProcessorTests.cs @@ -320,6 +320,42 @@ public void Stopping_graph_fires_on_graph_completed_once() value.Should().Be(1); } + [Fact] + [Trait("Graph", "Lifecycle")] + public void Stopping_graph_resolves_property_inputs_during_deactivation_cascade() + { + var graph = new Graph(); + graph.VariableDefinitions.DefineVariable("duration", 100.0); + graph.VariableDefinitions.DefineProperty( + "constant", + new VariantResolver(new Variant128(42), typeof(int))); + + TimerNode timer = CreateTimerNode("duration"); + var readNode = new ReadPropertyNode(); + readNode.BindInput(ReadPropertyNode.ValueInput, "constant"); + + graph.AddNode(timer); + graph.AddNode(readNode); + graph.AddConnection(new Connection( + graph.EntryNode.OutputPorts[EntryNode.OutputPort], + timer.InputPorts[StateNode.InputPort])); + graph.AddConnection(new Connection( + timer.OutputPorts[StateNode.OnDeactivatePort], + readNode.InputPorts[ActionNode.InputPort])); + + var processor = new GraphProcessor(graph); + processor.StartGraph(); + + processor.GraphContext.IsActive.Should().BeTrue(); + + processor.StopGraph(); + + readNode.ExecutionCount.Should().Be(1); + readNode.Found.Should().BeTrue( + "property-backed inputs must still resolve during the StopGraph deactivation cascade"); + readNode.LastReadValue.Should().Be(42); + } + [Fact] [Trait("Graph", "Complex")] public void Complex_graph_with_condition_and_multiple_actions_executes_correctly() diff --git a/Forge/Statescript/GraphProcessor.cs b/Forge/Statescript/GraphProcessor.cs index 04f1dd2..dee4108 100644 --- a/Forge/Statescript/GraphProcessor.cs +++ b/Forge/Statescript/GraphProcessor.cs @@ -103,14 +103,17 @@ public void UpdateGraph(double deltaTime) /// public void StopGraph() { - if (GraphContext.Processor != this) + if (GraphContext.Processor != this || !GraphContext.HasStarted) { return; } - GraphContext.Processor = null; + // Clear HasStarted first so the disable cascade is re-entrancy safe (e.g. an ExitNode triggering StopGraph, or a + // state node reaching FinalizeGraph) without nulling Processor yet. Keeping Processor set throughout the cascade + // lets action nodes on OnDeactivate paths still resolve property-backed inputs. GraphContext.HasStarted = false; Graph.EntryNode.StopGraph(GraphContext); + GraphContext.Processor = null; GraphContext.ActiveStateNodes.Clear(); GraphContext.InternalNodeActivationStatus.Clear(); GraphContext.RemoveAllNodeContext();