Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
NodeValidator.cpp
Go to the documentation of this file.
1/**
2 * @file NodeValidator.cpp
3 * @brief NodeValidator implementation (Phase 9).
4 * @author Olympe Engine
5 * @date 2026-03-09
6 */
7
8#include "NodeValidator.h"
9
10#include <unordered_map>
11#include <unordered_set>
12
13namespace Olympe {
14
15// ============================================================================
16// Public — ValidateGraph
17// ============================================================================
18
19std::vector<ValidationMessage> NodeValidator::ValidateGraph(const TaskGraphTemplate* graph)
20{
21 std::vector<ValidationMessage> messages;
22
23 if (!graph)
24 {
26 "Graph pointer is null.",
27 "Ensure a valid graph is loaded before validation.");
28 return messages;
29 }
30
34
35 return messages;
36}
37
38// ============================================================================
39// Public — ValidateNode
40// ============================================================================
41
42std::vector<ValidationMessage> NodeValidator::ValidateNode(const TaskNodeDefinition* node)
43{
44 std::vector<ValidationMessage> messages;
45
46 if (!node)
47 {
49 "Node pointer is null.", "");
50 return messages;
51 }
52
53 // SubGraph nodes must have a non-empty SubGraphPath
54 if (node->Type == TaskNodeType::SubGraph && node->SubGraphPath.empty())
55 {
56 AddMessage(messages, static_cast<int>(node->NodeID), NVSeverity::Error,
57 "SubGraph node '" + node->NodeName + "' has no SubGraphPath.",
58 "Set the SubGraphPath field to a valid .json blueprint file.");
59 }
60
61 return messages;
62}
63
64// ============================================================================
65// Private — CheckUnconnectedNodes
66// ============================================================================
67
69 std::vector<ValidationMessage>& messages)
70{
71 // Build a set of source-node IDs that appear in ExecConnections
72 std::unordered_set<int32_t> connectedSources;
73 for (size_t i = 0; i < graph->ExecConnections.size(); ++i)
74 connectedSources.insert(graph->ExecConnections[i].SourceNodeID);
75
76 for (size_t i = 0; i < graph->Nodes.size(); ++i)
77 {
78 const TaskNodeDefinition& node = graph->Nodes[i];
79
80 // Skip entry-point and root node types — they have no output requirement
82 continue;
83
84 if (connectedSources.find(node.NodeID) == connectedSources.end())
85 {
86 AddMessage(messages, static_cast<int>(node.NodeID), NVSeverity::Warning,
87 "Node '" + node.NodeName + "' has no outgoing exec connection.",
88 "Connect the node's output pin or remove it if unused.");
89 }
90 }
91}
92
93// ============================================================================
94// Private — CheckMissingSubGraphPaths
95// ============================================================================
96
98 std::vector<ValidationMessage>& messages)
99{
100 for (size_t i = 0; i < graph->Nodes.size(); ++i)
101 {
102 const TaskNodeDefinition& node = graph->Nodes[i];
103
104 if (node.Type != TaskNodeType::SubGraph)
105 continue;
106
107 if (node.SubGraphPath.empty())
108 {
109 AddMessage(messages, static_cast<int>(node.NodeID), NVSeverity::Error,
110 "SubGraph node '" + node.NodeName + "' has an empty SubGraphPath.",
111 "Set the SubGraphPath field to a valid .json blueprint file.");
112 }
113 }
114}
115
116// ============================================================================
117// Private — CheckInfiniteLoops (DFS cycle detection)
118// ============================================================================
119
120namespace {
121
122/// DFS helper: returns true if a cycle is found starting from startId.
124 const std::unordered_map<int32_t, std::vector<int32_t>>& adj,
125 std::unordered_set<int32_t>& visited,
126 std::unordered_set<int32_t>& inStack)
127{
128 visited.insert(current);
129 inStack.insert(current);
130
131 auto it = adj.find(current);
132 if (it != adj.end())
133 {
134 const std::vector<int32_t>& neighbours = it->second;
135 for (size_t i = 0; i < neighbours.size(); ++i)
136 {
138 if (inStack.find(next) != inStack.end())
139 return true; // Back-edge = cycle
140 if (visited.find(next) == visited.end())
141 {
143 return true;
144 }
145 }
146 }
147
148 inStack.erase(current);
149 return false;
150}
151
152} // anonymous namespace
153
155 std::vector<ValidationMessage>& messages)
156{
157 // Build adjacency list from ExecConnections
158 std::unordered_map<int32_t, std::vector<int32_t>> adj;
159 for (size_t i = 0; i < graph->ExecConnections.size(); ++i)
160 {
161 const ExecPinConnection& conn = graph->ExecConnections[i];
162 adj[conn.SourceNodeID].push_back(conn.TargetNodeID);
163 }
164
165 std::unordered_set<int32_t> visited;
166 std::unordered_set<int32_t> inStack;
167
168 for (size_t i = 0; i < graph->Nodes.size(); ++i)
169 {
170 int32_t nid = graph->Nodes[i].NodeID;
171 if (visited.find(nid) == visited.end())
172 {
173 if (DFSHasCycle(nid, adj, visited, inStack))
174 {
175 AddMessage(messages, static_cast<int>(nid), NVSeverity::Error,
176 "Cycle detected in exec flow involving node '"
177 + graph->Nodes[i].NodeName + "'.",
178 "Break the circular connection to allow the graph to terminate.");
179 // Report once per cycle; a full cycle-member report would require
180 // additional path tracking and is deferred to future enhancements.
181 break;
182 }
183 }
184 }
185}
186
187// ============================================================================
188// Utility
189// ============================================================================
190
191void NodeValidator::AddMessage(std::vector<ValidationMessage>& messages,
192 int nodeId,
193 NVSeverity severity,
194 const std::string& msg,
195 const std::string& hint)
196{
198 m.nodeId = nodeId;
199 m.severity = severity;
200 m.message = msg;
201 m.hint = hint;
202 messages.push_back(m);
203}
204
205} // namespace Olympe
206
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
Real-time node and graph validation (Phase 9).
static void AddMessage(std::vector< ValidationMessage > &messages, int nodeId, NVSeverity severity, const std::string &msg, const std::string &hint="")
static void CheckUnconnectedNodes(const TaskGraphTemplate *graph, std::vector< ValidationMessage > &messages)
Flags non-EntryPoint/ExitPoint nodes with no outgoing exec connections.
static std::vector< ValidationMessage > ValidateNode(const TaskNodeDefinition *node)
Validates a single node definition.
static void CheckInfiniteLoops(const TaskGraphTemplate *graph, std::vector< ValidationMessage > &messages)
Flags cycles detected via DFS over ExecPinConnections.
static void CheckMissingSubGraphPaths(const TaskGraphTemplate *graph, std::vector< ValidationMessage > &messages)
Flags SubGraph nodes that have an empty SubGraphPath.
static std::vector< ValidationMessage > ValidateGraph(const TaskGraphTemplate *graph)
Validates every node in the graph and returns all findings.
Immutable, shareable task graph asset.
bool DFSHasCycle(int32_t current, const std::unordered_map< int32_t, std::vector< int32_t > > &adj, std::unordered_set< int32_t > &visited, std::unordered_set< int32_t > &inStack)
DFS helper: returns true if a cycle is found starting from startId.
< Provides AssetID and INVALID_ASSET_ID
NVSeverity
Indicates how serious a NodeValidator finding is.
@ Warning
May cause unexpected behaviour.
@ Error
Will prevent correct execution.
@ SubGraph
Sub-graph call (SubTask)
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
@ Root
Entry point of the graph (exactly one per template)
Explicit connection between a named exec-out pin of a source node and the exec-in pin of a target nod...
Full description of a single node in the task graph.
A single finding produced by NodeValidator.
int nodeId
Offending node ID; -1 for graph-level messages.