Ember
Loading...
Searching...
No Matches
BehaviorTreeProject.cpp
Go to the documentation of this file.
3#include "Utils/Logger.h"
4#include <algorithm>
5#include <fstream>
6#include <iomanip>
7#include <libxml/parser.h>
8#include <libxml/tree.h>
9#include <numeric>
10#include <sstream>
11
12// Platform-specific path handling
13#ifdef _WIN32
14#include <direct.h>
15#define PATH_SEPARATOR '\\'
16#else
17#include <unistd.h>
18#define PATH_SEPARATOR '/'
19#endif
20
21namespace EmberCore {
22
23// ============================================================================
24// ProjectValidationReport Implementation
25// ============================================================================
26
28 return std::count_if(resource_statuses.begin(), resource_statuses.end(),
29 [](const ResourceValidationStatus &status) { return status.IsValid(); });
30}
31
33 return std::accumulate(resource_statuses.begin(), resource_statuses.end(), 0,
34 [](int sum, const ResourceValidationStatus &status) { return sum + status.tree_count; });
35}
36
38 return std::count_if(tree_statuses.begin(), tree_statuses.end(),
39 [](const auto &pair) { return pair.second.is_implemented; });
40}
41
43 std::ostringstream report;
44
45 report << "=== BehaviorTree Project Validation Report ===\n\n";
46 report << "Files: " << resource_statuses.size() << " XML files\n\n";
47
48 // Valid files
49 int valid_count = GetValidFileCount();
50 report << "VALID FILES (" << valid_count << "):\n";
51 for (const auto &status : resource_statuses) {
52 if (status.IsValid()) {
53 report << " [OK] " << status.filepath << " (" << status.tree_count << " trees)\n";
54 }
55 }
56 report << "\n";
57
58 // Invalid files
59 int invalid_count = static_cast<int>(resource_statuses.size()) - valid_count;
60 if (invalid_count > 0) {
61 report << "INVALID FILES (" << invalid_count << "):\n";
62 for (const auto &status : resource_statuses) {
63 if (!status.IsValid()) {
64 report << " [ERROR] " << status.filepath << "\n";
65 for (const auto &err : status.errors) {
66 report << " - " << err << "\n";
67 }
68 }
69 }
70 report << "\n";
71 }
72
73 // Warnings
74 if (!warnings.empty() || !unimplemented_trees.empty()) {
75 report << "WARNINGS (" << (warnings.size() + unimplemented_trees.size()) << "):\n";
76 for (const auto &warn : warnings) {
77 report << " [WARN] " << warn << "\n";
78 }
79 for (const auto &tree : unimplemented_trees) {
80 report << " [WARN] Tree '" << tree << "' is referenced but not implemented\n";
81 }
82 report << "\n";
83 }
84
85 // Errors
86 if (!errors.empty() || !circular_references.empty()) {
87 report << "ERRORS (" << (errors.size() + circular_references.size()) << "):\n";
88 for (const auto &err : errors) {
89 report << " [ERROR] " << err << "\n";
90 }
91 for (const auto &ref : circular_references) {
92 report << " [ERROR] Circular reference detected: " << ref << "\n";
93 }
94 report << "\n";
95 }
96
97 // Tree reference map
98 report << "TREE REFERENCE MAP:\n";
99 for (const auto &pair : tree_statuses) {
100 const auto &id = pair.first;
101 const auto &status = pair.second;
102 if (status.is_implemented) {
103 report << " [OK] " << id << " (implemented in " << status.defined_in_file << ")\n";
104 } else {
105 report << " [!!] " << id << " (not implemented)\n";
106 }
107 for (const auto &ref_file : status.referenced_in_files) {
108 report << " <- referenced by " << ref_file << "\n";
109 }
110 }
111
112 return report.str();
113}
114
115// ============================================================================
116// BehaviorTreeProject Implementation
117// ============================================================================
118
122
126
128 name_ = name;
130}
131
133 description_ = description;
135}
136
138
140 parser_profile_name_ = profile_name;
142}
143
145 // Check if already exists
146 if (HasResource(filepath)) {
147 LOG_WARNING("BehaviorTreeProject", "Resource already exists in project: " + filepath);
148 return false;
149 }
150
151 resources_.push_back(filepath);
153 LOG_INFO("BehaviorTreeProject", "Added resource: " + filepath);
154 return true;
155}
156
158 auto it = std::find(resources_.begin(), resources_.end(), filepath);
159 if (it != resources_.end()) {
160 resources_.erase(it);
162 LOG_INFO("BehaviorTreeProject", "Removed resource: " + filepath);
163 return true;
164 }
165
166 LOG_WARNING("BehaviorTreeProject", "Resource not found in project: " + filepath);
167 return false;
168}
169
170bool BehaviorTreeProject::HasResource(const String &filepath) const {
171 return std::find(resources_.begin(), resources_.end(), filepath) != resources_.end();
172}
173
179
181 if (project_filepath_.empty()) {
182 // If no project file, use current directory
183 char cwd[1024];
184#ifdef _WIN32
185 _getcwd(cwd, sizeof(cwd));
186#else
187 getcwd(cwd, sizeof(cwd));
188#endif
189 return String(cwd);
190 }
191
192 // Extract directory from project file path
193 size_t last_sep = project_filepath_.find_last_of("/\\");
194 if (last_sep != String::npos) {
195 return project_filepath_.substr(0, last_sep);
196 }
197 return ".";
198}
199
201 // If already absolute, return as-is
202 if (!resource_path.empty() &&
203 (resource_path[0] == '/' || (resource_path.length() > 1 && resource_path[1] == ':'))) {
204 return resource_path;
205 }
206
207 // Resolve relative to project base directory
208 String base = GetBaseDirectory();
209 if (base.empty() || base == ".") {
210 return resource_path;
211 }
212
213 return base + PATH_SEPARATOR + resource_path;
214}
215
217 String base = GetBaseDirectory();
218 if (base.empty()) {
219 return absolute_path;
220 }
221
222 // Normalize separators
223 String normalized_path = absolute_path;
224 String normalized_base = base;
225 std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/');
226 std::replace(normalized_base.begin(), normalized_base.end(), '\\', '/');
227
228 // Check if path starts with base
229 if (normalized_path.find(normalized_base) == 0) {
230 String relative = normalized_path.substr(normalized_base.length());
231 if (!relative.empty() && relative[0] == '/') {
232 relative = relative.substr(1);
233 }
234 return relative;
235 }
236
237 // Can't make relative, return absolute
238 return absolute_path;
239}
240
241// Helper function to recursively extract SubTree references
242static void ExtractSubTreeReferences(xmlNodePtr node, std::set<String> &subtree_refs) {
243 if (!node)
244 return;
245
246 for (xmlNodePtr child = node->children; child; child = child->next) {
247 if (child->type == XML_ELEMENT_NODE) {
248 const char *name = reinterpret_cast<const char *>(child->name);
249
250 // Check if this is a SubTree element
251 if (name && strcmp(name, "SubTree") == 0) {
252 xmlChar *id_attr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("ID"));
253 if (id_attr) {
254 subtree_refs.insert(reinterpret_cast<const char *>(id_attr));
255 xmlFree(id_attr);
256 }
257 }
258
259 // Recursively search children
260 ExtractSubTreeReferences(child, subtree_refs);
261 }
262 }
263}
264
267 status.filepath = filepath;
268
269 String resolved_path = ResolveResourcePath(filepath);
270
271 // Check if file exists
272 std::ifstream file(resolved_path);
273 if (!file.good()) {
274 status.errors.push_back("File not found: " + resolved_path);
275 return status;
276 }
277 status.exists = true;
278 file.close();
279
280 // Try to parse as XML
281 xmlDocPtr doc = xmlReadFile(resolved_path.c_str(), nullptr, XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
282 if (!doc) {
283 status.errors.push_back("Invalid XML syntax");
284 return status;
285 }
286 status.is_valid_xml = true;
287
288 // Check for BehaviorTree elements
289 xmlNodePtr root = xmlDocGetRootElement(doc);
290 if (!root) {
291 xmlFreeDoc(doc);
292 status.errors.push_back("XML document has no root element");
293 return status;
294 }
295
296 // Track SubTree references across all trees in this file
297 std::set<String> subtree_refs;
298
299 // Look for BehaviorTree and Blackboard elements
300 std::set<String> blackboard_includes;
301
302 for (xmlNodePtr child = root->children; child; child = child->next) {
303 if (child->type == XML_ELEMENT_NODE) {
304 const char *name = reinterpret_cast<const char *>(child->name);
305 if (name && strcmp(name, "BehaviorTree") == 0) {
306 status.has_behavior_trees = true;
307 status.tree_count++;
308
309 // Get tree ID
310 xmlChar *id_attr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("ID"));
311 if (id_attr) {
312 status.tree_ids.push_back(reinterpret_cast<const char *>(id_attr));
313 xmlFree(id_attr);
314 }
315
316 // Extract SubTree references from this tree
317 ExtractSubTreeReferences(child, subtree_refs);
318 } else if (name && strcmp(name, "Blackboard") == 0) {
319 status.has_blackboards = true;
320 status.blackboard_count++;
321
322 // Get blackboard ID
323 String bbId;
324 xmlChar *id_attr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("ID"));
325 if (id_attr) {
326 bbId = reinterpret_cast<const char *>(id_attr);
327 status.blackboard_ids.push_back(bbId);
328 xmlFree(id_attr);
329 } else {
330 status.errors.push_back("Blackboard element missing required 'ID' attribute");
331 }
332
333 // Get includes attribute and parse {ID1 ID2 ...} format
334 xmlChar *includes_attr = xmlGetProp(child, reinterpret_cast<const xmlChar *>("includes"));
335 if (includes_attr) {
336 String includes_str = reinterpret_cast<const char *>(includes_attr);
337 xmlFree(includes_attr);
338
339 // Validate includes syntax - must be in {ID1 ID2 ...} format
340 if (!includes_str.empty()) {
341 if (includes_str.front() != '{' || includes_str.back() != '}') {
342 status.warnings.push_back("Blackboard '" + bbId +
343 "' has malformed includes syntax (should be {ID1 ID2 ...})");
344 } else {
345 // Parse {ID1 ID2 ID3} format - strip braces and split by spaces
346 includes_str = includes_str.substr(1, includes_str.size() - 2);
347 // Split by whitespace
348 std::istringstream iss(includes_str);
349 String token;
350 while (iss >> token) {
351 if (!token.empty()) {
352 blackboard_includes.insert(token);
353 }
354 }
355 }
356 }
357 }
358
359 // Validate blackboard entries
360 std::set<String> seenKeys;
361 for (xmlNodePtr entryNode = child->children; entryNode; entryNode = entryNode->next) {
362 if (entryNode->type != XML_ELEMENT_NODE)
363 continue;
364 const char *entryName = reinterpret_cast<const char *>(entryNode->name);
365 if (!entryName || strcmp(entryName, "Entry") != 0)
366 continue;
367
368 // Check for required 'key' attribute
369 xmlChar *keyAttr = xmlGetProp(entryNode, reinterpret_cast<const xmlChar *>("key"));
370 if (!keyAttr) {
371 status.errors.push_back("Blackboard '" + bbId + "' has Entry missing required 'key' attribute");
372 continue;
373 }
374 String entryKey = reinterpret_cast<const char *>(keyAttr);
375 xmlFree(keyAttr);
376
377 // Check for duplicate keys within this blackboard
378 if (seenKeys.count(entryKey) > 0) {
379 status.warnings.push_back("Blackboard '" + bbId + "' has duplicate entry key: " + entryKey);
380 }
381 seenKeys.insert(entryKey);
382
383 // Check for required 'type' attribute
384 xmlChar *typeAttr = xmlGetProp(entryNode, reinterpret_cast<const xmlChar *>("type"));
385 if (!typeAttr) {
386 status.errors.push_back("Blackboard '" + bbId + "' entry '" + entryKey +
387 "' missing required 'type' attribute");
388 continue;
389 }
390 String entryType = reinterpret_cast<const char *>(typeAttr);
391 xmlFree(typeAttr);
392
393 // Validate type against known types
394 static const std::set<String> validTypes = {
395 "bool", "int", "int8", "float", "string", "Currency", "uint8_t", "uint32_t", "size_t", "auto",
396 "vec(int)", "vec(int8)", "vec(uint8_t)", "vec(uint32_t)", "vec(float)", "vec(string)",
397 "vec(Currency)", "vec(vec(uint8_t))", "vec(vec(uint32_t))", "vec(pair(float))", "pair(float)",
398 "pair(int)", "pair(string)", "map(string, string)", "map(string,string)", "map(string, int)",
399 "map(string,int)", "set(string)", "set(int)", "set(uint32_t)", "set",
400 // Custom types (we allow these as they may be game-specific)
401 "Results", "ReelFigs", "GameSounds"};
402
403 // Check if it's a valid type or a custom type (starts with uppercase, no parens)
404 bool isValidType = validTypes.count(entryType) > 0;
405 bool isCustomType = !entryType.empty() && std::isupper(entryType[0]) &&
406 entryType.find('(') == String::npos && entryType.find(')') == String::npos;
407
408 if (!isValidType && !isCustomType) {
409 status.warnings.push_back("Blackboard '" + bbId + "' entry '" + entryKey +
410 "' has unsupported type: " + entryType);
411 }
412 }
413 }
414 }
415 }
416
417 // Convert sets to vectors
418 status.subtree_refs.assign(subtree_refs.begin(), subtree_refs.end());
419 status.blackboard_includes.assign(blackboard_includes.begin(), blackboard_includes.end());
420
421 xmlFreeDoc(doc);
422
423 if (!status.has_behavior_trees && !status.has_blackboards) {
424 status.warnings.push_back("No BehaviorTree or Blackboard elements found in file");
425 }
426
427 return status;
428}
429
432
433 if (resources_.empty()) {
434 report.errors.push_back("Project has no resources");
435 return report;
436 }
437
438 // Validate each resource
439 std::set<String> all_tree_ids;
440 std::set<String> duplicate_tree_ids;
441 std::set<String> all_subtree_refs;
442 std::map<String, std::vector<String>> subtree_to_files; // Track which files reference which subtrees
443
444 for (const auto &resource : resources_) {
446
447 // Check for duplicates within this file
448 std::set<String> seen_in_file;
449 for (const auto &tree_id : status.tree_ids) {
450 if (seen_in_file.count(tree_id) > 0) {
451 // Found duplicate within same file
452 status.warnings.push_back("Duplicate tree ID within file: " + tree_id);
453 }
454 seen_in_file.insert(tree_id);
455 }
456
457 report.resource_statuses.push_back(status);
458
459 // Track tree IDs for duplicate detection
460 for (const auto &tree_id : status.tree_ids) {
461 if (all_tree_ids.count(tree_id) > 0) {
462 duplicate_tree_ids.insert(tree_id);
463 // Also add a warning to the specific file
464 const_cast<ResourceValidationStatus &>(status).warnings.push_back("Duplicate tree ID in project: " +
465 tree_id);
466 }
467 all_tree_ids.insert(tree_id);
468
469 // Create initial tree status
470 if (report.tree_statuses.find(tree_id) == report.tree_statuses.end()) {
471 TreeImplementationStatus tree_status(tree_id);
472 tree_status.is_implemented = true;
473 tree_status.has_root_node = true; // Assume valid for now
474 tree_status.defined_in_file = resource;
475 report.tree_statuses[tree_id] = tree_status;
476 }
477 }
478
479 // Track SubTree references
480 for (const auto &subtree_id : status.subtree_refs) {
481 all_subtree_refs.insert(subtree_id);
482 subtree_to_files[subtree_id].push_back(resource);
483 }
484 }
485
486 // Check for unimplemented SubTree references
487 for (const auto &subtree_id : all_subtree_refs) {
488 if (all_tree_ids.find(subtree_id) == all_tree_ids.end()) {
489 // This SubTree is referenced but not implemented in any file
490 report.unimplemented_trees.push_back(subtree_id);
491
492 // Create tree status for unimplemented tree
493 TreeImplementationStatus tree_status(subtree_id);
494 tree_status.is_implemented = false;
495 tree_status.has_root_node = false;
496 tree_status.referenced_in_files = subtree_to_files[subtree_id];
497 report.tree_statuses[subtree_id] = tree_status;
498 } else {
499 // Update the implemented tree's referenced_in_files
500 report.tree_statuses[subtree_id].referenced_in_files = subtree_to_files[subtree_id];
501 }
502 }
503
504 // Report duplicate tree IDs
505 for (const auto &dup : duplicate_tree_ids) {
506 report.warnings.push_back("Duplicate tree ID found: " + dup);
507 }
508
509 // ---- Blackboard cross-file validation ----
510 std::set<String> all_blackboard_ids;
511 std::set<String> duplicate_bb_ids;
512 std::set<String> all_bb_includes;
513
514 for (auto &res_status : report.resource_statuses) {
515 // Check for duplicate blackboard IDs within this file
516 std::set<String> seen_bb_in_file;
517 for (const auto &bb_id : res_status.blackboard_ids) {
518 if (seen_bb_in_file.count(bb_id) > 0) {
519 res_status.warnings.push_back("Duplicate blackboard ID within file: " + bb_id);
520 }
521 seen_bb_in_file.insert(bb_id);
522 }
523
524 // Check for duplicate blackboard IDs across files
525 for (const auto &bb_id : res_status.blackboard_ids) {
526 if (all_blackboard_ids.count(bb_id) > 0) {
527 duplicate_bb_ids.insert(bb_id);
528 res_status.warnings.push_back("Duplicate blackboard ID in project: " + bb_id);
529 }
530 all_blackboard_ids.insert(bb_id);
531 }
532
533 // Collect all includes for later resolution check
534 for (const auto &inc : res_status.blackboard_includes) {
535 all_bb_includes.insert(inc);
536 }
537 }
538
539 // Report duplicate blackboard IDs at project level
540 for (const auto &dup : duplicate_bb_ids) {
541 report.duplicate_blackboard_ids.push_back(dup);
542 report.warnings.push_back("Duplicate blackboard ID found: " + dup);
543 }
544
545 // Check for unresolved blackboard includes
546 for (const auto &inc : all_bb_includes) {
547 if (all_blackboard_ids.find(inc) == all_blackboard_ids.end()) {
548 report.unresolved_blackboard_includes.push_back(inc);
549 report.warnings.push_back("Unresolved blackboard include: " + inc);
550
551 // Also add a file-level warning to the files that reference this include
552 for (auto &res_status : report.resource_statuses) {
553 if (std::any_of(res_status.blackboard_includes.begin(), res_status.blackboard_includes.end(),
554 [&inc](const String &file_inc) { return file_inc == inc; })) {
555 res_status.warnings.push_back("Missing blackboard include: " + inc);
556 }
557 }
558 }
559 }
560
561 // Check if any file has errors
562 report.is_valid = !std::any_of(report.resource_statuses.begin(), report.resource_statuses.end(),
563 [](const ResourceValidationStatus &status) { return !status.errors.empty(); });
564
565 // Add unimplemented trees from our stored status
566 for (const auto &pair : tree_statuses_) {
567 const auto &id = pair.first;
568 const auto &status = pair.second;
569 if (!status.is_implemented) {
570 report.unimplemented_trees.push_back(id);
571
572 // Update or add to tree statuses
573 report.tree_statuses[id] = status;
574 }
575 }
576
577 return report;
578}
579
582 return report.is_valid;
583}
584
587
588 if (!parser) {
589 report.is_valid = false;
590 report.errors.push_back("Parser is null");
591 return report;
592 }
593
594 if (resources_.empty()) {
595 report.is_valid = false;
596 report.errors.push_back("No resources in project");
597 return report;
598 }
599
600 // Use parser to validate the project
601 auto parse_result = parser->ParseProject(const_cast<BehaviorTreeProject *>(this));
602
603 // Convert FileParseInfo to ResourceValidationStatus
604 for (const auto &fileInfo : parse_result.file_infos) {
606 status.filepath = fileInfo.filepath;
607 status.exists = true; // If parsing reached here, file exists
608 status.is_valid_xml = fileInfo.parsed_successfully;
609 status.has_behavior_trees = fileInfo.has_behavior_trees;
610 status.has_blackboards = fileInfo.has_blackboards;
611 status.tree_count = fileInfo.tree_count;
612 status.blackboard_count = fileInfo.blackboard_count;
613 status.tree_ids = fileInfo.tree_ids;
614 status.blackboard_ids = fileInfo.blackboard_ids;
615 status.subtree_refs = fileInfo.subtree_refs;
616 status.blackboard_includes = fileInfo.blackboard_includes;
617 status.errors = fileInfo.errors;
618 status.warnings = fileInfo.warnings;
619
620 report.resource_statuses.push_back(status);
621
622 // Propagate file-level errors to top-level report
623 for (const auto &error : fileInfo.errors) {
624 report.errors.push_back(error);
625 }
626
627 // Propagate file-level warnings to top-level report
628 for (const auto &warning : fileInfo.warnings) {
629 report.warnings.push_back(warning);
630 }
631 }
632
633 // Convert ParseError to strings
634 for (const auto &err : parse_result.errors) {
635 report.errors.push_back(err.message + " (" + err.file_path + ")");
636 }
637
638 // Append project-level warnings (don't overwrite file-level warnings)
639 for (const auto &warning : parse_result.warnings) {
640 report.warnings.push_back(warning);
641 }
642
643 // Copy tree statuses
644 report.tree_statuses = parse_result.tree_statuses;
645
646 // Copy unimplemented references
647 report.unimplemented_trees = parse_result.unimplemented_references;
648
649 // Copy blackboard issues
650 report.unresolved_blackboard_includes = parse_result.unresolved_blackboard_includes;
651 report.duplicate_blackboard_ids = parse_result.duplicate_blackboard_ids;
652
653 // Copy circular references
654 report.circular_references = parse_result.circular_references;
655
656 // Add duplicate tree IDs to warnings and errors
657 for (const auto &dup_id : parse_result.duplicate_tree_ids) {
658 // Find which files have this duplicate and add to their status
659 for (auto &status : report.resource_statuses) {
660 if (std::find(status.tree_ids.begin(), status.tree_ids.end(), dup_id) != status.tree_ids.end()) {
661 status.warnings.push_back("Duplicate tree ID found: " + dup_id);
662 }
663 }
664 }
665
666 // Add duplicate blackboard IDs to warnings
667 for (const auto &dup_id : parse_result.duplicate_blackboard_ids) {
668 for (auto &status : report.resource_statuses) {
669 if (std::find(status.blackboard_ids.begin(), status.blackboard_ids.end(), dup_id) !=
670 status.blackboard_ids.end()) {
671 status.warnings.push_back("Duplicate blackboard ID found: " + dup_id);
672 }
673 }
674 }
675
676 // Add unresolved blackboard includes to warnings
677 for (const auto &unresolved : parse_result.unresolved_blackboard_includes) {
678 for (auto &status : report.resource_statuses) {
679 if (std::find(status.blackboard_includes.begin(), status.blackboard_includes.end(), unresolved) !=
680 status.blackboard_includes.end()) {
681 status.warnings.push_back("Unresolved blackboard include: " + unresolved);
682 }
683 }
684 }
685
686 // Determine overall validity
687 report.is_valid = parse_result.success && report.errors.empty();
688
689 // Even with warnings, the project can be valid
690 if (std::any_of(report.resource_statuses.begin(), report.resource_statuses.end(),
691 [](const ResourceValidationStatus &status) { return !status.IsValid(); })) {
692 report.is_valid = false;
693 }
694
695 return report;
696}
697
698void BehaviorTreeProject::SetTreeImplementationStatus(const std::map<String, TreeImplementationStatus> &statuses) {
699 tree_statuses_ = statuses;
700}
701
703 std::vector<String> unimplemented;
704 for (const auto &pair : tree_statuses_) {
705 const auto &status = pair.second;
706 if (!status.is_implemented && !status.referenced_in_files.empty()) {
707 unimplemented.push_back(pair.first);
708 }
709 }
710 return unimplemented;
711}
712
713std::vector<String> BehaviorTreeProject::GetImplementedTrees() const {
714 std::vector<String> implemented;
715 for (const auto &pair : tree_statuses_) {
716 if (pair.second.is_implemented) {
717 implemented.push_back(pair.first);
718 }
719 }
720 return implemented;
721}
722
724 auto it = tree_statuses_.find(tree_id);
725 if (it != tree_statuses_.end()) {
726 return it->second.is_implemented;
727 }
728 return false;
729}
730
731nlohmann::json BehaviorTreeProject::ToJson() const {
732 nlohmann::json json;
733
734 json["project_name"] = name_;
735 json["description"] = description_;
736 json["version"] = version_;
737 json["created_timestamp"] = created_timestamp_;
738 json["modified_timestamp"] = modified_timestamp_;
739 json["parser_profile"] = parser_profile_name_;
740 json["resources"] = resources_;
741
742 // Serialize tree implementation statuses
743 nlohmann::json tree_statuses_json = nlohmann::json::array();
744 for (const auto &pair : tree_statuses_) {
745 const auto &status = pair.second;
746 nlohmann::json status_json;
747 status_json["tree_id"] = status.tree_id;
748 status_json["is_implemented"] = status.is_implemented;
749 status_json["has_root_node"] = status.has_root_node;
750 status_json["defined_in_file"] = status.defined_in_file;
751 status_json["referenced_in_files"] = status.referenced_in_files;
752 tree_statuses_json.push_back(status_json);
753 }
754 json["tree_statuses"] = tree_statuses_json;
755
756 return json;
757}
758
759void BehaviorTreeProject::FromJson(const nlohmann::json &json) {
760 try {
761 // Validate required fields
762 if (!json.contains("project_name")) {
763 throw std::runtime_error("Missing required field: project_name");
764 }
765
766 // Load required fields
767 name_ = json["project_name"].get<String>();
768
769 // Load optional fields
770 if (json.contains("description")) {
771 description_ = json["description"].get<String>();
772 }
773 if (json.contains("version")) {
774 version_ = json["version"].get<String>();
775 }
776 if (json.contains("created_timestamp")) {
777 created_timestamp_ = json["created_timestamp"].get<int64_t>();
778 }
779 if (json.contains("modified_timestamp")) {
780 modified_timestamp_ = json["modified_timestamp"].get<int64_t>();
781 }
782 if (json.contains("parser_profile")) {
783 parser_profile_name_ = json["parser_profile"].get<String>();
784 }
785 if (json.contains("resources")) {
786 resources_ = json["resources"].get<std::vector<String>>();
787 }
788
789 // Load tree statuses
790 tree_statuses_.clear();
791 if (json.contains("tree_statuses")) {
792 for (const auto &status_json : json["tree_statuses"]) {
794 status.tree_id = status_json["tree_id"].get<String>();
795 status.is_implemented = status_json.value("is_implemented", false);
796 status.has_root_node = status_json.value("has_root_node", false);
797 status.defined_in_file = status_json.value("defined_in_file", "");
798 if (status_json.contains("referenced_in_files")) {
799 status.referenced_in_files = status_json["referenced_in_files"].get<std::vector<String>>();
800 }
801 tree_statuses_[status.tree_id] = status;
802 }
803 }
804
805 } catch (const std::exception &e) {
806 LOG_ERROR("BehaviorTreeProject", "Error parsing JSON project: " + std::string(e.what()));
807 throw;
808 }
809}
810
812 try {
813 project_filepath_ = filepath;
814
815 nlohmann::json json = ToJson();
816 std::ofstream file(filepath);
817
818 if (!file.is_open()) {
819 LOG_ERROR("BehaviorTreeProject", "Failed to open file for writing: " + filepath);
820 return false;
821 }
822
823 // Write with pretty formatting (4 space indent)
824 file << std::setw(4) << json << std::endl;
825 file.close();
826
827 LOG_INFO("BehaviorTreeProject", "Saved project '" + name_ + "' to: " + filepath);
828 return true;
829 } catch (const std::exception &e) {
830 LOG_ERROR("BehaviorTreeProject", "Error saving project to file: " + std::string(e.what()));
831 return false;
832 }
833}
834
836 try {
837 std::ifstream file(filepath);
838
839 if (!file.is_open()) {
840 LOG_ERROR("BehaviorTreeProject", "Failed to open file for reading: " + filepath);
841 return false;
842 }
843
844 nlohmann::json json;
845 file >> json;
846 file.close();
847
848 FromJson(json);
849 project_filepath_ = filepath;
850
851 LOG_INFO("BehaviorTreeProject", "Loaded project '" + name_ + "' from: " + filepath);
852 return true;
853 } catch (const std::exception &e) {
854 LOG_ERROR("BehaviorTreeProject", "Error loading project from file: " + std::string(e.what()));
855 return false;
856 }
857}
858
859std::shared_ptr<BehaviorTreeProject> BehaviorTreeProject::Clone(const String &new_name) const {
860 String cloned_name = new_name.empty() ? name_ + " (Copy)" : new_name;
861 auto cloned = std::make_shared<BehaviorTreeProject>(cloned_name, description_);
862
863 cloned->SetVersion(version_);
864 cloned->SetParserProfileName(parser_profile_name_);
865
866 // Copy resources
867 for (const auto &resource : resources_) {
868 cloned->AddResource(resource);
869 }
870
871 // Copy tree statuses
872 cloned->SetTreeImplementationStatus(tree_statuses_);
873
874 return cloned;
875}
876
877std::shared_ptr<BehaviorTreeProject> BehaviorTreeProject::CreateEmpty(const String &name) {
878 return std::make_shared<BehaviorTreeProject>(name);
879}
880
882 auto now = std::chrono::system_clock::now();
883 auto duration = now.time_since_epoch();
884 return std::chrono::duration_cast<std::chrono::seconds>(duration).count();
885}
886
887} // namespace EmberCore
#define PATH_SEPARATOR
#define LOG_ERROR(category, message)
Definition Logger.h:116
#define LOG_WARNING(category, message)
Definition Logger.h:115
#define LOG_INFO(category, message)
Definition Logger.h:114
String MakeRelativePath(const String &absolute_path) const
String project_filepath_
Path to the project file itself.
std::shared_ptr< BehaviorTreeProject > Clone(const String &new_name="") const
std::map< String, TreeImplementationStatus > tree_statuses_
bool AddResource(const String &filepath)
bool LoadFromFile(const String &filepath)
ProjectValidationReport ValidateWithParser(class LibXMLBehaviorTreeParser *parser) const
std::vector< String > GetImplementedTrees() const
ResourceValidationStatus ValidateSingleResource(const String &filepath) const
void SetParserProfileName(const String &profile_name)
void SetTreeImplementationStatus(const std::map< String, TreeImplementationStatus > &statuses)
std::vector< String > GetUnimplementedReferences() const
bool SaveToFile(const String &filepath)
void FromJson(const nlohmann::json &json)
static std::shared_ptr< BehaviorTreeProject > CreateEmpty(const String &name)
bool HasResource(const String &filepath) const
String parser_profile_name_
Name of the parser profile to use.
bool RemoveResource(const String &filepath)
ProjectValidationReport ValidateResources() const
bool IsTreeImplemented(const String &tree_id) const
std::vector< String > resources_
List of XML file paths (relative to project file)
String ResolveResourcePath(const String &resource_path) const
void SetDescription(const String &description)
Thread-safe XML parser using libxml2 for behavior tree files.
ProjectParseResult ParseProject(BehaviorTreeProject *project)
Main types header for EmberCore.
static void ExtractSubTreeReferences(xmlNodePtr node, std::set< String > &subtree_refs)
std::string String
Framework-agnostic string type.
Definition String.h:14
Complete validation report for a project.
std::vector< String > unresolved_blackboard_includes
Blackboard includes not found in project.
std::vector< String > warnings
Project-level warnings.
std::vector< ResourceValidationStatus > resource_statuses
Status for each resource.
std::vector< String > unimplemented_trees
Trees referenced but not implemented.
std::vector< String > errors
Project-level errors.
std::vector< String > duplicate_blackboard_ids
Duplicate blackboard IDs across files.
bool is_valid
Overall validation status.
std::vector< String > circular_references
Circular reference chains detected.
std::map< String, TreeImplementationStatus > tree_statuses
Status for each tree.
Resource validation status for a single file.
int blackboard_count
Number of blackboards found in the file.
std::vector< String > blackboard_ids
IDs of blackboards found in the file.
std::vector< String > errors
Validation errors for this file.
std::vector< String > tree_ids
IDs of trees found in the file.
std::vector< String > blackboard_includes
IDs referenced via includes="..." attribute.
int tree_count
Number of trees found in the file.
bool has_behavior_trees
File contains BehaviorTree elements.
std::vector< String > subtree_refs
IDs of SubTrees referenced in this file.
std::vector< String > warnings
Validation warnings for this file.
bool has_blackboards
File contains Blackboard elements.
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)
std::vector< String > referenced_in_files
Which files reference this tree via SubTree.