Olympe Engine 2.0
2D Game Engine with ECS Architecture
Loading...
Searching...
No Matches
BTGraphValidator.cpp
Go to the documentation of this file.
1/**
2 * @file BTGraphValidator.cpp
3 * @brief Implementation of BTGraphValidator
4 * @author Olympe Engine
5 * @date 2026-02-18
6 */
7
8#include "BTGraphValidator.h"
9#include "BTNodeRegistry.h"
10#include "../../system/system_utils.h"
11#include <set>
12#include <functional>
13
14namespace Olympe {
15namespace AI {
16
17std::vector<BTValidationMessage> BTGraphValidator::ValidateGraph(const NodeGraph::GraphDocument* graph) {
18 std::vector<BTValidationMessage> messages;
19
20 if (graph == nullptr) {
21 messages.push_back({
23 0,
24 "Graph is null",
25 "Create a valid graph document"
26 });
27 return messages;
28 }
29
30 // Execute all validation rules
36
37 return messages;
38}
39
40void BTGraphValidator::ValidateRootNode(const NodeGraph::GraphDocument* graph, std::vector<BTValidationMessage>& messages) {
41 // Count nodes without parent
42 int rootCount = 0;
44
45 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
46 const auto& node = *nodeIt;
47 bool hasParent = false;
48
49 // Check if this node is a child of any other node
50 for (auto otherIt = graph->GetNodes().begin(); otherIt != graph->GetNodes().end(); ++otherIt) {
51 const auto& otherNode = *otherIt;
52
53 // Check in children array
54 for (auto childIt = otherNode.children.begin(); childIt != otherNode.children.end(); ++childIt) {
55 if (childIt->value == node.id.value) {
56 hasParent = true;
57 break;
58 }
59 }
60
61 // Check decorator child
62 if (otherNode.decoratorChild.value == node.id.value) {
63 hasParent = true;
64 }
65
66 if (hasParent) break;
67 }
68
69 if (!hasParent) {
70 rootCount++;
71 lastRootId = node.id.value;
72 }
73 }
74
75 if (rootCount == 0) {
76 messages.push_back({
78 0,
79 "No root node found",
80 "Add a root node (Selector or Sequence)"
81 });
82 } else if (rootCount > 1) {
83 messages.push_back({
85 0,
86 "Multiple root nodes detected",
87 "Connect all nodes to a single root"
88 });
89 }
90}
91
92void BTGraphValidator::ValidateCycles(const NodeGraph::GraphDocument* graph, std::vector<BTValidationMessage>& messages) {
93 // DFS-based cycle detection
94 std::set<uint32_t> visited;
95 std::set<uint32_t> recursionStack;
96
97 // Recursive DFS function
98 std::function<bool(uint32_t)> hasCycleDFS = [&](uint32_t nodeId) -> bool {
99 visited.insert(nodeId);
100 recursionStack.insert(nodeId);
101
102 const auto* node = graph->GetNode(NodeGraph::NodeId{nodeId});
103 if (node != nullptr) {
104 // Check all children
105 for (auto childIt = node->children.begin(); childIt != node->children.end(); ++childIt) {
106 uint32_t childId = childIt->value;
107
108 if (visited.find(childId) == visited.end()) {
109 // Not visited yet, recurse
110 if (hasCycleDFS(childId)) {
111 return true;
112 }
113 } else if (recursionStack.find(childId) != recursionStack.end()) {
114 // Found a back edge (cycle)
115 messages.push_back({
117 nodeId,
118 "Cycle detected in graph",
119 "Remove circular connections"
120 });
121 return true;
122 }
123 }
124
125 // Check decorator child
126 if (node->decoratorChild.value != 0) {
127 uint32_t childId = node->decoratorChild.value;
128
129 if (visited.find(childId) == visited.end()) {
130 if (hasCycleDFS(childId)) {
131 return true;
132 }
133 } else if (recursionStack.find(childId) != recursionStack.end()) {
134 messages.push_back({
136 nodeId,
137 "Cycle detected in decorator chain",
138 "Remove circular connections"
139 });
140 return true;
141 }
142 }
143 }
144
145 recursionStack.erase(nodeId);
146 return false;
147 };
148
149 // Launch DFS from each unvisited node
150 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
151 const auto& node = *nodeIt;
152 if (visited.find(node.id.value) == visited.end()) {
153 hasCycleDFS(node.id.value);
154 }
155 }
156}
157
160
161 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
162 const auto& node = *nodeIt;
163 const BTNodeTypeInfo* typeInfo = registry.GetNodeTypeInfo(node.type);
164
165 if (typeInfo == nullptr) {
166 continue; // Type validation handled in ValidateNodeTypes
167 }
168
169 int childCount = static_cast<int>(node.children.size());
170
171 // Add decorator child to count if present
172 if (node.decoratorChild.value != 0) {
173 childCount++;
174 }
175
176 // Check minimum
177 if (typeInfo->minChildren >= 0 && childCount < typeInfo->minChildren) {
178 messages.push_back({
180 node.id.value,
181 "Too few children (" + std::to_string(childCount) + " < " + std::to_string(typeInfo->minChildren) + ")",
182 "Add at least " + std::to_string(typeInfo->minChildren - childCount) + " children"
183 });
184 }
185
186 // Check maximum
187 if (typeInfo->maxChildren >= 0 && childCount > typeInfo->maxChildren) {
188 messages.push_back({
190 node.id.value,
191 "Too many children (" + std::to_string(childCount) + " > " + std::to_string(typeInfo->maxChildren) + ")",
192 "Remove " + std::to_string(childCount - typeInfo->maxChildren) + " children"
193 });
194 }
195 }
196}
197
198void BTGraphValidator::ValidateOrphans(const NodeGraph::GraphDocument* graph, std::vector<BTValidationMessage>& messages) {
199 if (graph->GetNodes().empty()) {
200 return;
201 }
202
203 // Find the root node(s)
204 std::set<uint32_t> roots;
205 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
206 const auto& node = *nodeIt;
207 bool hasParent = false;
208
209 for (auto otherIt = graph->GetNodes().begin(); otherIt != graph->GetNodes().end(); ++otherIt) {
210 const auto& otherNode = *otherIt;
211
212 for (auto childIt = otherNode.children.begin(); childIt != otherNode.children.end(); ++childIt) {
213 if (childIt->value == node.id.value) {
214 hasParent = true;
215 break;
216 }
217 }
218
219 if (otherNode.decoratorChild.value == node.id.value) {
220 hasParent = true;
221 }
222
223 if (hasParent) break;
224 }
225
226 if (!hasParent) {
227 roots.insert(node.id.value);
228 }
229 }
230
231 if (roots.empty() || roots.size() > 1) {
232 return; // Root validation handles this
233 }
234
235 // BFS from root to find all reachable nodes
236 std::set<uint32_t> reachable;
237 std::vector<uint32_t> queue;
238
239 uint32_t rootId = *roots.begin();
240 queue.push_back(rootId);
241 reachable.insert(rootId);
242
243 while (!queue.empty()) {
244 uint32_t currentId = queue.front();
245 queue.erase(queue.begin());
246
247 const auto* node = graph->GetNode(NodeGraph::NodeId{currentId});
248 if (node != nullptr) {
249 // Add all children to queue
250 for (auto childIt = node->children.begin(); childIt != node->children.end(); ++childIt) {
251 uint32_t childId = childIt->value;
252 if (reachable.find(childId) == reachable.end()) {
253 reachable.insert(childId);
254 queue.push_back(childId);
255 }
256 }
257
258 // Add decorator child
259 if (node->decoratorChild.value != 0) {
260 uint32_t childId = node->decoratorChild.value;
261 if (reachable.find(childId) == reachable.end()) {
262 reachable.insert(childId);
263 queue.push_back(childId);
264 }
265 }
266 }
267 }
268
269 // Check for orphans
270 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
271 const auto& node = *nodeIt;
272 if (reachable.find(node.id.value) == reachable.end()) {
273 messages.push_back({
275 node.id.value,
276 "Orphan node detected (not connected to root)",
277 "Connect this node to the tree or delete it"
278 });
279 }
280 }
281}
282
283void BTGraphValidator::ValidateNodeTypes(const NodeGraph::GraphDocument* graph, std::vector<BTValidationMessage>& messages) {
285
286 for (auto nodeIt = graph->GetNodes().begin(); nodeIt != graph->GetNodes().end(); ++nodeIt) {
287 const auto& node = *nodeIt;
288
289 if (!registry.IsValidNodeType(node.type)) {
290 messages.push_back({
292 node.id.value,
293 "Unknown node type: " + node.type,
294 "Change to valid BT node type"
295 });
296 }
297 }
298}
299
300} // namespace AI
301} // namespace Olympe
Validation system for Behavior Tree graph structure.
Registry of all Behavior Tree node types for AIGraphPlugin_BT.
ComponentTypeID GetComponentTypeID_Static()
Definition ECS_Entity.h:56
static void ValidateNodeTypes(const NodeGraph::GraphDocument *graph, std::vector< BTValidationMessage > &messages)
Rule 5: Validate node types are registered.
static void ValidateRootNode(const NodeGraph::GraphDocument *graph, std::vector< BTValidationMessage > &messages)
Rule 1: Check for exactly one root node.
static void ValidateOrphans(const NodeGraph::GraphDocument *graph, std::vector< BTValidationMessage > &messages)
Rule 4: Check for orphan nodes (disconnected from root)
static std::vector< BTValidationMessage > ValidateGraph(const NodeGraph::GraphDocument *graph)
Validate a complete BT graph.
static void ValidateCycles(const NodeGraph::GraphDocument *graph, std::vector< BTValidationMessage > &messages)
Rule 2: Detect cycles in graph.
static void ValidateChildrenCount(const NodeGraph::GraphDocument *graph, std::vector< BTValidationMessage > &messages)
Rule 3: Validate child counts per node type.
static BTNodeRegistry & Get()
Get singleton instance.
Main document class for a node graph.
@ Warning
Warning (non-blocking)
@ Error
Error (blocking compilation)
< Provides AssetID and INVALID_ASSET_ID
Metadata for a behavior tree node type.