7#include <libxml/parser.h>
8#include <libxml/tree.h>
15#define PATH_SEPARATOR '\\'
18#define PATH_SEPARATOR '/'
39 [](
const auto &pair) { return pair.second.is_implemented; });
43 std::ostringstream report;
45 report <<
"=== BehaviorTree Project Validation Report ===\n\n";
50 report <<
"VALID FILES (" << valid_count <<
"):\n";
52 if (status.IsValid()) {
53 report <<
" [OK] " << status.filepath <<
" (" << status.tree_count <<
" trees)\n";
60 if (invalid_count > 0) {
61 report <<
"INVALID FILES (" << invalid_count <<
"):\n";
63 if (!status.IsValid()) {
64 report <<
" [ERROR] " << status.filepath <<
"\n";
65 for (
const auto &err : status.errors) {
66 report <<
" - " << err <<
"\n";
77 report <<
" [WARN] " << warn <<
"\n";
80 report <<
" [WARN] Tree '" << tree <<
"' is referenced but not implemented\n";
88 for (
const auto &err :
errors) {
89 report <<
" [ERROR] " << err <<
"\n";
92 report <<
" [ERROR] Circular reference detected: " << ref <<
"\n";
98 report <<
"TREE REFERENCE MAP:\n";
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";
105 report <<
" [!!] " <<
id <<
" (not implemented)\n";
107 for (
const auto &ref_file : status.referenced_in_files) {
108 report <<
" <- referenced by " << ref_file <<
"\n";
147 LOG_WARNING(
"BehaviorTreeProject",
"Resource already exists in project: " + filepath);
153 LOG_INFO(
"BehaviorTreeProject",
"Added resource: " + filepath);
162 LOG_INFO(
"BehaviorTreeProject",
"Removed resource: " + filepath);
166 LOG_WARNING(
"BehaviorTreeProject",
"Resource not found in project: " + filepath);
185 _getcwd(cwd,
sizeof(cwd));
187 getcwd(cwd,
sizeof(cwd));
194 if (last_sep != String::npos) {
202 if (!resource_path.empty() &&
203 (resource_path[0] ==
'/' || (resource_path.length() > 1 && resource_path[1] ==
':'))) {
204 return resource_path;
209 if (base.empty() || base ==
".") {
210 return resource_path;
219 return absolute_path;
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(),
'\\',
'/');
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);
238 return absolute_path;
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);
251 if (name && strcmp(name,
"SubTree") == 0) {
252 xmlChar *id_attr = xmlGetProp(child,
reinterpret_cast<const xmlChar *
>(
"ID"));
254 subtree_refs.insert(
reinterpret_cast<const char *
>(id_attr));
272 std::ifstream file(resolved_path);
274 status.
errors.push_back(
"File not found: " + resolved_path);
281 xmlDocPtr doc = xmlReadFile(resolved_path.c_str(),
nullptr, XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
283 status.
errors.push_back(
"Invalid XML syntax");
289 xmlNodePtr root = xmlDocGetRootElement(doc);
292 status.
errors.push_back(
"XML document has no root element");
297 std::set<String> subtree_refs;
300 std::set<String> blackboard_includes;
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) {
310 xmlChar *id_attr = xmlGetProp(child,
reinterpret_cast<const xmlChar *
>(
"ID"));
312 status.
tree_ids.push_back(
reinterpret_cast<const char *
>(id_attr));
318 }
else if (name && strcmp(name,
"Blackboard") == 0) {
324 xmlChar *id_attr = xmlGetProp(child,
reinterpret_cast<const xmlChar *
>(
"ID"));
326 bbId =
reinterpret_cast<const char *
>(id_attr);
330 status.
errors.push_back(
"Blackboard element missing required 'ID' attribute");
334 xmlChar *includes_attr = xmlGetProp(child,
reinterpret_cast<const xmlChar *
>(
"includes"));
336 String includes_str =
reinterpret_cast<const char *
>(includes_attr);
337 xmlFree(includes_attr);
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 ...})");
346 includes_str = includes_str.substr(1, includes_str.size() - 2);
348 std::istringstream iss(includes_str);
350 while (iss >> token) {
351 if (!token.empty()) {
352 blackboard_includes.insert(token);
360 std::set<String> seenKeys;
361 for (xmlNodePtr entryNode = child->children; entryNode; entryNode = entryNode->next) {
362 if (entryNode->type != XML_ELEMENT_NODE)
364 const char *entryName =
reinterpret_cast<const char *
>(entryNode->name);
365 if (!entryName || strcmp(entryName,
"Entry") != 0)
369 xmlChar *keyAttr = xmlGetProp(entryNode,
reinterpret_cast<const xmlChar *
>(
"key"));
371 status.
errors.push_back(
"Blackboard '" + bbId +
"' has Entry missing required 'key' attribute");
374 String entryKey =
reinterpret_cast<const char *
>(keyAttr);
378 if (seenKeys.count(entryKey) > 0) {
379 status.
warnings.push_back(
"Blackboard '" + bbId +
"' has duplicate entry key: " + entryKey);
381 seenKeys.insert(entryKey);
384 xmlChar *typeAttr = xmlGetProp(entryNode,
reinterpret_cast<const xmlChar *
>(
"type"));
386 status.
errors.push_back(
"Blackboard '" + bbId +
"' entry '" + entryKey +
387 "' missing required 'type' attribute");
390 String entryType =
reinterpret_cast<const char *
>(typeAttr);
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",
401 "Results",
"ReelFigs",
"GameSounds"};
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;
408 if (!isValidType && !isCustomType) {
409 status.
warnings.push_back(
"Blackboard '" + bbId +
"' entry '" + entryKey +
410 "' has unsupported type: " + entryType);
418 status.
subtree_refs.assign(subtree_refs.begin(), subtree_refs.end());
424 status.
warnings.push_back(
"No BehaviorTree or Blackboard elements found in file");
434 report.
errors.push_back(
"Project has no resources");
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;
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) {
452 status.
warnings.push_back(
"Duplicate tree ID within file: " + tree_id);
454 seen_in_file.insert(tree_id);
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);
467 all_tree_ids.insert(tree_id);
481 all_subtree_refs.insert(subtree_id);
482 subtree_to_files[subtree_id].push_back(resource);
487 for (
const auto &subtree_id : all_subtree_refs) {
488 if (all_tree_ids.find(subtree_id) == all_tree_ids.end()) {
500 report.
tree_statuses[subtree_id].referenced_in_files = subtree_to_files[subtree_id];
505 for (
const auto &dup : duplicate_tree_ids) {
506 report.
warnings.push_back(
"Duplicate tree ID found: " + dup);
510 std::set<String> all_blackboard_ids;
511 std::set<String> duplicate_bb_ids;
512 std::set<String> all_bb_includes;
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);
521 seen_bb_in_file.insert(bb_id);
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);
530 all_blackboard_ids.insert(bb_id);
534 for (
const auto &inc : res_status.blackboard_includes) {
535 all_bb_includes.insert(inc);
540 for (
const auto &dup : duplicate_bb_ids) {
542 report.
warnings.push_back(
"Duplicate blackboard ID found: " + dup);
546 for (
const auto &inc : all_bb_includes) {
547 if (all_blackboard_ids.find(inc) == all_blackboard_ids.end()) {
549 report.
warnings.push_back(
"Unresolved blackboard include: " + inc);
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);
567 const auto &
id = pair.first;
568 const auto &status = pair.second;
569 if (!status.is_implemented) {
590 report.
errors.push_back(
"Parser is null");
596 report.
errors.push_back(
"No resources in project");
604 for (
const auto &fileInfo : parse_result.file_infos) {
606 status.
filepath = fileInfo.filepath;
613 status.
tree_ids = fileInfo.tree_ids;
617 status.
errors = fileInfo.errors;
618 status.
warnings = fileInfo.warnings;
623 for (
const auto &error : fileInfo.errors) {
624 report.
errors.push_back(error);
628 for (
const auto &warning : fileInfo.warnings) {
634 for (
const auto &err : parse_result.errors) {
635 report.
errors.push_back(err.message +
" (" + err.file_path +
")");
639 for (
const auto &warning : parse_result.warnings) {
657 for (
const auto &dup_id : parse_result.duplicate_tree_ids) {
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);
667 for (
const auto &dup_id : parse_result.duplicate_blackboard_ids) {
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);
677 for (
const auto &unresolved : parse_result.unresolved_blackboard_includes) {
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);
703 std::vector<String> unimplemented;
705 const auto &status = pair.second;
706 if (!status.is_implemented && !status.referenced_in_files.empty()) {
707 unimplemented.push_back(pair.first);
710 return unimplemented;
714 std::vector<String> implemented;
716 if (pair.second.is_implemented) {
717 implemented.push_back(pair.first);
726 return it->second.is_implemented;
734 json[
"project_name"] =
name_;
743 nlohmann::json tree_statuses_json = nlohmann::json::array();
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);
754 json[
"tree_statuses"] = tree_statuses_json;
762 if (!json.contains(
"project_name")) {
763 throw std::runtime_error(
"Missing required field: project_name");
770 if (json.contains(
"description")) {
773 if (json.contains(
"version")) {
776 if (json.contains(
"created_timestamp")) {
779 if (json.contains(
"modified_timestamp")) {
782 if (json.contains(
"parser_profile")) {
785 if (json.contains(
"resources")) {
786 resources_ = json[
"resources"].get<std::vector<String>>();
791 if (json.contains(
"tree_statuses")) {
792 for (
const auto &status_json : json[
"tree_statuses"]) {
795 status.
is_implemented = status_json.value(
"is_implemented",
false);
796 status.
has_root_node = status_json.value(
"has_root_node",
false);
798 if (status_json.contains(
"referenced_in_files")) {
799 status.
referenced_in_files = status_json[
"referenced_in_files"].get<std::vector<String>>();
805 }
catch (
const std::exception &e) {
806 LOG_ERROR(
"BehaviorTreeProject",
"Error parsing JSON project: " + std::string(e.what()));
815 nlohmann::json json =
ToJson();
816 std::ofstream file(filepath);
818 if (!file.is_open()) {
819 LOG_ERROR(
"BehaviorTreeProject",
"Failed to open file for writing: " + filepath);
824 file << std::setw(4) << json << std::endl;
827 LOG_INFO(
"BehaviorTreeProject",
"Saved project '" +
name_ +
"' to: " + filepath);
829 }
catch (
const std::exception &e) {
830 LOG_ERROR(
"BehaviorTreeProject",
"Error saving project to file: " + std::string(e.what()));
837 std::ifstream file(filepath);
839 if (!file.is_open()) {
840 LOG_ERROR(
"BehaviorTreeProject",
"Failed to open file for reading: " + filepath);
851 LOG_INFO(
"BehaviorTreeProject",
"Loaded project '" +
name_ +
"' from: " + filepath);
853 }
catch (
const std::exception &e) {
854 LOG_ERROR(
"BehaviorTreeProject",
"Error loading project from file: " + std::string(e.what()));
860 String cloned_name = new_name.empty() ?
name_ +
" (Copy)" : new_name;
861 auto cloned = std::make_shared<BehaviorTreeProject>(cloned_name,
description_);
868 cloned->AddResource(resource);
878 return std::make_shared<BehaviorTreeProject>(name);
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();
#define LOG_ERROR(category, message)
#define LOG_WARNING(category, message)
#define LOG_INFO(category, message)
void SetName(const String &name)
void UpdateModifiedTimestamp()
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
int64_t created_timestamp_
ResourceValidationStatus ValidateSingleResource(const String &filepath) const
void SetParserProfileName(const String &profile_name)
static int64_t GetCurrentTimestamp()
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.
nlohmann::json ToJson() const
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)
String GetBaseDirectory() const
int64_t modified_timestamp_
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.
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.
int GetValidFileCount() const
int GetTotalTreeCount() const
std::vector< String > errors
Project-level errors.
std::vector< String > duplicate_blackboard_ids
Duplicate blackboard IDs across files.
bool is_valid
Overall validation status.
int GetImplementedTreeCount() const
std::vector< String > circular_references
Circular reference chains detected.
String GenerateReport() const
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.
String filepath
Path to the file.
std::vector< String > blackboard_ids
IDs of blackboards found in the file.
std::vector< String > errors
Validation errors for this file.
bool exists
File exists on disk.
bool is_valid_xml
File is valid XML.
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 tree_id
Tree identifier.
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.