Ember
Loading...
Searching...
No Matches
LibXMLBehaviorTreeParser.cpp
Go to the documentation of this file.
2#include "Core/BehaviorTree.h"
5#include "Core/Node.h"
6#include "Utils/Logger.h"
7#include <algorithm>
8#include <fstream>
9#include <mutex>
10
11namespace EmberCore {
12
13// Static initialization
17
19 : config_(ParserConfig::CreateDefault()), progress_callback_(nullptr), project_parsing_mode_(false),
20 max_threads_(std::thread::hardware_concurrency()), thread_safe_(true), libxml2_initialized_(false) {
22}
23
25 : config_(config), progress_callback_(nullptr), project_parsing_mode_(false),
26 max_threads_(std::thread::hardware_concurrency()), thread_safe_(true), libxml2_initialized_(false) {
28}
29
31 // Serialize destruction to prevent libxml2 race conditions
32 // This mutex ensures only one parser destructor runs at a time
33 std::lock_guard<std::mutex> lock(destruction_mutex_);
34
35 // Clear internal state that may reference libxml2 memory
36 parsed_trees_.clear();
37 parsed_blackboards_.clear();
39 expansion_stack_.clear();
40
41 // Don't call xmlCleanupParser() here - it's a global resource
42 // libxml2 cleanup should only happen at application shutdown
43}
44
45// Simple error suppression handler (thread-safe)
46static void SuppressLibXMLErrors(void *ctx, const char *msg, ...) {
47 // Suppress all libxml2 error output to avoid thread-safety issues
48 // Errors will be captured via structured error callbacks per-document
49}
50
52 std::call_once(libxml2_init_flag_, [this]() {
53 // Initialize libxml2 with threading support
54 // NOTE: In tests, this is also called in main() before threads for extra safety
55 xmlInitParser();
56 LIBXML_TEST_VERSION
57
58 // Enable threading support
59 xmlInitThreads();
60 thread_safe_ = true;
61 LOG_INFO("LibXMLParser", "libxml2 threading support enabled");
62
63 // Suppress default libxml2 error output (errors will be captured per-document)
64 // This avoids thread-safety issues with global error handlers
65 xmlSetGenericErrorFunc(nullptr, SuppressLibXMLErrors);
66
69 });
70}
71
72std::shared_ptr<BehaviorTree> LibXMLBehaviorTreeParser::ParseFromFile(const EmberCore::String &filepath) {
74 parsed_trees_.clear();
75 parsed_blackboards_.clear();
76 current_file_path_ = filepath;
77
78 // Report progress: Starting
79 if (!ReportProgress("Opening XML file...", 0, 5)) {
80 return nullptr; // User cancelled
81 }
82
83 // Check if file exists
84 std::ifstream file(filepath);
85 if (!file.good()) {
86 AddError(ParseError::FILE_NOT_FOUND, EmberCore::String("File not found: ") + filepath);
87 ReportProgress("ERROR: File not found", 0, 5);
88 return nullptr;
89 }
90
91 // Get file size for logging
92 file.seekg(0, std::ios::end);
93 std::streampos fileSize = file.tellg();
94 file.seekg(0, std::ios::beg);
95
96 EmberCore::String sizeStr;
97 if (fileSize < 1024) {
98 sizeStr = std::to_string(fileSize) + " bytes";
99 } else if (fileSize < 1024 * 1024) {
100 sizeStr = std::to_string(static_cast<double>(fileSize) / 1024.0) + " KB";
101 } else {
102 sizeStr = std::to_string(static_cast<double>(fileSize) / (1024.0 * 1024.0)) + " MB";
103 }
104 ReportProgress("File size: " + sizeStr + "", 0, 5);
105
106 // Report progress: Loading XML
107 if (!ReportProgress("Loading XML document...", 1, 5)) {
108 return nullptr; // User cancelled
109 }
110
111 // Load XML document with libxml2
112 xmlDocPtr doc = xmlParseFile(filepath.c_str());
113 if (!doc) {
114 AddError(ParseError::XML_SYNTAX_ERROR, EmberCore::String("Failed to parse XML file: ") + filepath);
115 ReportProgress("ERROR: XML syntax error", 1, 5);
116 return nullptr;
117 }
118
119 ReportProgress("XML document loaded successfully", 1, 5);
120 LOG_INFO("LibXMLParser", EmberCore::String("Loading: ") + filepath);
121
122 // Report progress: Parsing trees
123 if (!ReportProgress("Parsing behavior trees...", 2, 5)) {
124 xmlFreeDoc(doc);
125 return nullptr; // User cancelled
126 }
127
128 // Parse the document
129 bool success = ParseXMLDocument(doc, filepath);
130 xmlFreeDoc(doc);
131
132 if (!success) {
133 ReportProgress("ERROR: Failed to parse document structure", 2, 5);
134 return nullptr;
135 }
136
137 // Report progress: Finalizing
138 if (!ReportProgress("Finalizing...", 4, 5)) {
139 return nullptr; // User cancelled
140 }
141
142 ReportProgress("Parsed " + std::to_string(parsed_trees_.size()) + " tree(s) successfully", 4, 5);
143
144 // Return the main tree if specified
145 if (!main_tree_name_.empty() && parsed_trees_.count(main_tree_name_)) {
146 auto main_tree = parsed_trees_[main_tree_name_];
147
148 // Validate the tree structure using unified validator
149 if (!ReportProgress("Validating tree structure...", 4, 5)) {
150 return nullptr; // User cancelled
151 }
152
154 auto validation_result = validator.Validate(main_tree.get());
155
156 // Report validation errors (these block loading)
157 if (validation_result.HasErrors()) {
158 LOG_ERROR("LibXMLParser",
159 "Tree validation failed with " + std::to_string(validation_result.ErrorCount()) + " error(s):");
160
161 for (const auto &issue : validation_result.issues) {
162 if (issue.severity == BehaviorTreeValidator::Severity::ERROR) {
164 LOG_ERROR("LibXMLParser", " [" + issue.node_path + "] " + issue.message);
165 }
166 }
167
168 ReportProgress("ERROR: Tree validation failed", 4, 5);
169 return nullptr; // Errors block loading
170 }
171
172 // Report validation warnings (these don't block loading)
173 if (validation_result.HasWarnings()) {
174 LOG_WARNING("LibXMLParser",
175 "Tree has " + std::to_string(validation_result.WarningCount()) + " warning(s):");
176
177 for (const auto &issue : validation_result.issues) {
178 if (issue.severity == BehaviorTreeValidator::Severity::WARNING) {
179 LOG_WARNING("LibXMLParser", " [" + issue.node_path + "] " + issue.message);
180 }
181 }
182 }
183
184 ReportProgress("Tree validation passed", 4, 5);
185 return main_tree;
186 }
187
188 // Return the first tree if no main tree specified
189 if (!parsed_trees_.empty()) {
190 auto first_tree = parsed_trees_.begin()->second;
191
192 // Validate the tree structure
194 auto validation_result = validator.Validate(first_tree.get());
195
196 if (validation_result.HasErrors()) {
197 for (const auto &issue : validation_result.issues) {
198 if (issue.severity == BehaviorTreeValidator::Severity::ERROR) {
200 }
201 }
202 return nullptr; // Errors block loading
203 }
204
205 return first_tree;
206 }
207
208 AddError(ParseError::MISSING_BEHAVIOR_TREE, "No behavior trees found in XML file");
209 return nullptr;
210}
211
212std::shared_ptr<BehaviorTree> LibXMLBehaviorTreeParser::ParseFromString(const EmberCore::String &xml_content) {
213 ClearErrors();
214 parsed_trees_.clear();
215 parsed_blackboards_.clear();
216 current_file_path_ = "<string>";
217
218 // Load XML from string with libxml2
219 xmlDocPtr doc = xmlParseMemory(xml_content.c_str(), xml_content.length());
220 if (!doc) {
221 AddError(ParseError::XML_SYNTAX_ERROR, "Failed to parse XML content");
222 return nullptr;
223 }
224
225 // Parse the document
226 if (!ParseXMLDocument(doc)) {
227 xmlFreeDoc(doc);
228 return nullptr;
229 }
230
231 xmlFreeDoc(doc);
232
233 // Return the main tree if specified
234 if (!main_tree_name_.empty() && parsed_trees_.count(main_tree_name_)) {
235 auto main_tree = parsed_trees_[main_tree_name_];
236
237 // Validate the tree structure
239 auto validation_result = validator.Validate(main_tree.get());
240
241 if (validation_result.HasErrors()) {
242 for (const auto &issue : validation_result.issues) {
243 if (issue.severity == BehaviorTreeValidator::Severity::ERROR) {
245 }
246 }
247 return nullptr; // Errors block loading
248 }
249
250 return main_tree;
251 }
252
253 // Return the first tree if no main tree specified
254 if (!parsed_trees_.empty()) {
255 auto first_tree = parsed_trees_.begin()->second;
256
257 // Validate the tree structure
259 auto validation_result = validator.Validate(first_tree.get());
260
261 if (validation_result.HasErrors()) {
262 for (const auto &issue : validation_result.issues) {
263 if (issue.severity == BehaviorTreeValidator::Severity::ERROR) {
265 }
266 }
267 return nullptr; // Errors block loading
268 }
269
270 return first_tree;
271 }
272
273 AddError(ParseError::MISSING_BEHAVIOR_TREE, "No behavior trees found in XML content");
274 return nullptr;
275}
276
277std::vector<LibXMLBehaviorTreeParser::ParseResult>
278LibXMLBehaviorTreeParser::ParseMultipleFiles(const std::vector<EmberCore::String> &filepaths) {
279
280 std::vector<ParseResult> results;
281 results.reserve(filepaths.size());
282
283 for (const auto &filepath : filepaths) {
284 ParseResult result;
285 result.file_path = filepath;
286
287 try {
288 result.tree = ParseFromFile(filepath);
289 result.success = (result.tree != nullptr);
290 if (!result.success) {
291 std::lock_guard<std::mutex> lock(errors_mutex_);
292 result.errors = errors_;
293 }
294 } catch (const std::exception &e) {
295 result.success = false;
296 result.errors.push_back(
297 ParseError(ParseError::THREAD_ERROR, "Exception parsing " + filepath + ": " + std::string(e.what())));
298 }
299
300 results.push_back(result);
301 }
302
303 return results;
304}
305
306// ============================================================================
307// Project Parsing Methods
308// ============================================================================
309
311 int count = 0;
312 for (const auto &pair : tree_statuses) {
313 if (pair.second.is_implemented) {
314 count++;
315 }
316 }
317 return count;
318}
319
321 return static_cast<int>(tree_statuses.size());
322}
323
325 if (!project) {
326 ProjectParseResult result;
327 result.errors.push_back(ParseError(ParseError::INVALID_VALUE, "Project is null"));
328 return result;
329 }
330
331 // Get absolute paths for all resources
332 std::vector<EmberCore::String> filepaths;
333 for (const auto &resource : project->GetResources()) {
334 filepaths.push_back(project->ResolveResourcePath(resource));
335 }
336
337 // Parse all files with shared registry
339
340 // Update project with tree implementation statuses
341 if (result.success) {
343 }
344
345 return result;
346}
347
349LibXMLBehaviorTreeParser::ParseFilesWithSharedRegistry(const std::vector<EmberCore::String> &filepaths) {
350
351 ProjectParseResult result;
352 result.success = true;
353
354 // Clear existing state
355 ClearErrors();
356 parsed_trees_.clear();
357 parsed_blackboards_.clear();
360 project_parsing_mode_ = true; // Enable project parsing mode
361
362 int total_files = static_cast<int>(filepaths.size());
363 int current_file = 0;
364
365 ReportProgress("Starting project parsing...", 0, total_files + 2);
366
367 std::vector<EmberCore::String> main_tree_candidates;
368
369 // First pass: Parse all files to collect all trees
370 for (const auto &filepath : filepaths) {
371 current_file++;
372 current_file_path_ = filepath;
373
374 ReportProgress("Parsing file " + std::to_string(current_file) + "/" + std::to_string(total_files) + ": " +
375 filepath,
376 current_file, total_files + 2);
377
378 // Check if file exists
379 std::ifstream file(filepath);
380 if (!file.good()) {
381 // Create FileParseInfo even for failed files
382 FileParseInfo failedInfo;
383 failedInfo.filepath = filepath;
384 failedInfo.parsed_successfully = false;
385 failedInfo.errors.push_back("File not found: " + filepath);
386 result.file_infos.push_back(failedInfo);
387
388 result.errors.push_back(
389 ParseError(ParseError::FILE_NOT_FOUND, EmberCore::String("File not found: ") + filepath, filepath));
390 result.success = false;
391 continue;
392 }
393 file.close();
394
395 // Parse the XML file
396 xmlDocPtr doc = xmlParseFile(filepath.c_str());
397 if (!doc) {
398 // Create FileParseInfo even for failed files
399 FileParseInfo failedInfo;
400 failedInfo.filepath = filepath;
401 failedInfo.parsed_successfully = false;
402 failedInfo.errors.push_back("Failed to parse XML file - syntax error");
403 result.file_infos.push_back(failedInfo);
404
406 EmberCore::String("Failed to parse XML file: ") + filepath, filepath));
407 result.success = false;
408 continue;
409 }
410
411 // Parse document but don't expand subtrees yet
412 // (we'll do that after all files are parsed)
413 xmlNodePtr root = xmlDocGetRootElement(doc);
414 if (!root) {
415 // Create FileParseInfo even for failed files
416 FileParseInfo failedInfo;
417 failedInfo.filepath = filepath;
418 failedInfo.parsed_successfully = false;
419 failedInfo.errors.push_back("XML document has no root element");
420 result.file_infos.push_back(failedInfo);
421
422 xmlFreeDoc(doc);
423 result.errors.push_back(
424 ParseError(ParseError::MISSING_ROOT_ELEMENT, "XML document has no root element", filepath));
425 result.success = false;
426 continue;
427 }
428
429 // Get configured element names
430 const auto &doc_config = config_.GetDocumentConfig();
431 const auto &tree_config = config_.GetTreeConfig();
432
433 // Validate root element
434 EmberCore::String root_name = GetNodeName(root);
435 bool root_valid = false;
436 if (doc_config.case_sensitive) {
437 root_valid = (root_name == doc_config.root_element);
438 } else {
439 auto to_lower = [](String str) {
440 std::transform(str.begin(), str.end(), str.begin(), ::tolower);
441 return str;
442 };
443 root_valid = (to_lower(root_name) == to_lower(doc_config.root_element));
444 }
445
446 if (!root_valid) {
447 // Create FileParseInfo even for failed files
448 FileParseInfo failedInfo;
449 failedInfo.filepath = filepath;
450 failedInfo.parsed_successfully = false;
451 failedInfo.errors.push_back("Expected '" + doc_config.root_element + "' element, found '" + root_name +
452 "'");
453 result.file_infos.push_back(failedInfo);
454
455 xmlFreeDoc(doc);
457 EmberCore::String("Expected '") + doc_config.root_element +
458 "' element, found '" + root_name + "'",
459 filepath));
460 result.success = false;
461 continue;
462 }
463
464 // Collect main_tree_to_execute candidate from this file
465 {
466 EmberCore::String main_tree = GetNodeAttribute(root, doc_config.main_tree_attribute);
467 if (!main_tree.empty()) {
468 main_tree_candidates.push_back(main_tree);
469 }
470 }
471
472 // Create file info for detailed validation
473 FileParseInfo fileInfo;
474 fileInfo.filepath = filepath;
475 fileInfo.parsed_successfully = true;
476
477 std::set<String> file_tree_ids;
478 std::set<String> file_bb_ids;
479 std::set<String> subtree_refs_set;
480 std::set<String> bb_includes_set;
481 std::map<String, std::vector<String>> per_bb_includes;
482
483 // Parse all BehaviorTree and Blackboard nodes in this file
484 for (xmlNodePtr child = root->children; child; child = child->next) {
485 if (child->type == XML_ELEMENT_NODE) {
486 EmberCore::String name = GetNodeName(child);
487
488 if (name == tree_config.behavior_tree_element) {
489 // Parse BehaviorTree
490 fileInfo.has_behavior_trees = true;
491 fileInfo.tree_count++;
492
493 EmberCore::String tree_id = GetNodeAttribute(child, tree_config.tree_id_attribute);
494 if (tree_id.empty()) {
495 fileInfo.errors.push_back("BehaviorTree element missing required 'ID' attribute");
496 } else {
497 fileInfo.tree_ids.push_back(tree_id);
498
499 // Check for duplicate tree IDs within this file
500 if (file_tree_ids.count(tree_id) > 0) {
501 fileInfo.warnings.push_back("Duplicate tree ID within file: " + tree_id);
502 }
503 file_tree_ids.insert(tree_id);
504
505 auto tree = ParseBehaviorTree(child);
506 if (tree) {
507 parsed_trees_[tree_id] = tree;
508
509 // Track implementation status
510 TreeImplementationStatus status(tree_id);
511 status.is_implemented = tree->HasRootNode() && tree->GetNodeCount() > 0;
512 status.has_root_node = tree->HasRootNode();
513 status.defined_in_file = filepath;
514 tree_implementation_statuses_[tree_id] = status;
515
516 // Collect SubTree references
517 if (tree->HasRootNode()) {
518 CollectSubTreeReferences(tree->GetRootNode(), subtree_refs_set);
519 }
520
521 LOG_INFO("LibXMLParser", "Parsed tree '" + tree_id + "' from " + filepath);
522 }
523 }
524 } else if (name == "Blackboard") {
525 // Parse Blackboard
526 fileInfo.has_blackboards = true;
527 fileInfo.blackboard_count++;
528
529 EmberCore::String bb_id = GetNodeAttribute(child, "ID");
530 if (bb_id.empty()) {
531 fileInfo.errors.push_back("Blackboard element missing required 'ID' attribute");
532 } else {
533 fileInfo.blackboard_ids.push_back(bb_id);
534
535 // Check for duplicate blackboard IDs within this file
536 if (file_bb_ids.count(bb_id) > 0) {
537 fileInfo.warnings.push_back("Duplicate blackboard ID within file: " + bb_id);
538 }
539 file_bb_ids.insert(bb_id);
540
541 // Parse includes attribute
542 EmberCore::String includes = GetNodeAttribute(child, "includes");
543 if (!includes.empty()) {
544 // Validate format {ID1 ID2 ...}
545 if (includes.front() != '{' || includes.back() != '}') {
546 fileInfo.warnings.push_back(
547 "Blackboard '" + bb_id +
548 "' has malformed includes syntax (should be {ID1 ID2 ...})");
549 } else {
550 // Parse includes
551 String includes_content = includes.substr(1, includes.size() - 2);
552 std::istringstream iss(includes_content);
553 String token;
554 std::vector<String> bb_inc_list;
555 while (iss >> token) {
556 if (!token.empty()) {
557 bb_includes_set.insert(token);
558 fileInfo.blackboard_includes.push_back(token);
559 bb_inc_list.push_back(token);
560 }
561 }
562 if (!bb_inc_list.empty()) {
563 per_bb_includes[bb_id] = std::move(bb_inc_list);
564 }
565 }
566 }
567
568 // Validate entries
569 std::set<String> seen_keys;
570 for (xmlNodePtr entryNode = child->children; entryNode; entryNode = entryNode->next) {
571 if (entryNode->type != XML_ELEMENT_NODE)
572 continue;
573 if (GetNodeName(entryNode) != "Entry")
574 continue;
575
576 String key = GetNodeAttribute(entryNode, "key");
577 String type = GetNodeAttribute(entryNode, "type");
578
579 if (key.empty()) {
580 fileInfo.errors.push_back("Blackboard '" + bb_id +
581 "' has Entry missing required 'key' attribute");
582 continue;
583 }
584 if (type.empty()) {
585 fileInfo.errors.push_back("Blackboard '" + bb_id + "' entry '" + key +
586 "' missing required 'type' attribute");
587 continue;
588 }
589
590 // Check for duplicate keys
591 if (seen_keys.count(key) > 0) {
592 fileInfo.warnings.push_back("Blackboard '" + bb_id +
593 "' has duplicate entry key: " + key);
594 }
595 seen_keys.insert(key);
596
597 // Validate type (using existing validation list)
598 static const std::set<String> validTypes = {"bool",
599 "int",
600 "int8",
601 "float",
602 "string",
603 "Currency",
604 "uint8_t",
605 "uint32_t",
606 "size_t",
607 "auto",
608 "vec(int)",
609 "vec(int8)",
610 "vec(uint8_t)",
611 "vec(uint32_t)",
612 "vec(float)",
613 "vec(string)",
614 "vec(Currency)",
615 "vec(vec(uint8_t))",
616 "vec(vec(uint32_t))",
617 "vec(pair(float))",
618 "pair(float)",
619 "pair(int)",
620 "pair(string)",
621 "map(string, string)",
622 "map(string,string)",
623 "map(string, int)",
624 "map(string,int)",
625 "set(string)",
626 "set(int)",
627 "set(uint32_t)",
628 "set",
629 "Results",
630 "ReelFigs",
631 "GameSounds"};
632
633 bool isValidType = validTypes.count(type) > 0;
634 bool isCustomType = !type.empty() && std::isupper(type[0]) &&
635 type.find('(') == String::npos && type.find(')') == String::npos;
636
637 if (!isValidType && !isCustomType) {
638 fileInfo.warnings.push_back("Blackboard '" + bb_id + "' entry '" + key +
639 "' has unsupported type: " + type);
640 }
641 }
642
643 // Actually parse the blackboard
644 auto blackboard = ParseBlackboard(child);
645 if (blackboard) {
646 parsed_blackboards_[bb_id] = std::move(blackboard);
647 LOG_INFO("LibXMLParser", "Parsed blackboard '" + bb_id + "' from " + filepath);
648 }
649 }
650 }
651 }
652 }
653
654 // Store subtree refs in file info
655 fileInfo.subtree_refs.assign(subtree_refs_set.begin(), subtree_refs_set.end());
656
657 for (auto &kv : per_bb_includes) {
658 result.blackboard_includes_map[kv.first] = std::move(kv.second);
659 }
660
661 // Check if file has neither trees nor blackboards
662 if (!fileInfo.has_behavior_trees && !fileInfo.has_blackboards) {
663 fileInfo.warnings.push_back("No BehaviorTree or Blackboard elements found in file");
664 }
665
666 // Add file info to result
667 result.file_infos.push_back(fileInfo);
668
669 xmlFreeDoc(doc);
670 }
671
672 // Select the main tree: among valid candidates, pick the one with the most subtree references
673 // (the project's main execution tree typically orchestrates the most subtrees)
674 {
675 EmberCore::String best_candidate;
676 size_t best_ref_count = 0;
677
678 auto countRefs = [](Node *root) -> size_t {
679 if (!root)
680 return 0;
681 std::set<EmberCore::String> refs;
682 std::vector<Node *> stack = {root};
683 while (!stack.empty()) {
684 Node *n = stack.back();
685 stack.pop_back();
686 EmberCore::String ref = n->GetAttribute("__subtree_ref__", "");
687 if (!ref.empty())
688 refs.insert(ref);
689 for (size_t i = 0; i < n->GetChildCount(); ++i)
690 stack.push_back(n->GetChild(i));
691 }
692 return refs.size();
693 };
694
695 for (const auto &candidate : main_tree_candidates) {
696 auto it = parsed_trees_.find(candidate);
697 if (it == parsed_trees_.end() || !it->second || !it->second->HasRootNode())
698 continue;
699 size_t refCount = countRefs(it->second->GetRootNode());
700 if (best_candidate.empty() || refCount > best_ref_count) {
701 best_candidate = candidate;
702 best_ref_count = refCount;
703 }
704 }
705
706 if (!best_candidate.empty()) {
707 result.main_tree_name = best_candidate;
708 } else if (!main_tree_candidates.empty()) {
709 result.main_tree_name = main_tree_candidates.front();
710 }
711 }
712
713 // Note: Even if some files failed to parse, we continue with validation
714 // for the files that did parse successfully. result.success will remain
715 // false to indicate errors occurred, but we still do cross-file validation.
716
717 // Copy any errors from failed parsing
718 if (!result.success) {
719 std::lock_guard<std::mutex> lock(errors_mutex_);
720 for (const auto &err : errors_) {
721 result.errors.push_back(err);
722 }
723 }
724
725 // Second pass: Collect all SubTree references and identify unimplemented trees
726 ReportProgress("Collecting SubTree references...", total_files + 1, total_files + 2);
727
728 std::set<EmberCore::String> all_references;
729 for (const auto &tree_pair : parsed_trees_) {
730 if (tree_pair.second && tree_pair.second->HasRootNode()) {
731 CollectSubTreeReferences(tree_pair.second->GetRootNode(), all_references);
732 }
733 }
734
735 // Collect all implemented tree IDs
736 std::set<String> implemented_tree_ids;
737 for (const auto &tree_pair : parsed_trees_) {
738 implemented_tree_ids.insert(tree_pair.first);
739 }
740
741 // Track references in implementation status and add per-file warnings
742 for (const auto &ref : all_references) {
744 // This is a referenced but not implemented tree
745 TreeImplementationStatus status(ref);
746 status.is_implemented = false;
747 status.has_root_node = false;
748 tree_implementation_statuses_[ref] = status;
749 unimplemented_references_.insert(ref);
750 result.unimplemented_references.push_back(ref);
751 result.warnings.push_back("Tree '" + ref + "' is referenced but not implemented");
752 LOG_WARNING("LibXMLParser", "Tree '" + ref + "' is referenced but not implemented");
753
754 // Add warning to each file that references this unimplemented tree
755 for (auto &fileInfo : result.file_infos) {
756 for (const auto &subtree_ref : fileInfo.subtree_refs) {
757 if (subtree_ref == ref) {
758 fileInfo.warnings.push_back("References unimplemented tree: " + ref);
759 break;
760 }
761 }
762 }
763 }
764 }
765
766 // Third pass: Expand all SubTree placeholders (with project mode)
767 ReportProgress("Expanding SubTree references...", total_files + 2, total_files + 2);
769
770 // Cross-file validation: detect duplicates and unresolved references
771 ReportProgress("Validating cross-file references...", total_files + 2, total_files + 2);
772
773 // Collect all tree IDs and detect duplicates
774 std::map<String, std::vector<String>> tree_id_to_files;
775 for (const auto &fileInfo : result.file_infos) {
776 for (const auto &tree_id : fileInfo.tree_ids) {
777 tree_id_to_files[tree_id].push_back(fileInfo.filepath);
778 }
779 }
780 for (const auto &pair : tree_id_to_files) {
781 if (pair.second.size() > 1) {
782 result.duplicate_tree_ids.push_back(pair.first);
783 result.warnings.push_back("Duplicate tree ID found across files: " + pair.first);
784 }
785 }
786
787 // Collect all blackboard IDs and detect duplicates
788 std::map<String, std::vector<String>> bb_id_to_files;
789 std::set<String> all_blackboard_ids;
790 for (const auto &fileInfo : result.file_infos) {
791 for (const auto &bb_id : fileInfo.blackboard_ids) {
792 bb_id_to_files[bb_id].push_back(fileInfo.filepath);
793 all_blackboard_ids.insert(bb_id);
794 }
795 }
796 for (const auto &pair : bb_id_to_files) {
797 if (pair.second.size() > 1) {
798 result.duplicate_blackboard_ids.push_back(pair.first);
799 result.warnings.push_back("Duplicate blackboard ID found across files: " + pair.first);
800 }
801 }
802
803 // Detect unresolved blackboard includes
804 std::set<String> unresolved_includes;
805 for (const auto &fileInfo : result.file_infos) {
806 for (const auto &include : fileInfo.blackboard_includes) {
807 if (all_blackboard_ids.count(include) == 0) {
808 unresolved_includes.insert(include);
809 }
810 }
811 }
812 result.unresolved_blackboard_includes.assign(unresolved_includes.begin(), unresolved_includes.end());
813 for (const auto &include : unresolved_includes) {
814 result.warnings.push_back("Unresolved blackboard include: " + include);
815 }
816
817 // Copy circular references detected during expansion
819
820 // Copy results
822
823 // Convert unique_ptr blackboards to shared_ptr
824 for (auto &pair : parsed_blackboards_) {
825 if (pair.second) {
826 // Move ownership from unique_ptr to shared_ptr
827 result.parsed_blackboards[pair.first] = std::shared_ptr<Blackboard>(std::move(pair.second));
828 }
829 }
830
832
833 // Copy any additional errors
834 {
835 std::lock_guard<std::mutex> lock(errors_mutex_);
836 for (const auto &err : errors_) {
837 result.errors.push_back(err);
838 }
839 }
840
841 result.success = result.errors.empty();
842 project_parsing_mode_ = false;
843
844 ReportProgress("Project parsing complete: " + std::to_string(result.GetImplementedTreeCount()) + " implemented, " +
845 std::to_string(result.unimplemented_references.size()) + " unimplemented",
846 total_files + 2, total_files + 2);
847
848 return result;
849}
850
851void LibXMLBehaviorTreeParser::CollectSubTreeReferences(Node *node, std::set<EmberCore::String> &references) {
852 if (!node)
853 return;
854
855 // Check if this is a SubTree placeholder
856 EmberCore::String subtree_ref = node->GetAttribute("__subtree_ref__", "");
857 if (!subtree_ref.empty()) {
858 references.insert(subtree_ref);
859
860 // Track which files reference this tree
861 if (tree_implementation_statuses_.find(subtree_ref) != tree_implementation_statuses_.end()) {
862 auto &status = tree_implementation_statuses_[subtree_ref];
863 if (std::find(status.referenced_in_files.begin(), status.referenced_in_files.end(), current_file_path_) ==
864 status.referenced_in_files.end()) {
865 status.referenced_in_files.push_back(current_file_path_);
866 }
867 }
868 }
869
870 // Recursively check children
871 for (size_t i = 0; i < node->GetChildCount(); ++i) {
872 CollectSubTreeReferences(node->GetChild(i), references);
873 }
874}
875
877 // Clear cache before expansion
879 expansion_stack_.clear();
880 circular_references_.clear();
881
882 for (auto &tree_pair : parsed_trees_) {
883 auto &tree = tree_pair.second;
884 if (tree && tree->HasRootNode()) {
885 // Add the tree ID to the expansion stack before expanding
886 expansion_stack_.insert(tree_pair.first);
887 ExpandSubTreePlaceholderForProject(tree->GetRootNode());
888 expansion_stack_.erase(tree_pair.first);
889 }
890 }
891}
892
894 if (!node)
895 return false;
896
897 bool expanded_any = false;
898
899 // Check if this node is a SubTree placeholder
900 EmberCore::String subtree_ref = node->GetAttribute("__subtree_ref__", "");
901 if (!subtree_ref.empty()) {
902 // This is a SubTree placeholder node
903
904 if (parsed_trees_.find(subtree_ref) != parsed_trees_.end()) {
905 auto referenced_tree = parsed_trees_[subtree_ref];
906 if (referenced_tree && referenced_tree->HasRootNode()) {
907 Node *referenced_root = referenced_tree->GetRootNode();
908
909 // Check for circular reference
910 bool is_expanding = (expansion_stack_.find(subtree_ref) != expansion_stack_.end());
911 bool already_expanded = (expanded_subtree_cache_.find(subtree_ref) != expanded_subtree_cache_.end());
912
913 if (is_expanding) {
914 // Track the circular reference - build a message showing the cycle
915 String cycle_msg = "Circular reference: ";
916 bool first = true;
917 for (const auto &tree_in_stack : expansion_stack_) {
918 if (!first)
919 cycle_msg += " -> ";
920 cycle_msg += tree_in_stack;
921 first = false;
922 }
923 cycle_msg += " -> " + subtree_ref;
924
925 circular_references_.insert(cycle_msg);
926 AddError(ParseError::INVALID_TREE_STRUCTURE, "Circular SubTree reference detected: " + cycle_msg);
927 LOG_ERROR("LibXMLParser", "Circular SubTree reference detected: " + cycle_msg);
928 return false;
929 }
930
931 if (!already_expanded) {
932 expansion_stack_.insert(subtree_ref);
933 ExpandSubTreePlaceholderForProject(referenced_root);
934 expanded_subtree_cache_.insert(subtree_ref);
935 expansion_stack_.erase(subtree_ref);
936 }
937
938 // Copy properties from referenced tree
939 node->SetName(referenced_root->GetName());
940 node->SetType(referenced_root->GetType());
941 node->RemoveAttribute("__is_placeholder__");
942
943 for (const auto &attr_pair : referenced_root->GetAllAttributes()) {
944 node->SetAttribute(attr_pair.first, attr_pair.second);
945 }
946
947 for (size_t i = 0; i < referenced_root->GetChildCount(); ++i) {
948 Node *child = referenced_root->GetChild(i);
949 if (child) {
950 node->AddChild(child->DeepCopy());
951 }
952 }
953
954 expanded_any = true;
955 } else {
956 // Referenced tree exists but is empty - mark as unimplemented
957 node->RemoveAttribute("__is_placeholder__");
958 node->SetAttribute("__unimplemented__", "true");
959 node->SetName("SubTree: " + subtree_ref + " [EMPTY]");
960 LOG_WARNING("LibXMLParser", "SubTree '" + subtree_ref + "' is empty (no root node)");
961 }
962 } else {
963 // Tree not found - in project mode, don't error, just mark as unimplemented
964 // Remove the placeholder marker and mark as unimplemented instead
965 node->RemoveAttribute("__is_placeholder__");
966 node->SetAttribute("__unimplemented__", "true");
967 node->SetName("SubTree: " + subtree_ref + " [NOT IMPLEMENTED]");
968 LOG_WARNING("LibXMLParser", "SubTree '" + subtree_ref + "' not found (unimplemented reference)");
969 }
970 } else {
971 // Not a subtree placeholder, recursively expand children
972 for (size_t i = 0; i < node->GetChildCount(); ++i) {
974 expanded_any = true;
975 }
976 }
977 }
978
979 return expanded_any;
980}
981
983 auto it = tree_implementation_statuses_.find(tree_id);
984 if (it != tree_implementation_statuses_.end()) {
985 return it->second.is_implemented;
986 }
987 return false;
988}
989
990std::vector<EmberCore::String> LibXMLBehaviorTreeParser::GetUnimplementedReferences() const {
991 std::vector<EmberCore::String> result;
992 for (const auto &ref : unimplemented_references_) {
993 result.push_back(ref);
994 }
995 return result;
996}
997
998// ============================================================================
999// Core Parsing Methods
1000// ============================================================================
1001
1002bool LibXMLBehaviorTreeParser::ParseXMLDocument(xmlDocPtr doc, const EmberCore::String &source_path) {
1003 xmlNodePtr root = xmlDocGetRootElement(doc);
1004 if (!root) {
1005 AddError(ParseError::MISSING_ROOT_ELEMENT, "XML document has no root element");
1006 return false;
1007 }
1008
1009 // Use configured root element name
1010 const auto &doc_config = config_.GetDocumentConfig();
1011 EmberCore::String root_name = GetNodeName(root);
1012
1013 if (doc_config.case_sensitive) {
1014 if (root_name != doc_config.root_element) {
1015 AddError(ParseError::MISSING_ROOT_ELEMENT, EmberCore::String("Expected '") + doc_config.root_element +
1016 "' element, found '" + root_name + "'");
1017 return false;
1018 }
1019 } else {
1020 // Case-insensitive comparison
1021 auto to_lower = [](String str) {
1022 std::transform(str.begin(), str.end(), str.begin(), ::tolower);
1023 return str;
1024 };
1025 if (to_lower(root_name) != to_lower(doc_config.root_element)) {
1026 AddError(ParseError::MISSING_ROOT_ELEMENT, EmberCore::String("Expected '") + doc_config.root_element +
1027 "' element, found '" + root_name + "'");
1028 return false;
1029 }
1030 }
1031
1032 // Get main tree to execute using configured attribute name
1033 main_tree_name_ = GetNodeAttribute(root, doc_config.main_tree_attribute);
1034 if (!main_tree_name_.empty()) {
1035 ReportProgress(EmberCore::String("Main tree: ") + main_tree_name_, 2, 5);
1036 }
1037
1038 // Report progress: Parsing structure
1039 ReportProgress("Parsing tree structure...", 2, 5);
1040
1041 // Get configured element names
1042 const auto &tree_config = config_.GetTreeConfig();
1043 const auto &blackboard_config = config_.GetBlackboardConfig();
1044
1045 // Count total elements first for better progress reporting
1046 int totalElements = 0;
1047 for (xmlNodePtr child = root->children; child; child = child->next) {
1048 if (child->type == XML_ELEMENT_NODE) {
1049 EmberCore::String name = GetNodeName(child);
1050 if (name == tree_config.behavior_tree_element || name == blackboard_config.blackboard_element) {
1051 totalElements++;
1052 }
1053 }
1054 }
1055 ReportProgress("Found " + std::to_string(totalElements) + " element(s) to parse", 2, 5);
1056
1057 // Parse all BehaviorTree nodes
1058 int treeCount = 0;
1059 int blackboardCount = 0;
1060 for (xmlNodePtr child = root->children; child; child = child->next) {
1061 if (child->type == XML_ELEMENT_NODE) {
1062 EmberCore::String name = GetNodeName(child);
1063 if (name == tree_config.behavior_tree_element) {
1064 auto tree = ParseBehaviorTree(child);
1065 if (tree) {
1066 EmberCore::String tree_id = GetNodeAttribute(child, tree_config.tree_id_attribute);
1067 if (!tree_id.empty()) {
1068 parsed_trees_[tree_id] = tree;
1069 treeCount++;
1070 // Report progress for each tree parsed
1071 EmberCore::String progressMsg = "Parsed tree " + std::to_string(treeCount) + "/" +
1072 std::to_string(totalElements) + ": " + tree_id;
1073 ReportProgress(progressMsg, 2, 5);
1074
1075 // Log node count
1076 if (tree->HasRootNode()) {
1077 size_t nodeCount = tree->GetNodeCount();
1078 ReportProgress(" " + std::to_string(nodeCount) + " node(s) in tree", 2, 5);
1079 }
1080 } else {
1082 tree_config.behavior_tree_element + " missing " + tree_config.tree_id_attribute +
1083 " attribute",
1084 child);
1085 ReportProgress(" WARNING: Tree missing ID attribute", 2, 5);
1086 }
1087 }
1088 } else if (name == blackboard_config.blackboard_element) {
1089 auto blackboard = ParseBlackboard(child);
1090 if (blackboard) {
1091 EmberCore::String blackboard_id = blackboard->GetId();
1092 parsed_blackboards_[blackboard_id] = std::move(blackboard);
1093 blackboardCount++;
1094 ReportProgress(EmberCore::String("Parsed blackboard: ") + blackboard_id, 2, 5);
1095 }
1096 } else if (!doc_config.allow_unknown_elements) {
1097 LOG_WARNING("LibXMLParser", "Unknown element '" + name + "' in root (ignored)");
1098 }
1099 }
1100 }
1101
1102 LOG_INFO("LibXMLParser", "Parsed " + std::to_string(parsed_trees_.size()) + " trees and " +
1103 std::to_string(parsed_blackboards_.size()) + " blackboards");
1104
1105 ReportProgress("Parsed " + std::to_string(treeCount) + " tree(s) and " + std::to_string(blackboardCount) +
1106 " blackboard(s)",
1107 2, 5);
1108
1109 // Report progress: Expanding subtrees
1110 ReportProgress("Expanding subtrees...", 3, 5);
1111
1112 // Expand all SubTree placeholders now that all trees have been parsed
1114 ReportProgress("Subtree expansion complete", 3, 5);
1115
1116 // Assign blackboards to all parsed trees after parsing is complete
1117 for (const auto &blackboard_pair : parsed_blackboards_) {
1118 const auto &blackboard = blackboard_pair.second;
1119 for (auto &tree_pair : parsed_trees_) {
1120 tree_pair.second->AddBlackboard(blackboard->Clone());
1121 }
1122 }
1123
1124 // Check for errors
1125 if (HasErrors()) {
1126 LOG_ERROR("LibXMLParser", "Parsing failed with " + std::to_string(errors_.size()) + " error(s):");
1127 for (size_t i = 0; i < errors_.size(); ++i) {
1128 LOG_ERROR("LibXMLParser", " " + std::to_string(i + 1) + ": " + errors_[i].message);
1129 }
1130 return false;
1131 }
1132
1133 LOG_INFO("LibXMLParser", "Parsing completed successfully");
1134 return true;
1135}
1136
1137// Helper methods for libxml2 integration
1139 if (!node || !node->name)
1140 return "";
1141 return std::string(reinterpret_cast<const char *>(node->name));
1142}
1143
1145 if (!node)
1146 return "";
1147
1148 xmlChar *value = xmlGetProp(node, reinterpret_cast<const xmlChar *>(attr_name.c_str()));
1149 if (!value)
1150 return "";
1151
1152 EmberCore::String result = std::string(reinterpret_cast<const char *>(value));
1153 xmlFree(value);
1154 return result;
1155}
1156
1157// Thread-safe error handling
1159 std::lock_guard<std::mutex> lock(errors_mutex_);
1160 return !errors_.empty();
1161}
1162
1163const std::vector<LibXMLBehaviorTreeParser::ParseError> &LibXMLBehaviorTreeParser::GetErrors() const {
1164 std::lock_guard<std::mutex> lock(errors_mutex_);
1165 return errors_;
1166}
1167
1169 std::lock_guard<std::mutex> lock(errors_mutex_);
1170 errors_.clear();
1171}
1172
1174 const EmberCore::String &context) {
1175 std::lock_guard<std::mutex> lock(errors_mutex_);
1176 ParseError error(type, message, current_file_path_);
1177
1178 if (node) {
1179 error.line_number = node->line;
1180 error.column_number = 0; // libxml2 doesn't provide column info in older versions
1181 error.node_path = GetNodePath(node);
1182 }
1183
1184 if (!context.empty()) {
1185 error.context = context;
1186 error.message += " (Context: " + context + ")";
1187 }
1188
1189 errors_.push_back(error);
1190 LOG_ERROR("LibXMLParser", "Parse error: " + message + "");
1191}
1192
1194 if (!node)
1195 return "";
1196
1197 EmberCore::String path;
1198 xmlNodePtr current = node;
1199
1200 while (current && current->parent) {
1201 EmberCore::String node_name = GetNodeName(current);
1202 EmberCore::String id = GetNodeAttribute(current, "ID");
1203 EmberCore::String name = GetNodeAttribute(current, "name");
1204
1205 if (!id.empty()) {
1206 node_name += "[ID=" + id + "]";
1207 } else if (!name.empty()) {
1208 node_name += "[name=" + name + "]";
1209 }
1210
1211 if (path.empty()) {
1212 path = node_name;
1213 } else {
1214 path = node_name + "/" + path;
1215 }
1216
1217 current = current->parent;
1218 }
1219
1220 return path;
1221}
1222
1223bool LibXMLBehaviorTreeParser::ReportProgress(const EmberCore::String &message, int current, int total) {
1224 if (progress_callback_) {
1225 return progress_callback_->OnProgress(message, current, total);
1226 }
1227 // If no callback, always continue
1228 return true;
1229}
1230
1231std::shared_ptr<BehaviorTree> LibXMLBehaviorTreeParser::ParseBehaviorTree(xmlNodePtr tree_node) {
1232 const auto &tree_config = config_.GetTreeConfig();
1233
1234 if (!ValidateRequiredAttribute(tree_node, tree_config.tree_id_attribute)) {
1235 return nullptr;
1236 }
1237
1238 EmberCore::String tree_id = GetNodeAttribute(tree_node, tree_config.tree_id_attribute);
1239 auto behavior_tree = std::make_shared<BehaviorTree>();
1240
1241 // Set the tree name from the ID attribute (important for serialization round-trips)
1242 behavior_tree->SetName(tree_id);
1243
1244 // Capture document-level comments
1245 CaptureComments(tree_node, behavior_tree.get());
1246
1247 // Parse the root node of this behavior tree
1248 xmlNodePtr child = tree_node->children;
1249 std::unique_ptr<Node> root_node = nullptr;
1250
1251 while (child) {
1252 if (child->type == XML_ELEMENT_NODE) {
1253 auto node = ParseNode(child);
1254 if (node) {
1255 if (!root_node) {
1256 root_node = std::move(node);
1257 } else {
1259 tree_config.behavior_tree_element + " can only have one root node", child);
1260 return nullptr;
1261 }
1262 }
1263 }
1264 child = child->next;
1265 }
1266
1267 if (!root_node && tree_config.require_root_node) {
1269 EmberCore::String(tree_config.behavior_tree_element) + " '" + tree_id + "' has no root node");
1270 return nullptr;
1271 }
1272
1273 if (root_node) {
1274 // Create a visual wrapper node for the BehaviorTree itself
1275 auto wrapper_node = std::make_unique<Node>(tree_id, Node::Type::BehaviorTree);
1276
1277 // Copy any BehaviorTree-level attributes to the wrapper node
1278 xmlAttrPtr attr = tree_node->properties;
1279 while (attr) {
1280 EmberCore::String attr_name = reinterpret_cast<const char *>(attr->name);
1281 EmberCore::String attr_value = GetNodeAttribute(tree_node, attr_name);
1282 wrapper_node->SetAttribute(attr_name, attr_value);
1283 attr = attr->next; // CRITICAL: Advance to next attribute
1284 }
1285
1286 // Add the actual root node (Sequence, Selector, etc.) as a child
1287 wrapper_node->AddChild(std::move(root_node));
1288
1289 // Set the wrapper as the BehaviorTree's root
1290 behavior_tree->SetRootNode(std::move(wrapper_node));
1291 }
1292 return behavior_tree;
1293}
1294
1295std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseNode(xmlNodePtr xml_node) {
1296 if (!xml_node || xml_node->type != XML_ELEMENT_NODE) {
1297 return nullptr;
1298 }
1299
1300 const auto &node_config = config_.GetNodeConfig();
1301 const auto &tree_config = config_.GetTreeConfig();
1302
1303 EmberCore::String element_name = GetNodeName(xml_node);
1304 std::unique_ptr<Node> node = nullptr;
1305
1306 // Element name indicates the category (Control, Action, Decorator, Condition)
1307 // The specific behavior is specified in the ID attribute
1308 if (element_name == node_config.control_element) {
1309 node = ParseControlNode(xml_node);
1310 } else if (element_name == node_config.action_element) {
1311 node = ParseActionNode(xml_node);
1312 } else if (element_name == node_config.condition_element) {
1313 node = ParseConditionNode(xml_node);
1314 } else if (element_name == node_config.decorator_element) {
1315 node = ParseDecoratorNode(xml_node);
1316 } else if (element_name == tree_config.subtree_element) {
1317 node = ParseSubTreeNode(xml_node);
1318 } else if (element_name == node_config.generic_node_element) {
1319 // Generic node - try to infer type
1320 node = ParseActionNode(xml_node); // Default to action
1321 } else {
1322 // Unknown node type - handle according to config
1323 const auto &classification_config = config_.GetClassificationConfig();
1324 switch (classification_config.unknown_behavior) {
1326 AddError(ParseError::UNKNOWN_NODE_TYPE, "Unknown node element: " + element_name, xml_node);
1327 return nullptr;
1329 LOG_WARNING("LibXMLParser", "Unknown node element: " + element_name + " (skipped)");
1330 return nullptr;
1332 LOG_WARNING("LibXMLParser", "Unknown node element: " + element_name + " (creating generic action node)");
1333 node = ParseActionNode(xml_node);
1334 break;
1335 }
1336 }
1337
1338 if (node) {
1339 // For SubTree nodes, attributes and children are handled differently
1340 // (they are copied from the referenced tree)
1341 if (element_name != tree_config.subtree_element) {
1342 SetNodeAttributes(node.get(), xml_node);
1343 ParseChildNodes(node.get(), xml_node);
1344 }
1345 }
1346
1347 return node;
1348}
1349
1350std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseControlNode(xmlNodePtr xml_node) {
1351 const auto &node_config = config_.GetNodeConfig();
1352
1353 if (!ValidateRequiredAttribute(xml_node, node_config.node_id_attribute)) {
1354 return nullptr;
1355 }
1356
1357 EmberCore::String control_type = GetNodeAttribute(xml_node, node_config.node_id_attribute);
1358
1359 // Validate this is actually a control type
1360 if (!config_.IsControlType(control_type)) {
1361 const auto &classification_config = config_.GetClassificationConfig();
1362 if (classification_config.unknown_behavior == ParserConfig::UnknownTypeBehavior::ERROR) {
1363 AddError(ParseError::UNKNOWN_NODE_TYPE, "Unknown control node type: " + control_type, xml_node);
1364 return nullptr;
1365 }
1366 LOG_WARNING("LibXMLParser", "Unknown control type '" + control_type + "', creating generic control node");
1367 }
1368
1369 auto node = std::make_unique<Node>(control_type, Node::Type::Control);
1370 LOG_TRACE("LibXMLParser", "Created control node: " + control_type);
1371
1372 return node;
1373}
1374
1375std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseActionNode(xmlNodePtr xml_node) {
1376 const auto &node_config = config_.GetNodeConfig();
1377
1378 if (!ValidateRequiredAttribute(xml_node, node_config.node_id_attribute)) {
1379 return nullptr;
1380 }
1381
1382 EmberCore::String action_type = GetNodeAttribute(xml_node, node_config.node_id_attribute);
1383
1384 // Check if it's a known action type (if list is defined)
1385 if (!config_.IsActionType(action_type)) {
1386 const auto &classification_config = config_.GetClassificationConfig();
1387 if (!classification_config.action_types.empty() &&
1388 classification_config.unknown_behavior == ParserConfig::UnknownTypeBehavior::ERROR) {
1389 AddError(ParseError::UNKNOWN_NODE_TYPE, "Unknown action type: " + action_type, xml_node);
1390 return nullptr;
1391 }
1392 LOG_TRACE("LibXMLParser", "Unknown action type '" + action_type + "', creating generic Action node");
1393 }
1394
1395 auto node = std::make_unique<Node>(action_type, Node::Type::Action);
1396 LOG_TRACE("LibXMLParser", "Created action node: " + action_type);
1397
1398 return node;
1399}
1400
1401std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseConditionNode(xmlNodePtr xml_node) {
1402 const auto &node_config = config_.GetNodeConfig();
1403
1404 if (!ValidateRequiredAttribute(xml_node, node_config.node_id_attribute)) {
1405 return nullptr;
1406 }
1407
1408 EmberCore::String condition_type = GetNodeAttribute(xml_node, node_config.node_id_attribute);
1409
1410 // Check if it's a known condition type (if list is defined)
1411 if (!config_.IsConditionType(condition_type)) {
1412 const auto &classification_config = config_.GetClassificationConfig();
1413 if (!classification_config.condition_types.empty() &&
1414 classification_config.unknown_behavior == ParserConfig::UnknownTypeBehavior::ERROR) {
1415 AddError(ParseError::UNKNOWN_NODE_TYPE, "Unknown condition type: " + condition_type, xml_node);
1416 return nullptr;
1417 }
1418 LOG_TRACE("LibXMLParser", "Unknown condition type '" + condition_type + "', creating generic Condition node");
1419 }
1420
1421 auto node = std::make_unique<Node>(condition_type, Node::Type::Condition);
1422 LOG_TRACE("LibXMLParser", "Created condition node: " + condition_type);
1423
1424 return node;
1425}
1426
1427std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseDecoratorNode(xmlNodePtr xml_node) {
1428 const auto &node_config = config_.GetNodeConfig();
1429
1430 if (!ValidateRequiredAttribute(xml_node, node_config.node_id_attribute)) {
1431 return nullptr;
1432 }
1433
1434 EmberCore::String decorator_type = GetNodeAttribute(xml_node, node_config.node_id_attribute);
1435
1436 // Validate this is actually a decorator type
1437 if (!config_.IsDecoratorType(decorator_type)) {
1438 const auto &classification_config = config_.GetClassificationConfig();
1439 if (classification_config.unknown_behavior == ParserConfig::UnknownTypeBehavior::ERROR) {
1440 AddError(ParseError::UNKNOWN_NODE_TYPE, "Unknown decorator type: " + decorator_type, xml_node);
1441 return nullptr;
1442 }
1443 LOG_WARNING("LibXMLParser", "Unknown decorator type '" + decorator_type + "', creating generic decorator node");
1444 }
1445
1446 auto node = std::make_unique<Node>(decorator_type, Node::Type::Decorator);
1447 LOG_TRACE("LibXMLParser", "Created decorator node: " + decorator_type);
1448
1449 return node;
1450}
1451
1452std::unique_ptr<Node> LibXMLBehaviorTreeParser::ParseSubTreeNode(xmlNodePtr xml_node) {
1453 const auto &tree_config = config_.GetTreeConfig();
1454
1455 if (!ValidateRequiredAttribute(xml_node, tree_config.subtree_reference_attribute)) {
1456 return nullptr;
1457 }
1458
1459 EmberCore::String subtree_id = GetNodeAttribute(xml_node, tree_config.subtree_reference_attribute);
1460
1461 // Create a placeholder node that will be expanded later (two-pass system)
1462 // This allows forward references where SubTree A references Tree B that appears later in XML
1463 // Use BehaviorTree type for SubTree placeholders (they reference other behavior trees)
1464 auto placeholder = std::make_unique<Node>("SubTree[" + subtree_id + "]", Node::Type::BehaviorTree);
1465 placeholder->SetAttribute("__subtree_ref__", subtree_id);
1466 placeholder->SetAttribute("__is_placeholder__", "true");
1467
1468 LOG_TRACE("LibXMLParser",
1469 "Created SubTree placeholder for '" + subtree_id + "' (will expand after all trees parsed)");
1470
1471 // TODO: Handle SubTree parameters and variable substitution
1472 // For now, we just store the reference and expand later
1473
1474 return placeholder;
1475}
1476
1477std::unique_ptr<Blackboard> LibXMLBehaviorTreeParser::ParseBlackboard(xmlNodePtr blackboard_node) {
1478 const auto &blackboard_config = config_.GetBlackboardConfig();
1479
1480 if (!ValidateRequiredAttribute(blackboard_node, blackboard_config.blackboard_id_attribute)) {
1481 return nullptr;
1482 }
1483
1484 EmberCore::String blackboard_id = GetNodeAttribute(blackboard_node, blackboard_config.blackboard_id_attribute);
1485 auto blackboard = std::make_unique<Blackboard>(blackboard_id);
1486
1487 // Check for parent attribute (inheritance)
1488 EmberCore::String parent_id = GetNodeAttribute(blackboard_node, blackboard_config.parent_attribute);
1489 if (!parent_id.empty()) {
1490 blackboard->SetParentId(parent_id);
1491 LOG_TRACE("LibXMLParser", "Blackboard '" + blackboard_id + "' inherits from '" + parent_id + "'");
1492 }
1493
1494 // Parse all Entry children
1495 xmlNodePtr child = blackboard_node->children;
1496 while (child) {
1497 if (child->type == XML_ELEMENT_NODE && GetNodeName(child) == blackboard_config.entry_element) {
1498 auto entry = ParseBlackboardEntry(child);
1499 if (entry) {
1500 blackboard->AddEntry(std::move(entry));
1501 }
1502 }
1503 child = child->next;
1504 }
1505
1506 LOG_TRACE("LibXMLParser", "Parsed blackboard '" + blackboard_id + "' with " +
1507 std::to_string(blackboard->GetEntryCount()) + " entries");
1508
1509 return blackboard;
1510}
1511
1512std::unique_ptr<BlackboardEntry> LibXMLBehaviorTreeParser::ParseBlackboardEntry(xmlNodePtr entry_node) {
1513 const auto &blackboard_config = config_.GetBlackboardConfig();
1514
1515 if (!ValidateRequiredAttribute(entry_node, blackboard_config.entry_key_attribute) ||
1516 !ValidateRequiredAttribute(entry_node, blackboard_config.entry_type_attribute)) {
1517 return nullptr;
1518 }
1519
1520 EmberCore::String key = GetNodeAttribute(entry_node, blackboard_config.entry_key_attribute);
1521 EmberCore::String type_str = GetNodeAttribute(entry_node, blackboard_config.entry_type_attribute);
1522 EmberCore::String value = GetNodeAttribute(entry_node, blackboard_config.entry_value_attribute);
1523
1525 if (data_type == BlackboardEntry::DataType::UNKNOWN) {
1526 if (!blackboard_config.allow_undefined_keys) {
1527 AddError(ParseError::INVALID_VALUE, "Unknown blackboard entry type: " + type_str, entry_node);
1528 return nullptr;
1529 }
1530 LOG_WARNING("LibXMLParser", "Unknown blackboard type '" + type_str + "', using AUTO");
1532 }
1533
1534 auto entry = std::make_unique<BlackboardEntry>(key, data_type, value);
1535
1536 if (!entry->IsValidValue()) {
1537 // For empty values on collection/complex types, just warn and continue
1538 // This is common for types like 'set', 'auto', etc. that may be initialized later
1539 LOG_WARNING("LibXMLParser", "Empty or invalid value '" + value + "' for type '" + type_str + "' (key: " + key +
1540 ") - will use default");
1541 // Don't fail parsing for this - it's not critical
1542 }
1543
1544 LOG_TRACE("LibXMLParser",
1545 "Parsed blackboard entry: " + key + " (" + type_str + ") = " + (value.empty() ? "<empty>" : value));
1546
1547 return entry;
1548}
1549
1550void LibXMLBehaviorTreeParser::SetNodeAttributes(Node *node, xmlNodePtr xml_node) {
1551 const auto &node_config = config_.GetNodeConfig();
1552
1553 // Set basic attributes using configured attribute names
1554 EmberCore::String name = GetNodeAttribute(xml_node, node_config.node_name_attribute);
1555 if (!name.empty()) {
1556 node->SetName(name);
1557 }
1558
1559 EmberCore::String id = GetNodeAttribute(xml_node, node_config.node_id_attribute);
1560 if (!id.empty() && name.empty()) {
1561 node->SetName(id); // Use ID as name if no explicit name specified
1562 }
1563
1564 // Explicitly store the ID attribute so it can be serialized back
1565 if (!id.empty()) {
1566 node->SetAttribute(node_config.node_id_attribute, id);
1567 }
1568
1569 // Parse and store all custom attributes (if allowed)
1570 if (node_config.allow_custom_attributes) {
1571 xmlAttrPtr attr = xml_node->properties;
1572 while (attr) {
1573 EmberCore::String attr_name = std::string(reinterpret_cast<const char *>(attr->name));
1574 EmberCore::String attr_value = std::string(reinterpret_cast<const char *>(attr->children->content));
1575
1576 // Store all attributes except the name attribute (already handled above)
1577 // ID attribute is handled explicitly above but we allow it to be stored again to avoid issues
1578 if (attr_name != node_config.node_name_attribute) {
1579 node->SetAttribute(attr_name, attr_value);
1580 LOG_TRACE("LibXMLParser", "Stored node attribute: " + attr_name + " = " + attr_value);
1581 }
1582
1583 attr = attr->next;
1584 }
1585 }
1586}
1587
1588void LibXMLBehaviorTreeParser::ParseChildNodes(Node *parent, xmlNodePtr xml_parent) {
1589 xmlNodePtr child = xml_parent->children;
1590 size_t children_added = 0;
1591
1592 while (child) {
1593 if (child->type == XML_ELEMENT_NODE) {
1594 auto child_node = ParseNode(child);
1595 if (child_node) {
1596 // Now we have unique_ptr, so we can move it directly
1597 parent->AddChild(std::move(child_node));
1598 children_added++;
1599 LOG_TRACE("LibXMLParser", "Added child node to " + parent->GetName());
1600 } else {
1601 LOG_TRACE("LibXMLParser", "Skipped child node for " + parent->GetName());
1602 }
1603 }
1604 child = child->next;
1605 }
1606
1607 // Log if a parent ended up with no children (this might indicate SubTree skipping)
1608 if (children_added == 0 && xml_parent->children) {
1609 LOG_WARNING("LibXMLParser",
1610 "Node '" + parent->GetName() +
1611 "' ended up with no children after parsing (likely due to skipped SubTree nodes)");
1612 }
1613}
1614
1616 // Clear cache before expansion
1618 expansion_stack_.clear(); // Clear the stack tracking for circular reference detection
1619
1620 for (auto &tree_pair : parsed_trees_) {
1621 auto &tree = tree_pair.second;
1622 if (tree && tree->HasRootNode()) {
1623 ExpandSubTreePlaceholder(tree->GetRootNode());
1624 }
1625 }
1626}
1627
1629 if (!node)
1630 return false;
1631
1632 bool expanded_any = false;
1633
1634 // Check if this node is a SubTree placeholder
1635 EmberCore::String subtree_ref = node->GetAttribute("__subtree_ref__", "");
1636 if (!subtree_ref.empty()) {
1637 // This is a SubTree placeholder node - expand it
1638
1639 if (parsed_trees_.find(subtree_ref) != parsed_trees_.end()) {
1640 auto referenced_tree = parsed_trees_[subtree_ref];
1641 if (referenced_tree && referenced_tree->HasRootNode()) {
1642 Node *referenced_root = referenced_tree->GetRootNode();
1643
1644 // Check if this subtree is currently being expanded (circular reference)
1645 // OR if it has already been fully expanded (cache hit)
1646 bool is_expanding = (expansion_stack_.find(subtree_ref) != expansion_stack_.end());
1647 bool already_expanded = (expanded_subtree_cache_.find(subtree_ref) != expanded_subtree_cache_.end());
1648
1649 if (is_expanding) {
1650 // We're in a circular reference - abort
1651 AddError(ParseError::INVALID_TREE_STRUCTURE, "Circular SubTree reference detected: " + subtree_ref);
1652 LOG_ERROR("LibXMLParser", "Circular SubTree reference detected: " + subtree_ref);
1653 return false;
1654 }
1655
1656 if (!already_expanded) {
1657 // First time expanding this subtree - mark as being expanded
1658 expansion_stack_.insert(subtree_ref);
1659
1660 // Recursively expand the referenced tree's root and its children
1661 ExpandSubTreePlaceholder(referenced_root);
1662
1663 // Mark as fully expanded in cache
1664 expanded_subtree_cache_.insert(subtree_ref);
1665
1666 // Remove from expansion stack - this subtree is now safe to reference
1667 expansion_stack_.erase(subtree_ref);
1668 }
1669
1670 // Copy properties and children from the (now fully expanded) referenced_root
1671 node->SetName(referenced_root->GetName());
1672 node->SetType(referenced_root->GetType());
1673 node->RemoveAttribute("__is_placeholder__");
1674
1675 // Copy attributes from referenced tree
1676 for (const auto &attr_pair : referenced_root->GetAllAttributes()) {
1677 node->SetAttribute(attr_pair.first, attr_pair.second);
1678 }
1679
1680 // Deep copy children (which are already fully expanded)
1681 for (size_t i = 0; i < referenced_root->GetChildCount(); ++i) {
1682 Node *child = referenced_root->GetChild(i);
1683 if (child) {
1684 node->AddChild(child->DeepCopy());
1685 }
1686 }
1687
1688 // Since we deep copied from an already-expanded tree,
1689 // we DON'T need to expand the copied children again
1690
1691 expanded_any = true;
1692 } else {
1693 // Referenced tree exists but is empty - mark as unimplemented
1694 node->RemoveAttribute("__is_placeholder__");
1695 node->SetAttribute("__unimplemented__", "true");
1696 node->SetName("SubTree: " + subtree_ref + " [EMPTY]");
1697 LOG_WARNING("LibXMLParser", "SubTree '" + subtree_ref + "' is empty (no root node)");
1698 }
1699 } else {
1700 // SubTree not found - mark as unimplemented instead of leaving as placeholder
1701 // This allows the tree to be used even with missing subtree references
1702 node->RemoveAttribute("__is_placeholder__");
1703 node->SetAttribute("__unimplemented__", "true");
1704 node->SetName("SubTree: " + subtree_ref + " [NOT IMPLEMENTED]");
1705 LOG_WARNING("LibXMLParser", "SubTree '" + subtree_ref + "' not found (unimplemented reference)");
1706 }
1707 } else {
1708 // Not a subtree placeholder, just recursively expand children
1709 for (size_t i = 0; i < node->GetChildCount(); ++i) {
1710 if (ExpandSubTreePlaceholder(node->GetChild(i))) {
1711 expanded_any = true;
1712 }
1713 }
1714 }
1715
1716 return expanded_any;
1717}
1718
1720 if (!node) {
1721 AddError(ParseError::MISSING_ATTRIBUTE, "Node is null when validating attribute '" + attr_name + "'");
1722 return false;
1723 }
1724
1725 EmberCore::String value = GetNodeAttribute(node, attr_name);
1726 if (value.empty()) {
1727 AddError(ParseError::MISSING_ATTRIBUTE, "Required attribute '" + attr_name + "' is missing", node);
1728 return false;
1729 }
1730
1731 return true;
1732}
1733
1734void LibXMLBehaviorTreeParser::CaptureComments(xmlNodePtr parent, BehaviorTree *tree, Node *node) {
1735 if (!parent || !tree) {
1736 return;
1737 }
1738
1739 auto &xml_metadata = tree->GetXMLMetadata();
1740
1741 // Iterate through all children to find comments
1742 for (xmlNodePtr child = parent->children; child; child = child->next) {
1743 if (child->type == XML_COMMENT_NODE) {
1744 // Extract comment text
1745 xmlChar *content = xmlNodeGetContent(child);
1746 if (content) {
1747 EmberCore::String comment_text = reinterpret_cast<const char *>(content);
1748 xmlFree(content);
1749
1750 // Determine if this comment is before or after a node
1751 bool before_node = true;
1752 xmlNodePtr next_element = child->next;
1753 while (next_element && next_element->type != XML_ELEMENT_NODE) {
1754 next_element = next_element->next;
1755 }
1756
1757 if (!next_element) {
1758 before_node = false; // Comment after last element
1759 }
1760
1761 if (node) {
1762 // Associate comment with specific node
1763 XMLMetadata::Comment comment;
1764 comment.text = comment_text;
1765 comment.before_node = before_node;
1766 xml_metadata.node_comments[node->GetId()].push_back(comment);
1767 } else {
1768 // Document-level comment
1769 if (before_node) {
1770 xml_metadata.header_comments.push_back(comment_text);
1771 } else {
1772 xml_metadata.footer_comments.push_back(comment_text);
1773 }
1774 }
1775 }
1776 }
1777 }
1778}
1779
1780} // namespace EmberCore
#define LOG_ERROR(category, message)
Definition Logger.h:116
#define LOG_TRACE(category, message)
Definition Logger.h:113
#define LOG_WARNING(category, message)
Definition Logger.h:115
#define LOG_INFO(category, message)
Definition Logger.h:114
Represents a BehaviorTree project containing multiple XML resources.
void SetTreeImplementationStatus(const std::map< String, TreeImplementationStatus > &statuses)
const std::vector< String > & GetResources() const
String ResolveResourcePath(const String &resource_path) const
Unified validation system for behavior trees.
ValidationResult Validate(const BehaviorTree *tree) const
Validate a behavior tree against the parser profile.
Represents a complete behavior tree data structure.
XMLMetadata & GetXMLMetadata()
static DataType ParseDataType(const EmberCore::String &typeStr)
DataType
Supported data types for blackboard entries.
std::unique_ptr< Node > ParseNode(xmlNodePtr xml_node)
std::shared_ptr< BehaviorTree > ParseFromString(const EmberCore::String &xml_content)
std::vector< EmberCore::String > GetUnimplementedReferences() const
ProjectParseResult ParseFilesWithSharedRegistry(const std::vector< EmberCore::String > &filepaths)
std::set< EmberCore::String > unimplemented_references_
std::set< EmberCore::String > circular_references_
std::unique_ptr< Node > ParseSubTreeNode(xmlNodePtr xml_node)
std::map< EmberCore::String, TreeImplementationStatus > tree_implementation_statuses_
std::unique_ptr< Node > ParseActionNode(xmlNodePtr xml_node)
void ParseChildNodes(Node *parent, xmlNodePtr xml_parent)
bool ParseXMLDocument(xmlDocPtr doc, const EmberCore::String &source_path="")
void AddError(ParseError::Type type, const EmberCore::String &message, xmlNodePtr node=nullptr, const EmberCore::String &context="")
std::unique_ptr< BlackboardEntry > ParseBlackboardEntry(xmlNodePtr entry_node)
std::unique_ptr< Node > ParseConditionNode(xmlNodePtr xml_node)
void CaptureComments(xmlNodePtr parent, BehaviorTree *tree, Node *node=nullptr)
std::set< EmberCore::String > expansion_stack_
void CollectSubTreeReferences(Node *node, std::set< EmberCore::String > &references)
std::unique_ptr< Node > ParseControlNode(xmlNodePtr xml_node)
std::map< EmberCore::String, std::shared_ptr< BehaviorTree > > parsed_trees_
const std::vector< ParseError > & GetErrors() const
EmberCore::String GetNodeAttribute(xmlNodePtr node, const EmberCore::String &attr_name)
EmberCore::String GetNodePath(xmlNodePtr node)
std::unique_ptr< Blackboard > ParseBlackboard(xmlNodePtr blackboard_node)
EmberCore::String GetNodeName(xmlNodePtr node)
ProjectParseResult ParseProject(BehaviorTreeProject *project)
void SetNodeAttributes(Node *node, xmlNodePtr xml_node)
std::shared_ptr< BehaviorTree > ParseFromFile(const EmberCore::String &filepath)
bool IsTreeImplemented(const EmberCore::String &tree_id) const
std::set< EmberCore::String > expanded_subtree_cache_
std::shared_ptr< BehaviorTree > ParseBehaviorTree(xmlNodePtr tree_node)
std::map< EmberCore::String, std::unique_ptr< Blackboard > > parsed_blackboards_
std::unique_ptr< Node > ParseDecoratorNode(xmlNodePtr xml_node)
std::vector< ParseResult > ParseMultipleFiles(const std::vector< EmberCore::String > &filepaths)
bool ReportProgress(const EmberCore::String &message, int current=0, int total=0)
bool ValidateRequiredAttribute(xmlNodePtr node, const EmberCore::String &attr_name)
Represents a node in a behavior tree structure.
Definition Node.h:20
void SetType(Type type)
Definition Node.h:85
void SetName(const String &name)
Definition Node.h:82
const std::map< String, String > & GetAllAttributes() const
Definition Node.cpp:462
String GetAttribute(const String &name, const String &default_value="") const
Definition Node.cpp:451
Node * GetChild(size_t index) const
Definition Node.cpp:175
size_t GetId() const
Definition Node.h:80
Type GetType() const
Definition Node.h:84
const String & GetName() const
Definition Node.h:81
void SetAttribute(const String &name, const String &value)
Definition Node.cpp:444
void RemoveAttribute(const String &name)
Definition Node.cpp:460
void AddChild(std::unique_ptr< Node > child)
Definition Node.cpp:77
size_t GetChildCount() const
Definition Node.h:76
std::unique_ptr< Node > DeepCopy() const
Definition Node.cpp:149
Configuration for XML parser behavior and element/attribute mappings.
Main types header for EmberCore.
static void SuppressLibXMLErrors(void *ctx, const char *msg,...)
std::string String
Framework-agnostic string type.
Definition String.h:14
Per-file parsing information for project validation.
Error information for parsing failures (enhanced with libxml2 details)
std::map< EmberCore::String, std::vector< EmberCore::String > > blackboard_includes_map
std::map< EmberCore::String, TreeImplementationStatus > tree_statuses
std::map< EmberCore::String, std::shared_ptr< BehaviorTree > > parsed_trees
std::map< EmberCore::String, std::shared_ptr< Blackboard > > parsed_blackboards
Status of a tree's implementation in the project.
String defined_in_file
Which file defines this tree (empty if not defined)
bool has_root_node
Whether tree has a root node.
bool is_implemented
Has actual implementation (not just empty/placeholder)