Ember
Loading...
Searching...
No Matches
BehaviorTreeValidator.cpp
Go to the documentation of this file.
2#include "Core/Node.h"
3#include "Utils/Logger.h"
4#include <algorithm>
5
6namespace EmberCore {
7
9 // Use default ParserConfig constructor which provides sensible defaults
10}
11
13
15 ValidationResult result;
16
17 if (!tree) {
18 result.AddError("Tree is null");
19 return result;
20 }
21
22 // Validate tree structure (including cycle detection)
23 ValidateTreeStructure(tree, result);
24
25 // Only proceed with node validation if no cycles detected
26 // (cycles would cause infinite recursion in ValidateNode)
27 bool has_cycles = std::any_of(result.issues.begin(), result.issues.end(),
29 return issue.message.find("circular") != EmberCore::String::npos ||
30 issue.message.find("cycle") != EmberCore::String::npos;
31 });
32
33 if (!has_cycles) {
34 // Validate root node and its descendants
35 Node *root = tree->GetRootNode();
36 if (root) {
37 std::vector<const Node *> visited;
38 ValidateNode(root, result, visited, "");
39 }
40 }
41
42 // Validate blackboards
43 ValidateBlackboards(tree, result);
44
45 return result;
46}
47
49 const ParserConfig &config) {
50 BehaviorTreeValidator validator(config);
51 return validator.Validate(tree);
52}
53
55 // Check for root node
56 Node *root = tree->GetRootNode();
57 if (!root) {
58 result.AddError("Tree has no root node");
59 return;
60 }
61
62 // Check for cycles with protection against infinite recursion
63 try {
64 std::vector<const Node *> visited;
65 visited.reserve(100); // Pre-allocate to reduce reallocation overhead
66 if (HasCycles(root, visited)) {
67 result.AddError("Tree contains circular references (cycles)");
68 }
69 } catch (const std::exception &e) {
70 result.AddError(EmberCore::String("Cycle detection failed: ") + e.what());
71 } catch (...) {
72 result.AddError("Cycle detection failed with unknown error (possible circular structure)");
73 }
74}
75
76void BehaviorTreeValidator::ValidateNode(const Node *node, ValidationResult &result, std::vector<const Node *> &visited,
77 const EmberCore::String &parent_path) const {
78 if (!node) {
79 result.AddError("Null node found in tree", parent_path);
80 return;
81 }
82
83 // Build path for this node
84 EmberCore::String node_path = BuildNodePath(parent_path, node);
85
86 // Check for circular reference
87 if (std::find(visited.begin(), visited.end(), node) != visited.end()) {
88 result.AddError("Circular reference detected", node_path);
89 return;
90 }
91 visited.push_back(node);
92
93 // Validate node name
94 if (node->GetName().empty()) {
95 result.AddError("Node has empty name", node_path);
96 }
97
98 // Validate node type against profile
99 ValidateNodeType(node, result, node_path);
100
101 // Validate node attributes against profile
102 ValidateNodeAttributes(node, result, node_path);
103
104 // Validate child count rules
105 ValidateChildCount(node, result, node_path);
106
107 // Recursively validate children
108 for (size_t i = 0; i < node->GetChildCount(); ++i) {
109 const Node *child = node->GetChild(i);
110 if (child) {
111 ValidateNode(child, result, visited, node_path);
112 } else {
113 result.AddError("Node has null child at index " + std::to_string(i), node_path);
114 }
115 }
116}
117
119 const EmberCore::String &path) const {
120 // Get the node's ID attribute (which contains the specific type like "Sequence", "SetInt")
121 const auto &node_config = config_.GetNodeConfig();
122 EmberCore::String node_type_id = node->GetAttribute(node_config.node_id_attribute);
123
124 // If no ID attribute, this is a problem (needed for serialization)
125 if (node_type_id.empty()) {
126 result.AddWarning("Node missing '" + node_config.node_id_attribute + "' attribute (required for serialization)",
127 path);
128 return;
129 }
130
131 // Check if the node type is registered in the profile
132 bool type_registered = false;
133 switch (node->GetType()) {
135 type_registered = config_.IsControlType(node_type_id);
136 if (!type_registered) {
137 result.AddError("Control node type '" + node_type_id + "' not registered in profile", path);
138 }
139 break;
140
142 type_registered = config_.IsActionType(node_type_id);
143 // Actions might have empty type lists (accept all)
144 if (!type_registered && !config_.GetClassificationConfig().action_types.empty()) {
145 result.AddError("Action node type '" + node_type_id + "' not registered in profile", path);
146 }
147 break;
148
150 type_registered = config_.IsConditionType(node_type_id);
151 if (!type_registered && !config_.GetClassificationConfig().condition_types.empty()) {
152 result.AddError("Condition node type '" + node_type_id + "' not registered in profile", path);
153 }
154 break;
155
157 type_registered = config_.IsDecoratorType(node_type_id);
158 if (!type_registered) {
159 result.AddError("Decorator node type '" + node_type_id + "' not registered in profile", path);
160 }
161 break;
162
163 default:
164 result.AddWarning("Node has unknown type enum value", path);
165 break;
166 }
167}
168
170 const EmberCore::String &path) const {
171 const auto &node_config = config_.GetNodeConfig();
172
173 // Check for required ID attribute
174 if (!node_config.node_id_attribute.empty()) {
175 EmberCore::String id = node->GetAttribute(node_config.node_id_attribute);
176 if (id.empty()) {
177 result.AddWarning("Node missing required '" + node_config.node_id_attribute + "' attribute", path);
178 }
179 }
180
181 // Check for internal placeholder attributes (should not exist in finalized trees)
182 if (node->HasAttribute("__is_placeholder__")) {
183 result.AddError("Node has unexpanded SubTree placeholder", path);
184 }
185
186 // Check for unimplemented SubTree references (warning, not error)
187 // These are SubTree nodes that reference trees not found in the parsed files
188 if (node->HasAttribute("__unimplemented__")) {
189 EmberCore::String subtree_ref = node->GetAttribute("__subtree_ref__", "");
190 if (!subtree_ref.empty()) {
191 result.AddWarning("SubTree '" + subtree_ref + "' is referenced but not implemented", path);
192 } else {
193 result.AddWarning("Node references an unimplemented tree", path);
194 }
195 }
196
197 // Check for SubTree references and validate they are resolved
198 EmberCore::String subtree_ref = node->GetAttribute("__subtree_ref__", "");
199 if (!subtree_ref.empty() && !node->HasAttribute("__unimplemented__")) {
200 // SubTree reference exists but not marked as unimplemented - it should have been expanded
201 // This is just informational, not an error, as the tree might be deliberately unexpanded
202 // for preview purposes
203 }
204}
205
207 const EmberCore::String &path) const {
208 size_t child_count = node->GetChildCount();
209
210 switch (node->GetType()) {
212 if (child_count == 0) {
213 result.AddError("Decorator node has no children (must have exactly 1)", path);
214 } else if (child_count > 1) {
215 result.AddWarning("Decorator node has " + std::to_string(child_count) + " children (should have exactly 1)",
216 path);
217 }
218 break;
219
221 if (child_count == 0) {
222 result.AddWarning("Control node has no children", path);
223 }
224 break;
225
228 if (child_count > 0) {
229 result.AddWarning("Leaf node (" + std::to_string(static_cast<int>(node->GetType())) + ") has " +
230 std::to_string(child_count) + " children (leaf nodes typically have no children)",
231 path);
232 }
233 break;
234
235 default:
236 break;
237 }
238}
239
241 const auto &blackboards = tree->GetBlackboards();
242
243 for (const auto &bb_pair : blackboards) {
244 const auto &bb_id = bb_pair.first;
245 const auto *blackboard = bb_pair.second.get();
246
247 if (!blackboard) {
248 result.AddError("Blackboard '" + bb_id + "' is null");
249 continue;
250 }
251
252 // Validate blackboard has an ID
253 if (bb_id.empty()) {
254 result.AddWarning("Blackboard has empty ID");
255 }
256
257 // Note: Detailed blackboard entry validation would require access to Blackboard internals
258 // For now, just check that the blackboard exists
259 }
260}
261
263 EmberCore::String node_name = node->GetName();
264 if (node_name.empty()) {
265 node_name = "[unnamed]";
266 }
267
268 if (parent_path.empty()) {
269 return node_name;
270 }
271 return parent_path + "/" + node_name;
272}
273
274bool BehaviorTreeValidator::HasCycles(const Node *node, std::vector<const Node *> &visited) const {
275 if (!node)
276 return false;
277
278 // Safety check: limit max recursion depth to prevent stack overflow
279 constexpr size_t MAX_DEPTH = 500;
280 if (visited.size() >= MAX_DEPTH) {
281 LOG_ERROR("BehaviorTreeValidator", "Max recursion depth exceeded - likely circular structure");
282 return true; // Treat as cycle to prevent stack overflow
283 }
284
285 // Check if this node is already in the visited stack (cycle detection)
286 // Use pointer comparison for exact identity check
287 if (std::any_of(visited.begin(), visited.end(),
288 [node](const Node *visited_node) { return visited_node == node; })) {
289 return true; // Cycle detected
290 }
291
292 visited.push_back(node);
293
294 // Check all children with nullptr protection
295 size_t child_count = 0;
296 try {
297 child_count = node->GetChildCount();
298 } catch (...) {
299 LOG_ERROR("BehaviorTreeValidator", "Failed to get child count - possibly corrupted node");
300 visited.pop_back();
301 return true;
302 }
303
304 for (size_t i = 0; i < child_count; ++i) {
305 const Node *child = nullptr;
306 try {
307 child = node->GetChild(i);
308 } catch (...) {
309 LOG_ERROR("BehaviorTreeValidator", "Failed to get child - possibly corrupted tree");
310 visited.pop_back();
311 return true;
312 }
313
314 if (child && HasCycles(child, visited)) {
315 visited.pop_back();
316 return true;
317 }
318 }
319
320 visited.pop_back();
321 return false;
322}
323
324} // namespace EmberCore
#define LOG_ERROR(category, message)
Definition Logger.h:116
ValidationResult Validate(const BehaviorTree *tree) const
Validate a behavior tree against the parser profile.
EmberCore::String BuildNodePath(const EmberCore::String &parent_path, const Node *node) const
void ValidateTreeStructure(const BehaviorTree *tree, ValidationResult &result) const
static ValidationResult ValidateWithConfig(const BehaviorTree *tree, const ParserConfig &config)
Validate a behavior tree with profile override.
void ValidateNodeType(const Node *node, ValidationResult &result, const EmberCore::String &path) const
bool HasCycles(const Node *node, std::vector< const Node * > &visited) const
void ValidateNodeAttributes(const Node *node, ValidationResult &result, const EmberCore::String &path) const
void ValidateNode(const Node *node, ValidationResult &result, std::vector< const Node * > &visited, const EmberCore::String &path) const
void ValidateBlackboards(const BehaviorTree *tree, ValidationResult &result) const
void ValidateChildCount(const Node *node, ValidationResult &result, const EmberCore::String &path) const
Represents a complete behavior tree data structure.
Node * GetRootNode() const
const std::map< EmberCore::String, std::unique_ptr< Blackboard > > & GetBlackboards() const
Represents a node in a behavior tree structure.
Definition Node.h:20
bool HasAttribute(const String &name) const
Definition Node.cpp:456
String GetAttribute(const String &name, const String &default_value="") const
Definition Node.cpp:451
Node * GetChild(size_t index) const
Definition Node.cpp:175
Type GetType() const
Definition Node.h:84
const String & GetName() const
Definition Node.h:81
size_t GetChildCount() const
Definition Node.h:76
Configuration for XML parser behavior and element/attribute mappings.
Main types header for EmberCore.
std::string String
Framework-agnostic string type.
Definition String.h:14
void AddError(const EmberCore::String &message, const EmberCore::String &node_path="")
void AddWarning(const EmberCore::String &message, const EmberCore::String &node_path="")