Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
GraphValidationPanel.cpp
Go to the documentation of this file.
1/**
2 * @file GraphValidationPanel.cpp
3 * @brief VS graph validation implementation (Phase 7).
4 * @author Olympe Engine
5 * @date 2026-03-10
6 */
7
9
10#include <unordered_map>
11#include <unordered_set>
12#include <vector>
13
14#include "../system/system_utils.h"
15
16namespace Olympe {
17
18// ============================================================================
19// Singleton
20// ============================================================================
21
23{
24 static GraphValidationPanel s_Instance;
25 return s_Instance;
26}
27
28// ============================================================================
29// Construction
30// ============================================================================
31
33 : m_SelectedNodeId(-1)
34{
35}
36
37// ============================================================================
38// Helpers
39// ============================================================================
40
42 const std::string& message,
43 ValidationSeverity severity)
44{
46 e.nodeId = nodeId;
47 e.message = message;
48 e.severity = severity;
49 m_Errors.push_back(e);
50}
51
52// ============================================================================
53// Validate
54// ============================================================================
55
57{
58 Clear();
59
60 if (graph.Nodes.empty())
61 {
62 AddError(-1, "Graph contains no nodes.", ValidationSeverity::Critical);
63 return;
64 }
65
69
70 SYSTEM_LOG << "[GraphValidationPanel] Validation complete: "
71 << static_cast<int>(m_Errors.size()) << " finding(s)."
72 << std::endl;
73}
74
75// ============================================================================
76// CheckDeadEnds
77// ============================================================================
78
80{
81 // Build set of node IDs that have at least one outgoing exec connection.
82 std::unordered_set<int> hasExecOut;
83 for (size_t i = 0; i < graph.ExecConnections.size(); ++i)
84 hasExecOut.insert(static_cast<int>(graph.ExecConnections[i].SourceNodeID));
85
86 // Pure data nodes are not expected to have exec outputs.
87 for (size_t i = 0; i < graph.Nodes.size(); ++i)
88 {
89 const TaskNodeDefinition& node = graph.Nodes[i];
90
93 || node.Type == TaskNodeType::MathOp);
94
95 if (isDataOnly)
96 continue;
97
98 if (hasExecOut.find(static_cast<int>(node.NodeID)) == hasExecOut.end())
99 {
100 // EntryPoint with no output is critical; others are errors.
104
105 AddError(static_cast<int>(node.NodeID),
106 "Node '" + node.NodeName + "' has no outgoing exec connection (dead end).",
107 sev);
108 }
109 }
110}
111
112// ============================================================================
113// CheckMissingConnections
114// ============================================================================
115
117{
118 for (size_t i = 0; i < graph.Nodes.size(); ++i)
119 {
120 const TaskNodeDefinition& node = graph.Nodes[i];
121
122 // SubGraph nodes must have a non-empty SubGraphPath.
123 if (node.Type == TaskNodeType::SubGraph && node.SubGraphPath.empty())
124 {
125 AddError(static_cast<int>(node.NodeID),
126 "SubGraph node '" + node.NodeName + "' has no SubGraphPath set.",
128 }
129 }
130
131 // Check that EntryPoint exists.
132 if (graph.EntryPointID == NODE_INDEX_NONE)
133 {
134 AddError(-1, "Graph has no EntryPoint node.", ValidationSeverity::Critical);
135 }
136}
137
138// ============================================================================
139// CheckCycles
140// ============================================================================
141
143{
144 // Build adjacency list: nodeId -> list of target nodeIds
145 std::unordered_map<int, std::vector<int>> adj;
146
147 for (size_t i = 0; i < graph.Nodes.size(); ++i)
148 adj[static_cast<int>(graph.Nodes[i].NodeID)] = std::vector<int>();
149
150 for (size_t i = 0; i < graph.ExecConnections.size(); ++i)
151 {
152 int src = static_cast<int>(graph.ExecConnections[i].SourceNodeID);
153 int dst = static_cast<int>(graph.ExecConnections[i].TargetNodeID);
154 adj[src].push_back(dst);
155 }
156
157 // Iterative DFS with colour marking:
158 // 0 = white (unvisited), 1 = grey (on stack), 2 = black (done)
159 std::unordered_map<int, int> colour;
160 for (size_t i = 0; i < graph.Nodes.size(); ++i)
161 colour[static_cast<int>(graph.Nodes[i].NodeID)] = 0;
162
163 std::unordered_set<int> cycleReported;
164
165 for (size_t i = 0; i < graph.Nodes.size(); ++i)
166 {
167 int startId = static_cast<int>(graph.Nodes[i].NodeID);
168
169 if (colour[startId] != 0)
170 continue;
171
172 // DFS stack: pair<nodeId, index into adj[nodeId]>
173 std::vector<std::pair<int,int>> stack;
174 stack.push_back(std::make_pair(startId, 0));
175 colour[startId] = 1;
176
177 while (!stack.empty())
178 {
179 std::pair<int,int>& top = stack.back();
180 int node = top.first;
181 int& idx = top.second;
182
183 const std::vector<int>& neighbours = adj[node];
184
185 if (idx < static_cast<int>(neighbours.size()))
186 {
187 int next = neighbours[idx];
188 ++idx;
189
190 if (colour.find(next) == colour.end())
191 continue; // unknown node — skip
192
193 if (colour[next] == 1)
194 {
195 // Back edge — cycle detected
196 if (cycleReported.find(next) == cycleReported.end())
197 {
198 cycleReported.insert(next);
200 "Cycle detected involving node ID " + std::to_string(next) + ".",
202 }
203 }
204 else if (colour[next] == 0)
205 {
206 colour[next] = 1;
207 stack.push_back(std::make_pair(next, 0));
208 }
209 }
210 else
211 {
212 colour[node] = 2;
213 stack.pop_back();
214 }
215 }
216 }
217}
218
219// ============================================================================
220// Accessors
221// ============================================================================
222
223const std::vector<GraphValidationError>& GraphValidationPanel::GetErrors() const
224{
225 return m_Errors;
226}
227
229{
230 return !m_Errors.empty();
231}
232
234{
235 for (size_t i = 0; i < m_Errors.size(); ++i)
236 {
237 if (m_Errors[i].severity == ValidationSeverity::Critical)
238 return true;
239 }
240 return false;
241}
242
244{
245 m_Errors.clear();
246 m_SelectedNodeId = -1;
247}
248
250{
251 m_SelectedNodeId = nodeId;
252}
253
258
259} // namespace Olympe
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
VS graph validation with clickable error list (Phase 7).
Singleton that validates a TaskGraphTemplate and tracks selected node.
void CheckCycles(const TaskGraphTemplate &graph)
Flags cycles detected by DFS over ExecConnections.
const std::vector< GraphValidationError > & GetErrors() const
Returns the current list of validation findings.
int GetSelectedNodeId() const
Returns the node ID set by the most recent OnErrorClick() call (-1 if none).
void Validate(const TaskGraphTemplate &graph)
Clears previous results and validates graph.
bool HasErrors() const
Returns true if there are any findings of any severity.
void CheckMissingConnections(const TaskGraphTemplate &graph)
Flags SubGraph nodes that have no SubGraphPath set.
void AddError(int nodeId, const std::string &message, ValidationSeverity severity)
void CheckDeadEnds(const TaskGraphTemplate &graph)
Flags nodes whose exec-output pins have no outgoing connection.
bool HasCriticalErrors() const
Returns true if any finding has severity Critical.
std::vector< GraphValidationError > m_Errors
static GraphValidationPanel & Get()
Returns the single shared instance.
void Clear()
Clears all findings and resets the selected node.
void OnErrorClick(int nodeId)
Records nodeId as the currently selected node.
Immutable, shareable task graph asset.
< Provides AssetID and INVALID_ASSET_ID
ValidationSeverity
Indicates how serious a validation finding is.
@ Critical
Graph cannot be executed.
@ Error
Likely to cause incorrect behaviour.
@ SubGraph
Sub-graph call (SubTask)
@ GetBBValue
Data node – reads a Blackboard key.
@ MathOp
Data node – arithmetic operation (+, -, *, /)
@ SetBBValue
Data node – writes a Blackboard key.
@ EntryPoint
Unique entry node for VS graphs (replaces Root)
constexpr int32_t NODE_INDEX_NONE
Sentinel value for "no node" in node index / ID fields.
A single validation finding.
int nodeId
Offending node ID; -1 for graph-level errors.
Full description of a single node in the task graph.
#define SYSTEM_LOG