• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BehaviorTree / BehaviorTree.CPP / 21668978307

04 Feb 2026 11:04AM UTC coverage: 79.742% (+0.1%) from 79.593%
21668978307

Pull #1105

github

web-flow
Merge 1716a60b1 into e15354126
Pull Request #1105: Fix #880: createTreeFromText now finds previously registered subtrees

50 of 50 new or added lines in 1 file covered. (100.0%)

5 existing lines in 1 file now uncovered.

4692 of 5884 relevant lines covered (79.74%)

22625.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.01
/src/xml_parsing.cpp
1
/*  Copyright (C) 2018-2025 Davide Faconti, Eurecat -  All Rights Reserved
2
*
3
*   Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
4
*   to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
5
*   and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
*   The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
*
8
*   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9
*   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
10
*   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11
*/
12

13
#include "behaviortree_cpp/basic_types.h"
14

15
#include <cstdio>
16
#include <cstring>
17
#include <functional>
18
#include <iostream>
19
#include <list>
20
#include <sstream>
21
#include <string>
22
#include <tuple>
23
#include <typeindex>
24
#include <unordered_set>
25

26
#if defined(_MSVC_LANG) && !defined(__clang__)
27
#define __bt_cplusplus (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
28
#else
29
#define __bt_cplusplus __cplusplus
30
#endif
31

32
#if defined(__linux) || defined(__linux__)
33
#pragma GCC diagnostic push
34
#pragma GCC diagnostic ignored "-Wattributes"
35
#pragma GCC diagnostic ignored "-Wtype-limits"
36
#endif
37

38
#include "tinyxml2.h"
39

40
#if defined(__linux) || defined(__linux__)
41
#pragma GCC diagnostic pop
42
#endif
43

44
#include "behaviortree_cpp/xml_parsing.h"
45

46
#include <filesystem>
47
#include <map>
48

49
#ifdef USING_ROS2
50
#include <ament_index_cpp/get_package_share_directory.hpp>
51
#endif
52

53
#include "behaviortree_cpp/blackboard.h"
54
#include "behaviortree_cpp/tree_node.h"
55
#include "behaviortree_cpp/utils/demangle_util.h"
56

57
namespace
58
{
59
std::string xsdAttributeType(const BT::PortInfo& port_info)
×
60
{
61
  if(port_info.direction() == BT::PortDirection::OUTPUT)
×
62
  {
63
    return "blackboardType";
×
64
  }
65
  const auto& type_info = port_info.type();
×
66
  if((type_info == typeid(int)) || (type_info == typeid(unsigned int)))
×
67
  {
68
    return "integerOrBlackboardType";
×
69
  }
70
  else if(type_info == typeid(double))
×
71
  {
72
    return "decimalOrBlackboardType";
×
73
  }
74
  else if(type_info == typeid(bool))
×
75
  {
76
    return "booleanOrBlackboardType";
×
77
  }
78
  else if(type_info == typeid(std::string))
×
79
  {
80
    return "stringOrBlackboardType";
×
81
  }
82

83
  return std::string();
×
84
}
85

86
}  // namespace
87

88
namespace BT
89
{
90
using namespace tinyxml2;
91

92
namespace
93
{
94
auto StrEqual = [](const char* str1, const char* str2) -> bool {
286✔
95
  return strcmp(str1, str2) == 0;
286✔
96
};
97

98
// Helper to format forbidden character for error messages
99
std::string formatForbiddenChar(char c)
4✔
100
{
101
  if(c < 32 || c == 127)
4✔
102
  {
103
    return "control character (ASCII " + std::to_string(static_cast<int>(c)) + ")";
×
104
  }
105
  return std::string("'") + c + "'";
12✔
106
}
107

108
void validateModelName(const std::string& name, int line_number)
1,423✔
109
{
110
  const auto line_str = std::to_string(line_number);
1,423✔
111
  if(name.empty())
1,423✔
112
  {
113
    throw RuntimeError("Error at line ", line_str,
114
                       ": Model/Node type name cannot be empty");
×
115
  }
116
  if(name == "Root" || name == "root")
1,423✔
117
  {
118
    throw RuntimeError("Error at line ", line_str,
119
                       ": 'Root' is a reserved name and cannot be used as a node type");
2✔
120
  }
121
  if(char c = findForbiddenChar(name); c != '\0')
1,421✔
122
  {
123
    throw RuntimeError("Error at line ", line_str, ": Model name '", name,
124
                       "' contains forbidden character ", formatForbiddenChar(c));
3✔
125
  }
126
}
1,423✔
127

128
void validatePortName(const std::string& name, int line_number)
8✔
129
{
130
  const auto line_str = std::to_string(line_number);
8✔
131
  if(name.empty())
8✔
132
  {
133
    throw RuntimeError("Error at line ", line_str, ": Port name cannot be empty");
×
134
  }
135
  if(std::isdigit(static_cast<unsigned char>(name[0])) != 0)
8✔
136
  {
137
    throw RuntimeError("Error at line ", line_str, ": Port name '", name,
138
                       "' cannot start with a digit");
1✔
139
  }
140
  if(char c = findForbiddenChar(name); c != '\0')
7✔
141
  {
142
    throw RuntimeError("Error at line ", line_str, ": Port name '", name,
143
                       "' contains forbidden character ", formatForbiddenChar(c));
1✔
144
  }
145
  if(IsReservedAttribute(name))
6✔
146
  {
147
    throw RuntimeError("Error at line ", line_str, ": Port name '", name,
148
                       "' is a reserved attribute name");
1✔
149
  }
150
}
8✔
151

152
void validateInstanceName(const std::string& name, int line_number)
120✔
153
{
154
  // Instance name CAN be empty (defaults to model name)
155
  // Instance names are XML attribute VALUES, so they can contain spaces,
156
  // periods, and most characters. We only reject control characters that
157
  // are invalid in XML.
158
  if(name.empty())
120✔
159
  {
160
    return;
×
161
  }
162
  for(const char c : name)
1,038✔
163
  {
164
    const auto uc = static_cast<unsigned char>(c);
918✔
165
    // Only reject control characters that are invalid in XML
166
    // (XML allows tab=0x09, newline=0x0A, carriage return=0x0D)
167
    if(uc < 32 && uc != 0x09 && uc != 0x0A && uc != 0x0D)
918✔
168
    {
169
      const auto line_str = std::to_string(line_number);
×
170
      throw RuntimeError("Error at line ", line_str, ": Instance name '", name,
171
                         "' contains invalid control character (ASCII ",
172
                         std::to_string(static_cast<int>(uc)), ")");
×
173
    }
×
174
    if(uc == 127)
918✔
175
    {
176
      const auto line_str = std::to_string(line_number);
×
177
      throw RuntimeError("Error at line ", line_str, ": Instance name '", name,
178
                         "' contains invalid control character (ASCII 127)");
×
179
    }
×
180
  }
181
}
182

183
}  // namespace
184

185
struct SubtreeModel
186
{
187
  std::unordered_map<std::string, BT::PortInfo> ports;
188
};
189

190
struct XMLParser::PImpl
191
{
192
  TreeNode::Ptr createNodeFromXML(const XMLElement* element,
193
                                  const Blackboard::Ptr& blackboard,
194
                                  const TreeNode::Ptr& node_parent,
195
                                  const std::string& prefix_path, Tree& output_tree);
196

197
  void recursivelyCreateSubtree(const std::string& tree_ID, const std::string& tree_path,
198
                                const std::string& prefix_path, Tree& output_tree,
199
                                Blackboard::Ptr blackboard,
200
                                const TreeNode::Ptr& root_node,
201
                                std::unordered_set<std::string>& ancestors);
202

203
  void getPortsRecursively(const XMLElement* element,
204
                           std::vector<std::string>& output_ports);
205

206
  void loadDocImpl(XMLDocument* doc, bool add_includes);
207

208
  std::list<std::unique_ptr<XMLDocument> > opened_documents;
209
  std::map<std::string, const XMLElement*> tree_roots;
210

211
  const BehaviorTreeFactory* factory = nullptr;
212

213
  std::filesystem::path current_path;
214
  std::map<std::string, SubtreeModel> subtree_models;
215

216
  int suffix_count = 0;
217

218
  explicit PImpl(const BehaviorTreeFactory& fact)
292✔
219
    : factory(&fact), current_path(std::filesystem::current_path())
292✔
220
  {}
292✔
221

222
  void clear()
×
223
  {
224
    suffix_count = 0;
×
225
    current_path = std::filesystem::current_path();
×
226
    opened_documents.clear();
×
227
    tree_roots.clear();
×
228
  }
×
229

230
private:
231
  void loadSubtreeModel(const XMLElement* xml_root);
232
};
233

234
#if defined(__linux) || defined(__linux__)
235
#pragma GCC diagnostic pop
236
#endif
237

238
XMLParser::XMLParser(const BehaviorTreeFactory& factory) : _p(new PImpl(factory))
292✔
239
{}
292✔
240

241
XMLParser::XMLParser(XMLParser&& other) noexcept : _p(std::move(other._p))
×
242
{}
×
243

244
XMLParser& XMLParser::operator=(XMLParser&& other) noexcept
×
245
{
246
  this->_p = std::move(other._p);
×
247
  return *this;
×
248
}
249

250
XMLParser::~XMLParser()
292✔
251
{}
292✔
252

253
void XMLParser::loadFromFile(const std::filesystem::path& filepath, bool add_includes)
6✔
254
{
255
  _p->opened_documents.push_back(std::make_unique<XMLDocument>());
6✔
256

257
  XMLDocument* doc = _p->opened_documents.back().get();
6✔
258
  doc->LoadFile(filepath.string().c_str());
6✔
259

260
  _p->current_path = std::filesystem::absolute(filepath.parent_path());
6✔
261

262
  _p->loadDocImpl(doc, add_includes);
6✔
263
}
6✔
264

265
void XMLParser::loadFromText(const std::string& xml_text, bool add_includes)
274✔
266
{
267
  _p->opened_documents.push_back(std::make_unique<XMLDocument>());
274✔
268

269
  XMLDocument* doc = _p->opened_documents.back().get();
274✔
270
  doc->Parse(xml_text.c_str(), xml_text.size());
274✔
271

272
  _p->loadDocImpl(doc, add_includes);
274✔
273
}
251✔
274

275
std::vector<std::string> XMLParser::registeredBehaviorTrees() const
229✔
276
{
277
  std::vector<std::string> out;
229✔
278
  for(const auto& it : _p->tree_roots)
352✔
279
  {
280
    out.push_back(it.first);
123✔
281
  }
282
  return out;
229✔
283
}
×
284

285
void BT::XMLParser::PImpl::loadSubtreeModel(const XMLElement* xml_root)
268✔
286
{
287
  for(auto models_node = xml_root->FirstChildElement("TreeNodesModel");
268✔
288
      models_node != nullptr; models_node = models_node->NextSiblingElement("TreeNodesMo"
270✔
289
                                                                            "del"))
290
  {
291
    for(auto sub_node = models_node->FirstChildElement("SubTree"); sub_node != nullptr;
7✔
292
        sub_node = sub_node->NextSiblingElement("SubTree"))
2✔
293
    {
294
      auto subtree_id = sub_node->Attribute("ID");
5✔
295
      auto& subtree_model = subtree_models[subtree_id];
10✔
296

297
      const std::pair<const char*, BT::PortDirection> port_types[3] = {
5✔
298
        { "input_port", BT::PortDirection::INPUT },
299
        { "output_port", BT::PortDirection::OUTPUT },
300
        { "inout_port", BT::PortDirection::INOUT }
301
      };
302

303
      for(const auto& [name, direction] : port_types)
11✔
304
      {
305
        for(auto port_node = sub_node->FirstChildElement(name); port_node != nullptr;
14✔
306
            port_node = port_node->NextSiblingElement(name))
5✔
307
        {
308
          BT::PortInfo port(direction);
8✔
309
          auto name = port_node->Attribute("name");
8✔
310
          if(name == nullptr)
8✔
311
          {
312
            throw RuntimeError("Missing attribute [name] in port (SubTree model)");
×
313
          }
314
          // Validate port name
315
          validatePortName(name, port_node->GetLineNum());
19✔
316
          if(auto default_value = port_node->Attribute("default"))
5✔
317
          {
318
            port.setDefaultValue(default_value);
2✔
319
          }
320
          if(auto description = port_node->Attribute("description"))
5✔
321
          {
322
            port.setDescription(description);
×
323
          }
324
          subtree_model.ports[name] = std::move(port);
10✔
325
        }
8✔
326
      }
327
    }
328
  }
329
}
265✔
330

331
void XMLParser::PImpl::loadDocImpl(XMLDocument* doc, bool add_includes)
288✔
332
{
333
  if(doc->Error())
288✔
334
  {
335
    char buffer[512];
336
    std::ignore =
337
        snprintf(buffer, sizeof buffer, "Error parsing the XML: %s", doc->ErrorStr());
2✔
338
    throw RuntimeError(buffer);
2✔
339
  }
340

341
  const XMLElement* xml_root = doc->RootElement();
286✔
342
  if(xml_root == nullptr)
286✔
343
  {
344
    throw RuntimeError("Invalid XML: missing root element");
×
345
  }
346

347
  auto format = xml_root->Attribute("BTCPP_format");
286✔
348
  if(format == nullptr)
286✔
349
  {
350
    std::cout << "Warnings: The first tag of the XML (<root>) should contain the "
351
                 "attribute [BTCPP_format=\"4\"]\n"
352
              << "Please check if your XML is compatible with version 4.x of BT.CPP"
×
353
              << std::endl;
×
354
  }
355

356
  // recursively include other files
357
  for(auto incl_node = xml_root->FirstChildElement("include"); incl_node != nullptr;
294✔
358
      incl_node = incl_node->NextSiblingElement("include"))
8✔
359
  {
360
    if(!add_includes)
8✔
361
    {
362
      break;
×
363
    }
364

365
    const char* path_attr = incl_node->Attribute("path");
8✔
366
    if(path_attr == nullptr)
8✔
367
    {
368
      throw RuntimeError("Invalid <include> tag: missing 'path' attribute");
×
369
    }
370

371
#if __bt_cplusplus >= 202002L
372
    auto file_path{ std::filesystem::path(path_attr) };
373
#else
374
    auto file_path{ std::filesystem::u8path(path_attr) };
8✔
375
#endif
376

377
    const char* ros_pkg_relative_path = incl_node->Attribute("ros_pkg");
8✔
378

379
    if(ros_pkg_relative_path != nullptr)
8✔
380
    {
381
      if(file_path.is_absolute())
×
382
      {
383
        std::cout << "WARNING: <include path=\"...\"> contains an absolute path.\n"
384
                  << "Attribute [ros_pkg] will be ignored." << std::endl;
×
385
      }
386
      else
387
      {
388
        std::string ros_pkg_path;  // NOLINT(misc-const-correctness)
×
389
#if defined USING_ROS2
390
        ros_pkg_path =
391
            ament_index_cpp::get_package_share_directory(ros_pkg_relative_path);
392
#else
393
        throw RuntimeError("Using attribute [ros_pkg] in <include>, but this library was "
394
                           "compiled without ROS support. Recompile the BehaviorTree.CPP "
395
                           "using catkin");
×
396
#endif
397
        file_path = std::filesystem::path(ros_pkg_path) / file_path;
398
      }
×
399
    }
400

401
    if(!file_path.is_absolute())
8✔
402
    {
403
      file_path = current_path / file_path;
8✔
404
    }
405

406
    opened_documents.push_back(std::make_unique<XMLDocument>());
8✔
407
    XMLDocument* next_doc = opened_documents.back().get();
8✔
408

409
    // change current path to the included file for handling additional relative paths
410
    const auto previous_path = current_path;
8✔
411
    current_path = std::filesystem::absolute(file_path.parent_path());
8✔
412

413
    next_doc->LoadFile(file_path.string().c_str());
8✔
414
    loadDocImpl(next_doc, add_includes);
8✔
415

416
    // reset current path to the previous value
417
    current_path = previous_path;
8✔
418
  }
8✔
419

420
  // Collect the names of all nodes registered with the behavior tree factory
421
  std::unordered_map<std::string, BT::NodeType> registered_nodes;
286✔
422
  for(const auto& it : factory->manifests())
12,360✔
423
  {
424
    registered_nodes.insert({ it.first, it.second.type });
12,074✔
425
  }
426

427
  XMLPrinter printer;
286✔
428
  doc->Print(&printer);
286✔
429
  auto xml_text = std::string(printer.CStr(), size_t(printer.CStrSize()));
286✔
430

431
  // Verify the validity of the XML before adding any behavior trees to the parser's list of registered trees
432
  VerifyXML(xml_text, registered_nodes);
286✔
433

434
  loadSubtreeModel(xml_root);
268✔
435

436
  // Register each BehaviorTree within the XML
437
  for(auto bt_node = xml_root->FirstChildElement("BehaviorTree"); bt_node != nullptr;
579✔
438
      bt_node = bt_node->NextSiblingElement("BehaviorTree"))
314✔
439
  {
440
    std::string tree_name;
314✔
441
    if(bt_node->Attribute("ID") != nullptr)
314✔
442
    {
443
      tree_name = bt_node->Attribute("ID");
204✔
444
    }
445
    else
446
    {
447
      tree_name = "BehaviorTree_" + std::to_string(suffix_count++);
110✔
448
    }
449

450
    tree_roots[tree_name] = bt_node;
314✔
451
  }
314✔
452
}
328✔
453

454
void VerifyXML(const std::string& xml_text,
286✔
455
               const std::unordered_map<std::string, BT::NodeType>& registered_nodes)
456
{
457
  XMLDocument doc;
286✔
458
  auto xml_error = doc.Parse(xml_text.c_str(), xml_text.size());
286✔
459
  if(xml_error != tinyxml2::XML_SUCCESS)
286✔
460
  {
461
    char buffer[512];
462
    std::ignore =
463
        snprintf(buffer, sizeof buffer, "Error parsing the XML: %s", doc.ErrorName());
×
464
    throw RuntimeError(buffer);
×
465
  }
466

467
  //-------- Helper functions (lambdas) -----------------
468
  auto ThrowError = [&](int line_num, const std::string& text) {
12✔
469
    char buffer[512];
470
    std::ignore = snprintf(buffer, sizeof buffer, "Error at line %d: -> %s", line_num,
12✔
471
                           text.c_str());
12✔
472
    throw RuntimeError(buffer);
12✔
473
  };
474

475
  auto ChildrenCount = [](const XMLElement* parent_node) {
1,549✔
476
    int count = 0;
1,549✔
477
    for(auto node = parent_node->FirstChildElement(); node != nullptr;
2,783✔
478
        node = node->NextSiblingElement())
1,234✔
479
    {
480
      count++;
1,234✔
481
    }
482
    return count;
1,549✔
483
  };
484
  //-----------------------------
485

486
  const XMLElement* xml_root = doc.RootElement();
286✔
487

488
  if(xml_root == nullptr || !StrEqual(xml_root->Name(), "root"))
286✔
489
  {
490
    throw RuntimeError("The XML must have a root node called <root>");
1✔
491
  }
492
  //-------------------------------------------------
493
  auto models_root = xml_root->FirstChildElement("TreeNodesModel");
285✔
494
  auto meta_sibling = models_root != nullptr ? models_root->NextSiblingElement("TreeNodes"
285✔
495
                                                                               "Model") :
496
                                               nullptr;
285✔
497

498
  if(meta_sibling != nullptr)
285✔
499
  {
500
    ThrowError(meta_sibling->GetLineNum(), " Only a single node <TreeNodesModel> is "
×
501
                                           "supported");
502
  }
503
  if(models_root != nullptr)
285✔
504
  {
505
    // not having a MetaModel is not an error. But consider that the
506
    // Graphical editor needs it.
507
    for(auto node = xml_root->FirstChildElement(); node != nullptr;
23✔
508
        node = node->NextSiblingElement())
16✔
509
    {
510
      const std::string name = node->Name();
16✔
511
      if(name == "Action" || name == "Decorator" || name == "SubTree" ||
32✔
512
         name == "Condition" || name == "Control")
32✔
513
      {
514
        const char* ID = node->Attribute("ID");
×
515
        if(ID == nullptr)
×
516
        {
517
          ThrowError(node->GetLineNum(), "Error at line %d: -> The attribute "
×
518
                                         "[ID] is mandatory");
519
        }
520
      }
521
    }
16✔
522
  }
523
  //-------------------------------------------------
524

525
  int behavior_tree_count = 0;
285✔
526
  for(auto child = xml_root->FirstChildElement(); child != nullptr;
639✔
527
      child = child->NextSiblingElement())
354✔
528
  {
529
    behavior_tree_count++;
354✔
530
  }
531

532
  // function to be called recursively
533
  constexpr int kMaxNestingDepth = 256;
285✔
534
  std::function<void(const XMLElement*, int)> recursiveStep;
285✔
535

536
  recursiveStep = [&](const XMLElement* node, int depth) {
×
537
    if(depth > kMaxNestingDepth)
1,550✔
538
    {
539
      ThrowError(node->GetLineNum(), "Maximum XML nesting depth exceeded (limit: " +
4✔
540
                                         std::to_string(kMaxNestingDepth) +
6✔
541
                                         "). The XML is too deeply nested.");
542
    }
543
    const int children_count = ChildrenCount(node);
1,549✔
544
    const std::string name = node->Name();
3,098✔
545
    const std::string ID = node->Attribute("ID") != nullptr ? node->Attribute("ID") : "";
1,549✔
546
    const int line_number = node->GetLineNum();
1,549✔
547

548
    // Precondition: built-in XML element types must define attribute [ID]
549
    const bool is_builtin =
550
        (name == "Decorator" || name == "Action" || name == "Condition" ||
3,096✔
551
         name == "Control" || name == "SubTree");
3,096✔
552
    if(is_builtin && ID.empty())
1,549✔
553
    {
554
      ThrowError(line_number,
×
555
                 std::string("The tag <") + name + "> must have the attribute [ID]");
×
556
    }
557

558
    if(name == "BehaviorTree")
1,549✔
559
    {
560
      if(ID.empty() && behavior_tree_count > 1)
334✔
561
      {
562
        ThrowError(line_number, "The tag <BehaviorTree> must have the attribute [ID]");
6✔
563
      }
564
      // Validate BehaviorTree ID as a model name
565
      if(!ID.empty())
332✔
566
      {
567
        validateModelName(ID, line_number);
222✔
568
      }
569
      if(registered_nodes.count(ID) != 0)
328✔
570
      {
571
        ThrowError(line_number, "The attribute [ID] of tag <BehaviorTree> must not use "
6✔
572
                                "the name of a registered Node");
573
      }
574
      if(children_count != 1)
326✔
575
      {
576
        ThrowError(line_number, "The tag <BehaviorTree> with ID '" + ID +
9✔
577
                                    "' must have exactly 1 child");
578
      }
579
    }
580
    else if(name == "SubTree")
1,215✔
581
    {
582
      if(children_count != 0)
81✔
583
      {
584
        ThrowError(line_number,
×
585
                   "<SubTree> with ID '" + ID + "' should not have any child");
×
586
      }
587
      // Validate SubTree ID as a model name
588
      validateModelName(ID, line_number);
81✔
589
      if(registered_nodes.count(ID) != 0)
80✔
590
      {
591
        ThrowError(line_number, "The attribute [ID] of tag <SubTree> must not use the "
×
592
                                "name of a registered Node");
593
      }
594
    }
595
    else
596
    {
597
      // use ID for builtin node types, otherwise use the element name
598
      const auto lookup_name = is_builtin ? ID : name;
1,134✔
599
      // Validate model name for custom node types (non-builtin element names)
600
      if(!is_builtin)
1,134✔
601
      {
602
        validateModelName(name, line_number);
1,120✔
603
      }
604
      const auto search = registered_nodes.find(lookup_name);
1,134✔
605
      const bool found = (search != registered_nodes.end());
1,134✔
606
      if(!found)
1,134✔
607
      {
608
        ThrowError(line_number, std::string("Node not recognized: ") + lookup_name);
12✔
609
      }
610

611
      const auto node_type = search->second;
1,131✔
612
      const std::string& registered_name = search->first;
1,131✔
613

614
      if(node_type == NodeType::DECORATOR)
1,131✔
615
      {
616
        if(children_count != 1)
58✔
617
        {
618
          ThrowError(line_number, std::string("The node '") + registered_name +
×
619
                                      "' must have exactly 1 child");
620
        }
621
      }
622
      else if(node_type == NodeType::CONTROL)
1,073✔
623
      {
624
        if(children_count == 0)
531✔
625
        {
626
          ThrowError(line_number, std::string("The node '") + registered_name +
×
627
                                      "' must have 1 or more children");
628
        }
629
        if(registered_name == "ReactiveSequence")
531✔
630
        {
631
          size_t async_count = 0;
17✔
632
          for(auto child = node->FirstChildElement(); child != nullptr;
57✔
633
              child = child->NextSiblingElement())
40✔
634
          {
635
            const std::string child_name = child->Name();
41✔
636
            const auto child_search = registered_nodes.find(child_name);
41✔
637
            if(child_search == registered_nodes.end())
41✔
638
            {
639
              ThrowError(child->GetLineNum(),
×
640
                         std::string("Unknown node type: ") + child_name);
×
641
            }
642
            const auto child_type = child_search->second;
41✔
643
            if(child_type == NodeType::CONTROL &&
45✔
644
               ((child_name == "ThreadedAction") ||
4✔
645
                (child_name == "StatefulActionNode") ||
4✔
646
                (child_name == "CoroActionNode") || (child_name == "AsyncSequence")))
4✔
647
            {
648
              ++async_count;
3✔
649
              if(async_count > 1)
3✔
650
              {
651
                ThrowError(line_number, std::string("A ReactiveSequence cannot have "
3✔
652
                                                    "more than one async child."));
653
              }
654
            }
655
          }
41✔
656
        }
657
      }
658
      else if(node_type == NodeType::ACTION || node_type == NodeType::CONDITION)
542✔
659
      {
660
        if(children_count != 0)
542✔
661
        {
662
          ThrowError(line_number, std::string("The node '") + registered_name +
×
663
                                      "' must not have any child");
664
        }
665
      }
666
    }
1,134✔
667
    //recursion
668
    for(auto child = node->FirstChildElement(); child != nullptr;
2,483✔
669
        child = child->NextSiblingElement())
950✔
670
    {
671
      recursiveStep(child, depth + 1);
1,216✔
672
    }
673
  };
2,116✔
674

675
  for(auto bt_root = xml_root->FirstChildElement("BehaviorTree"); bt_root != nullptr;
602✔
676
      bt_root = bt_root->NextSiblingElement("BehaviorTree"))
317✔
677
  {
678
    recursiveStep(bt_root, 0);
334✔
679
  }
680
}
303✔
681

682
Tree XMLParser::instantiateTree(const Blackboard::Ptr& root_blackboard,
246✔
683
                                std::string main_tree_ID)
684
{
685
  Tree output_tree;
246✔
686

687
  // use the main_tree_to_execute argument if it was provided by the user
688
  // or the one in the FIRST document opened
689
  if(main_tree_ID.empty())
246✔
690
  {
UNCOV
691
    XMLElement* first_xml_root = _p->opened_documents.front()->RootElement();
×
692

UNCOV
693
    if(auto main_tree_attribute = first_xml_root->Attribute("main_tree_to_execute"))
×
694
    {
UNCOV
695
      main_tree_ID = main_tree_attribute;
×
696
    }
UNCOV
697
    else if(_p->tree_roots.size() == 1)
×
698
    {
699
      // special case: there is only one registered BT.
UNCOV
700
      main_tree_ID = _p->tree_roots.begin()->first;
×
701
    }
702
    else
703
    {
704
      throw RuntimeError("[main_tree_to_execute] was not specified correctly");
×
705
    }
706
  }
707

708
  //--------------------------------------
709
  if(!root_blackboard)
246✔
710
  {
711
    throw RuntimeError("XMLParser::instantiateTree needs a non-empty "
712
                       "root_blackboard");
×
713
  }
714

715
  std::unordered_set<std::string> ancestors;
246✔
716
  _p->recursivelyCreateSubtree(main_tree_ID, {}, {}, output_tree, root_blackboard,
537✔
717
                               TreeNode::Ptr(), ancestors);
507✔
718
  output_tree.initialize();
231✔
719
  return output_tree;
462✔
720
}
261✔
721

722
void XMLParser::clearInternalState()
×
723
{
724
  _p->clear();
×
725
}
×
726

727
TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element,
934✔
728
                                                  const Blackboard::Ptr& blackboard,
729
                                                  const TreeNode::Ptr& node_parent,
730
                                                  const std::string& prefix_path,
731
                                                  Tree& output_tree)
732
{
733
  const auto element_name = element->Name();
934✔
734
  const auto element_ID = element->Attribute("ID");
934✔
735

736
  auto node_type = convertFromString<NodeType>(element_name);
934✔
737
  // name used by the factory
738
  std::string type_ID;
934✔
739

740
  if(node_type == NodeType::UNDEFINED)
934✔
741
  {
742
    // This is the case of nodes like <MyCustomAction>
743
    // check if the factory has this name
744
    if(factory->builders().count(element_name) == 0)
2,559✔
745
    {
746
      throw RuntimeError(element_name, " is not a registered node");
×
747
    }
748
    type_ID = element_name;
853✔
749

750
    if(element_ID != nullptr)
853✔
751
    {
752
      throw RuntimeError("Attribute [ID] is not allowed in <", type_ID, ">");
×
753
    }
754
  }
755
  else
756
  {
757
    // in this case, it is mandatory to have a field "ID"
758
    if(element_ID == nullptr)
81✔
759
    {
760
      throw RuntimeError("Attribute [ID] is mandatory in <", type_ID, ">");
×
761
    }
762
    type_ID = element_ID;
81✔
763
  }
764

765
  // By default, the instance name is equal to ID, unless the
766
  // attribute [name] is present.
767
  const char* attr_name = element->Attribute("name");
934✔
768
  const std::string instance_name = (attr_name != nullptr) ? attr_name : type_ID;
1,054✔
769

770
  // Validate instance name if explicitly provided
771
  if(attr_name != nullptr)
934✔
772
  {
773
    validateInstanceName(instance_name, element->GetLineNum());
120✔
774
  }
775

776
  const TreeNodeManifest* manifest = nullptr;
934✔
777

778
  auto manifest_it = factory->manifests().find(type_ID);
934✔
779
  if(manifest_it != factory->manifests().end())
934✔
780
  {
781
    manifest = &manifest_it->second;
855✔
782
  }
783

784
  PortsRemapping port_remap;
934✔
785
  NonPortAttributes other_attributes;
934✔
786

787
  for(const XMLAttribute* att = element->FirstAttribute(); att != nullptr;
1,613✔
788
      att = att->Next())
679✔
789
  {
790
    const std::string port_name = att->Name();
1,368✔
791
    const std::string port_value = att->Value();
684✔
792
    if(IsAllowedPortName(port_name))
684✔
793
    {
794
      const std::string port_name = att->Name();
818✔
795
      const std::string port_value = att->Value();
409✔
796

797
      if(manifest != nullptr)
409✔
798
      {
799
        auto port_model_it = manifest->ports.find(port_name);
369✔
800
        if(port_model_it == manifest->ports.end())
369✔
801
        {
802
          throw RuntimeError(StrCat("a port with name [", port_name,
4✔
803
                                    "] is found in the XML (<", element->Name(),
804
                                    ">, line ", std::to_string(att->GetLineNum()),
4✔
805
                                    ") but not in the providedPorts() of its "
806
                                    "registered node type."));
6✔
807
        }
808
        else
809
        {
810
          const auto& port_model = port_model_it->second;
367✔
811
          const bool is_blackboard = port_value.size() >= 3 &&
367✔
812
                                     port_value.front() == '{' &&
515✔
813
                                     port_value.back() == '}';
148✔
814
          // let's test already if conversion is possible
815
          if(!is_blackboard && port_model.converter() && port_model.isStronglyTyped())
367✔
816
          {
817
            // This may throw
818
            try
819
            {
820
              port_model.converter()(port_value);
155✔
821
            }
822
            catch(std::exception& ex)
3✔
823
            {
824
              auto msg = StrCat("The port with name \"", port_name, "\" and value \"",
3✔
825
                                port_value, "\" can not be converted to ",
826
                                port_model.typeName());
6✔
827
              throw LogicError(msg);
3✔
828
            }
6✔
829
          }
830
        }
831
      }
832

833
      port_remap[port_name] = port_value;
404✔
834
    }
414✔
835
    else if(!IsReservedAttribute(port_name))
275✔
836
    {
837
      other_attributes[port_name] = port_value;
3✔
838
    }
839
  }
689✔
840

841
  NodeConfig config;
929✔
842
  config.blackboard = blackboard;
929✔
843
  config.path = prefix_path + instance_name;
929✔
844
  config.uid = output_tree.getUID();
929✔
845
  config.manifest = manifest;
929✔
846

847
  if(type_ID == instance_name)
929✔
848
  {
849
    config.path += std::string("::") + std::to_string(config.uid);
2,433✔
850
  }
851

852
  auto AddCondition = [&](auto& conditions, const char* attr_name, auto ID) {
7,432✔
853
    if(auto script = element->Attribute(attr_name))
7,432✔
854
    {
855
      conditions.insert({ ID, std::string(script) });
54✔
856
      other_attributes.erase(attr_name);
162✔
857
    }
858
  };
8,361✔
859

860
  for(int i = 0; i < int(PreCond::COUNT_); i++)
4,645✔
861
  {
862
    auto pre = static_cast<PreCond>(i);
3,716✔
863
    AddCondition(config.pre_conditions, toStr(pre).c_str(), pre);
3,716✔
864
  }
865
  for(int i = 0; i < int(PostCond::COUNT_); i++)
4,645✔
866
  {
867
    auto post = static_cast<PostCond>(i);
3,716✔
868
    AddCondition(config.post_conditions, toStr(post).c_str(), post);
3,716✔
869
  }
870

871
  config.other_attributes = other_attributes;
929✔
872
  //---------------------------------------------
873
  TreeNode::Ptr new_node;
929✔
874

875
  if(node_type == NodeType::SUBTREE)
929✔
876
  {
877
    config.input_ports = port_remap;
79✔
878
    new_node =
879
        factory->instantiateTreeNode(instance_name, toStr(NodeType::SUBTREE), config);
79✔
880
    // If a substitution rule replaced the SubTree with a different node
881
    // (e.g. a TestNode), the dynamic_cast will return nullptr.
882
    auto subtree_node = dynamic_cast<SubTreeNode*>(new_node.get());
79✔
883
    if(subtree_node != nullptr)
79✔
884
    {
885
      subtree_node->setSubtreeID(type_ID);
78✔
886
    }
887
  }
888
  else
889
  {
890
    if(manifest == nullptr)
850✔
891
    {
892
      auto msg = StrCat("Missing manifest for element_ID: ", element_ID,
893
                        ". It shouldn't happen. Please report this issue.");
×
894
      throw RuntimeError(msg);
×
895
    }
×
896

897
    //Check that name in remapping can be found in the manifest
898
    for(const auto& [name_in_subtree, remap_value] : port_remap)
1,214✔
899
    {
900
      std::ignore = remap_value;  // unused in this loop
364✔
901
      if(manifest->ports.count(name_in_subtree) == 0)
364✔
902
      {
903
        throw RuntimeError("Possible typo? In the XML, you tried to remap port \"",
904
                           name_in_subtree, "\" in node [", config.path, "(type ",
905
                           type_ID,
906
                           ")], but the manifest/model of this node does not contain a "
907
                           "port "
908
                           "with this name.");
×
909
      }
910
    }
911

912
    // Initialize the ports in the BB to set the type
913
    for(const auto& [port_name, port_info] : manifest->ports)
1,263✔
914
    {
915
      auto remap_it = port_remap.find(port_name);
416✔
916
      if(remap_it == port_remap.end())
416✔
917
      {
918
        continue;
52✔
919
      }
920
      const StringView remapped_port = remap_it->second;
364✔
921

922
      if(auto param_res = TreeNode::getRemappedKey(port_name, remapped_port))
364✔
923
      {
924
        // port_key will contain the key to find the entry in the blackboard
925
        const auto port_key = static_cast<std::string>(param_res.value());
148✔
926

927
        // if the entry already exists, check that the type is the same
928
        if(auto prev_info = blackboard->entryInfo(port_key))
148✔
929
        {
930
          // Check consistency of types.
931
          bool const port_type_mismatch =
932
              (prev_info->isStronglyTyped() && port_info.isStronglyTyped() &&
81✔
933
               prev_info->type() != port_info.type());
29✔
934

935
          // special case related to convertFromString
936
          bool const string_input = (prev_info->type() == typeid(std::string));
52✔
937

938
          if(port_type_mismatch && !string_input)
52✔
939
          {
940
            blackboard->debugMessage();
3✔
941

942
            throw RuntimeError("The creation of the tree failed because the port [",
943
                               port_key, "] was initially created with type [",
944
                               demangle(prev_info->type()), "] and, later type [",
6✔
945
                               demangle(port_info.type()), "] was used somewhere else.");
9✔
946
          }
947
        }
948
        else
949
        {
950
          // not found, insert for the first time.
951
          blackboard->createEntry(port_key, port_info);
96✔
952
        }
953
      }
512✔
954
    }
955

956
    // Set the port direction in config
957
    for(const auto& remap_it : port_remap)
1,205✔
958
    {
959
      const auto& port_name = remap_it.first;
358✔
960
      auto port_it = manifest->ports.find(port_name);
358✔
961
      if(port_it != manifest->ports.end())
358✔
962
      {
963
        auto direction = port_it->second.direction();
358✔
964
        if(direction != PortDirection::OUTPUT)
358✔
965
        {
966
          config.input_ports.insert(remap_it);
299✔
967
        }
968
        if(direction != PortDirection::INPUT)
358✔
969
        {
970
          config.output_ports.insert(remap_it);
89✔
971
        }
972
      }
973
    }
974

975
    // use default value if available for empty ports. Only inputs
976
    for(const auto& port_it : manifest->ports)
1,257✔
977
    {
978
      const std::string& port_name = port_it.first;
410✔
979
      const PortInfo& port_info = port_it.second;
410✔
980

981
      const auto direction = port_info.direction();
410✔
982
      const auto& default_string = port_info.defaultValueString();
410✔
983
      if(!default_string.empty())
410✔
984
      {
985
        if(direction != PortDirection::OUTPUT && config.input_ports.count(port_name) == 0)
65✔
986
        {
987
          config.input_ports.insert({ port_name, default_string });
35✔
988
        }
989

990
        if(direction != PortDirection::INPUT &&
75✔
991
           config.output_ports.count(port_name) == 0 &&
75✔
992
           TreeNode::isBlackboardPointer(default_string))
10✔
993
        {
994
          config.output_ports.insert({ port_name, default_string });
10✔
995
        }
996
      }
997
    }
998

999
    new_node = factory->instantiateTreeNode(instance_name, type_ID, config);
847✔
1000
  }
1001

1002
  // add the pointer of this node to the parent
1003
  if(node_parent != nullptr)
926✔
1004
  {
1005
    if(auto* control_parent = dynamic_cast<ControlNode*>(node_parent.get()))
687✔
1006
    {
1007
      control_parent->addChild(new_node.get());
560✔
1008
    }
1009
    else if(auto* decorator_parent = dynamic_cast<DecoratorNode*>(node_parent.get()))
127✔
1010
    {
1011
      decorator_parent->setChild(new_node.get());
127✔
1012
    }
1013
  }
1014

1015
  return new_node;
1,852✔
1016
}
964✔
1017

1018
void BT::XMLParser::PImpl::recursivelyCreateSubtree(
321✔
1019
    const std::string& tree_ID, const std::string& tree_path,
1020
    const std::string& prefix_path, Tree& output_tree, Blackboard::Ptr blackboard,
1021
    const TreeNode::Ptr& root_node, std::unordered_set<std::string>& ancestors)
1022
{
1023
  if(!ancestors.insert(tree_ID).second)
321✔
1024
  {
1025
    throw RuntimeError("Recursive behavior tree cycle detected: tree '", tree_ID,
1026
                       "' references itself (directly or indirectly)");
2✔
1027
  }
1028
  constexpr int kMaxNestingDepth = 256;
319✔
1029
  std::function<void(const TreeNode::Ptr&, Tree::Subtree::Ptr, std::string,
1030
                     const XMLElement*, int)>
1031
      recursiveStep;
319✔
1032

1033
  recursiveStep = [&](TreeNode::Ptr parent_node, Tree::Subtree::Ptr subtree,
×
1034
                      std::string prefix, const XMLElement* element, int depth) {
1035
    if(depth > kMaxNestingDepth)
934✔
1036
    {
1037
      throw RuntimeError("Maximum XML nesting depth exceeded during tree "
1038
                         "instantiation (limit: ",
1039
                         std::to_string(kMaxNestingDepth),
×
1040
                         "). The XML is too deeply nested.");
×
1041
    }
1042
    // create the node
1043
    auto node = createNodeFromXML(element, blackboard, parent_node, prefix, output_tree);
934✔
1044
    subtree->nodes.push_back(node);
926✔
1045

1046
    // common case: iterate through all children
1047
    if(node->type() != NodeType::SUBTREE)
926✔
1048
    {
1049
      for(auto child_element = element->FirstChildElement(); child_element != nullptr;
1,455✔
1050
          child_element = child_element->NextSiblingElement())
607✔
1051
      {
1052
        recursiveStep(node, subtree, prefix, child_element, depth + 1);
637✔
1053
      }
1054
    }
1055
    else  // special case: SubTreeNode
1056
    {
1057
      auto new_bb = Blackboard::create(blackboard);
78✔
1058
      const std::string subtree_ID = element->Attribute("ID");
78✔
1059
      std::unordered_map<std::string, std::string> subtree_remapping;
78✔
1060
      bool do_autoremap = false;
78✔
1061

1062
      for(auto attr = element->FirstAttribute(); attr != nullptr; attr = attr->Next())
228✔
1063
      {
1064
        const std::string attr_name = attr->Name();
300✔
1065
        std::string attr_value = attr->Value();
150✔
1066
        if(attr_value == "{=}")
150✔
1067
        {
1068
          attr_value = StrCat("{", attr_name, "}");
1✔
1069
        }
1070

1071
        if(attr_name == "_autoremap")
150✔
1072
        {
1073
          do_autoremap = convertFromString<bool>(attr_value);
17✔
1074
          new_bb->enableAutoRemapping(do_autoremap);
17✔
1075
          continue;
17✔
1076
        }
1077
        if(!IsAllowedPortName(attr->Name()))
133✔
1078
        {
1079
          continue;
93✔
1080
        }
1081
        subtree_remapping.insert({ attr_name, attr_value });
40✔
1082
      }
260✔
1083
      // check if this subtree has a model. If it does,
1084
      // we want to check if all the mandatory ports were remapped and
1085
      // add default ones, if necessary
1086
      auto subtree_model_it = subtree_models.find(subtree_ID);
78✔
1087
      if(subtree_model_it != subtree_models.end())
78✔
1088
      {
1089
        const auto& subtree_model_ports = subtree_model_it->second.ports;
2✔
1090
        // check if:
1091
        // - remapping contains mondatory ports
1092
        // - if any of these has default value
1093
        for(const auto& [port_name, port_info] : subtree_model_ports)
7✔
1094
        {
1095
          auto it = subtree_remapping.find(port_name);
5✔
1096
          // don't override existing remapping
1097
          if(it == subtree_remapping.end() && !do_autoremap)
5✔
1098
          {
1099
            // remapping is not explicitly defined in the XML: use the model
1100
            if(port_info.defaultValueString().empty())
2✔
1101
            {
1102
              auto msg = StrCat("In the <TreeNodesModel> the <Subtree ID=\"", subtree_ID,
×
1103
                                "\"> is defining a mandatory port called [", port_name,
1104
                                "], but you are not remapping it");
×
1105
              throw RuntimeError(msg);
×
1106
            }
×
1107
            else
1108
            {
1109
              subtree_remapping.insert({ port_name, port_info.defaultValueString() });
2✔
1110
            }
1111
          }
1112
        }
1113
      }
1114

1115
      for(const auto& [attr_name, attr_value] : subtree_remapping)
120✔
1116
      {
1117
        if(TreeNode::isBlackboardPointer(attr_value))
42✔
1118
        {
1119
          // do remapping
1120
          const StringView port_name = TreeNode::stripBlackboardPointer(attr_value);
27✔
1121
          new_bb->addSubtreeRemapping(attr_name, port_name);
27✔
1122
        }
1123
        else
1124
        {
1125
          // constant string: just set that constant value into the BB
1126
          // IMPORTANT: this must not be autoremapped!!!
1127
          new_bb->enableAutoRemapping(false);
15✔
1128
          new_bb->set(attr_name, static_cast<std::string>(attr_value));
15✔
1129
          new_bb->enableAutoRemapping(do_autoremap);
15✔
1130
        }
1131
      }
1132

1133
      std::string subtree_path = subtree->instance_name;
78✔
1134
      if(!subtree_path.empty())
78✔
1135
      {
1136
        subtree_path += "/";
15✔
1137
      }
1138
      if(auto name = element->Attribute("name"))
78✔
1139
      {
1140
        subtree_path += name;
10✔
1141
      }
1142
      else
1143
      {
1144
        subtree_path += subtree_ID + "::" + std::to_string(node->UID());
68✔
1145
      }
1146

1147
      // Check if the path already exists - duplicate paths cause issues in Groot2
1148
      // and TreeObserver (see Groot2 issue #56)
1149
      for(const auto& sub : output_tree.subtrees)
196✔
1150
      {
1151
        if(sub->instance_name == subtree_path)
121✔
1152
        {
1153
          throw RuntimeError("Duplicate SubTree path detected: '", subtree_path,
1154
                             "'. Multiple SubTree nodes with the same 'name' attribute "
1155
                             "under the same parent are not allowed. "
1156
                             "Please use unique names or omit the 'name' attribute "
1157
                             "to auto-generate unique paths.");
3✔
1158
        }
1159
      }
1160

1161
      recursivelyCreateSubtree(subtree_ID,
158✔
1162
                               subtree_path,        // name
1163
                               subtree_path + "/",  //prefix
154✔
1164
                               output_tree, new_bb, node, ancestors);
1165
    }
99✔
1166
  };
1,245✔
1167

1168
  auto it = tree_roots.find(tree_ID);
319✔
1169
  if(it == tree_roots.end())
319✔
1170
  {
1171
    throw std::runtime_error(std::string("Can't find a tree with name: ") + tree_ID);
6✔
1172
  }
1173

1174
  auto root_element = it->second->FirstChildElement();
317✔
1175

1176
  //-------- start recursion -----------
1177

1178
  // Append a new subtree to the list
1179
  auto new_tree = std::make_shared<Tree::Subtree>();
317✔
1180
  new_tree->blackboard = blackboard;
317✔
1181
  new_tree->instance_name = tree_path;
317✔
1182
  new_tree->tree_ID = tree_ID;
317✔
1183
  output_tree.subtrees.push_back(new_tree);
317✔
1184

1185
  recursiveStep(root_node, new_tree, prefix_path, root_element, 0);
347✔
1186
  ancestors.erase(tree_ID);
302✔
1187
}
334✔
1188

1189
void XMLParser::PImpl::getPortsRecursively(const XMLElement* element,
×
1190
                                           std::vector<std::string>& output_ports)
1191
{
1192
  for(const XMLAttribute* attr = element->FirstAttribute(); attr != nullptr;
×
1193
      attr = attr->Next())
×
1194
  {
1195
    const char* attr_name = attr->Name();
×
1196
    const char* attr_value = attr->Value();
×
1197
    if(IsAllowedPortName(attr_name) && TreeNode::isBlackboardPointer(attr_value))
×
1198
    {
1199
      auto port_name = TreeNode::stripBlackboardPointer(attr_value);
×
1200
      output_ports.push_back(static_cast<std::string>(port_name));
×
1201
    }
1202
  }
1203

1204
  for(auto child_element = element->FirstChildElement(); child_element != nullptr;
×
1205
      child_element = child_element->NextSiblingElement())
×
1206
  {
1207
    getPortsRecursively(child_element, output_ports);
×
1208
  }
1209
}
×
1210

1211
namespace
1212
{
1213
void addNodeModelToXML(const TreeNodeManifest& model, XMLDocument& doc,
1,120✔
1214
                       XMLElement* model_root)
1215
{
1216
  XMLElement* element = doc.NewElement(toStr(model.type).c_str());
1,120✔
1217
  element->SetAttribute("ID", model.registration_ID.c_str());
1,120✔
1218

1219
  for(const auto& [port_name, port_info] : model.ports)
2,669✔
1220
  {
1221
    XMLElement* port_element = nullptr;
1,549✔
1222
    switch(port_info.direction())
1,549✔
1223
    {
1224
      case PortDirection::INPUT:
1,306✔
1225
        port_element = doc.NewElement("input_port");
1,306✔
1226
        break;
1,306✔
1227
      case PortDirection::OUTPUT:
108✔
1228
        port_element = doc.NewElement("output_port");
108✔
1229
        break;
108✔
1230
      case PortDirection::INOUT:
135✔
1231
        port_element = doc.NewElement("inout_port");
135✔
1232
        break;
135✔
1233
    }
1234

1235
    port_element->SetAttribute("name", port_name.c_str());
1,549✔
1236
    if(port_info.type() != typeid(void))
1,549✔
1237
    {
1238
      port_element->SetAttribute("type", BT::demangle(port_info.type()).c_str());
1,549✔
1239
    }
1240
    if(!port_info.defaultValue().empty())
1,549✔
1241
    {
1242
      port_element->SetAttribute("default", port_info.defaultValueString().c_str());
278✔
1243
    }
1244

1245
    if(!port_info.description().empty())
1,549✔
1246
    {
1247
      port_element->SetText(port_info.description().c_str());
604✔
1248
    }
1249
    element->InsertEndChild(port_element);
1,549✔
1250
  }
1251

1252
  if(!model.metadata.empty())
1,120✔
1253
  {
1254
    auto metadata_root = doc.NewElement("MetadataFields");
1✔
1255

1256
    for(const auto& [name, value] : model.metadata)
3✔
1257
    {
1258
      auto metadata_element = doc.NewElement("Metadata");
2✔
1259
      metadata_element->SetAttribute(name.c_str(), value.c_str());
2✔
1260
      metadata_root->InsertEndChild(metadata_element);
2✔
1261
    }
1262

1263
    element->InsertEndChild(metadata_root);
1✔
1264
  }
1265

1266
  model_root->InsertEndChild(element);
1,120✔
1267
}
1,120✔
1268

1269
void addTreeToXML(const Tree& tree, XMLDocument& doc, XMLElement* rootXML,
27✔
1270
                  bool add_metadata, bool add_builtin_models)
1271
{
1272
  std::function<void(const TreeNode&, XMLElement*)> addNode;
27✔
1273
  addNode = [&](const TreeNode& node, XMLElement* parent_elem) {
×
1274
    XMLElement* elem = nullptr;
61✔
1275

1276
    if(const auto* subtree = dynamic_cast<const SubTreeNode*>(&node))
61✔
1277
    {
1278
      elem = doc.NewElement(node.registrationName().c_str());
×
1279
      elem->SetAttribute("ID", subtree->subtreeID().c_str());
×
1280
      if(add_metadata)
×
1281
      {
1282
        elem->SetAttribute("_fullpath", subtree->config().path.c_str());
×
1283
      }
1284
    }
1285
    else
1286
    {
1287
      elem = doc.NewElement(node.registrationName().c_str());
61✔
1288
      elem->SetAttribute("name", node.name().c_str());
61✔
1289
    }
1290

1291
    if(add_metadata)
61✔
1292
    {
1293
      elem->SetAttribute("_uid", node.UID());
61✔
1294
    }
1295

1296
    for(const auto& [name, value] : node.config().input_ports)
62✔
1297
    {
1298
      elem->SetAttribute(name.c_str(), value.c_str());
1✔
1299
    }
1300
    for(const auto& [name, value] : node.config().output_ports)
61✔
1301
    {
1302
      // avoid duplicates, in the case of INOUT ports
1303
      if(node.config().input_ports.count(name) == 0)
×
1304
      {
1305
        elem->SetAttribute(name.c_str(), value.c_str());
×
1306
      }
1307
    }
1308

1309
    for(const auto& [pre, script] : node.config().pre_conditions)
61✔
1310
    {
1311
      elem->SetAttribute(toStr(pre).c_str(), script.c_str());
×
1312
    }
1313
    for(const auto& [post, script] : node.config().post_conditions)
61✔
1314
    {
1315
      elem->SetAttribute(toStr(post).c_str(), script.c_str());
×
1316
    }
1317

1318
    parent_elem->InsertEndChild(elem);
61✔
1319

1320
    if(const auto* control = dynamic_cast<const ControlNode*>(&node))
61✔
1321
    {
1322
      for(const auto& child : control->children())
51✔
1323
      {
1324
        addNode(*child, elem);
34✔
1325
      }
1326
    }
1327
    else if(const auto* decorator = dynamic_cast<const DecoratorNode*>(&node))
44✔
1328
    {
1329
      if(decorator->type() != NodeType::SUBTREE)
×
1330
      {
1331
        addNode(*decorator->child(), elem);
×
1332
      }
1333
    }
1334
  };
88✔
1335

1336
  for(const auto& subtree : tree.subtrees)
54✔
1337
  {
1338
    XMLElement* subtree_elem = doc.NewElement("BehaviorTree");
27✔
1339
    subtree_elem->SetAttribute("ID", subtree->tree_ID.c_str());
27✔
1340
    subtree_elem->SetAttribute("_fullpath", subtree->instance_name.c_str());
27✔
1341
    rootXML->InsertEndChild(subtree_elem);
27✔
1342
    addNode(*subtree->nodes.front(), subtree_elem);
27✔
1343
  }
1344

1345
  XMLElement* model_root = doc.NewElement("TreeNodesModel");
27✔
1346
  rootXML->InsertEndChild(model_root);
27✔
1347

1348
  static const BehaviorTreeFactory temp_factory;
27✔
1349

1350
  std::map<std::string, const TreeNodeManifest*> ordered_models;
27✔
1351
  for(const auto& [registration_ID, model] : tree.manifests)
1,144✔
1352
  {
1353
    if(add_builtin_models || temp_factory.builtinNodes().count(registration_ID) == 0)
1,117✔
1354
    {
1355
      ordered_models.insert({ registration_ID, &model });
1,117✔
1356
    }
1357
  }
1358

1359
  for(const auto& [registration_ID, model] : ordered_models)
1,144✔
1360
  {
1361
    addNodeModelToXML(*model, doc, model_root);
1,117✔
1362
  }
1363
}
27✔
1364
}  // namespace
1365

1366
std::string writeTreeNodesModelXML(const BehaviorTreeFactory& factory,
3✔
1367
                                   bool include_builtin)
1368
{
1369
  XMLDocument doc;
3✔
1370

1371
  XMLElement* rootXML = doc.NewElement("root");
3✔
1372
  rootXML->SetAttribute("BTCPP_format", "4");
3✔
1373
  doc.InsertFirstChild(rootXML);
3✔
1374

1375
  XMLElement* model_root = doc.NewElement("TreeNodesModel");
3✔
1376
  rootXML->InsertEndChild(model_root);
3✔
1377

1378
  std::map<std::string, const TreeNodeManifest*> ordered_models;
3✔
1379

1380
  for(const auto& [registration_ID, model] : factory.manifests())
129✔
1381
  {
1382
    if(include_builtin || factory.builtinNodes().count(registration_ID) == 0)
126✔
1383
    {
1384
      ordered_models.insert({ registration_ID, &model });
3✔
1385
    }
1386
  }
1387

1388
  for(const auto& [registration_ID, model] : ordered_models)
6✔
1389
  {
1390
    addNodeModelToXML(*model, doc, model_root);
3✔
1391
  }
1392

1393
  XMLPrinter printer;
3✔
1394
  doc.Print(&printer);
3✔
1395
  return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
6✔
1396
}
3✔
1397

1398
std::string writeTreeXSD(const BehaviorTreeFactory& factory)
×
1399
{
1400
  // There are 2 forms of representation for a node:
1401
  // compact: <Sequence .../>  and explicit: <Control ID="Sequence" ... />
1402
  // Only the compact form is supported because the explicit form doesn't
1403
  // make sense with XSD since we would need to allow any attribute.
1404
  // Prepare the data
1405

1406
  std::map<std::string, const TreeNodeManifest*> ordered_models;
×
1407
  for(const auto& [registration_id, model] : factory.manifests())
×
1408
  {
1409
    ordered_models.insert({ registration_id, &model });
×
1410
  }
1411

1412
  XMLDocument doc;
×
1413

1414
  // Add the XML declaration
1415
  XMLDeclaration* declaration = doc.NewDeclaration("xml version=\"1.0\" "
×
1416
                                                   "encoding=\"UTF-8\"");
1417
  doc.InsertFirstChild(declaration);
×
1418

1419
  // Create the root element with namespace and attributes
1420
  // To validate a BT XML file with `schema.xsd` in the same directory:
1421
  // <root BTCPP_format="4" main_tree_to_execute="MainTree"
1422
  //   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1423
  //   xsi:noNamespaceSchemaLocation="schema.xsd">
1424
  XMLElement* schema_element = doc.NewElement("xs:schema");
×
1425
  schema_element->SetAttribute("xmlns:xs", "http://www.w3.org/2001/XMLSchema");
×
1426
  schema_element->SetAttribute("elementFormDefault", "qualified");
×
1427
  doc.InsertEndChild(schema_element);
×
1428

1429
  auto parse_and_insert = [&doc](XMLElement* parent_elem, const char* str) {
×
1430
    XMLDocument tmp_doc;
×
1431
    tmp_doc.Parse(str);
×
1432
    if(tmp_doc.Error())
×
1433
    {
1434
      std::cerr << "Internal error parsing existing XML: " << tmp_doc.ErrorStr()
×
1435
                << std::endl;
×
1436
      return;
×
1437
    }
1438
    for(auto child = tmp_doc.FirstChildElement(); child != nullptr;
×
1439
        child = child->NextSiblingElement())
×
1440
    {
1441
      parent_elem->InsertEndChild(child->DeepClone(&doc));
×
1442
    }
1443
  };
×
1444

1445
  // Common elements.
1446
  XMLComment* comment = doc.NewComment("Define the common elements");
×
1447
  schema_element->InsertEndChild(comment);
×
1448

1449
  // TODO: add <xs:whiteSpace value="preserve"/> for `inputPortType` and `outputPortType`.
1450
  parse_and_insert(schema_element, R"(
×
1451
    <xs:simpleType name="blackboardType">
1452
        <xs:restriction base="xs:string">
1453
            <xs:pattern value="\{.*\}"/>
1454
        </xs:restriction>
1455
    </xs:simpleType>
1456
    <xs:simpleType name="booleanOrBlackboardType">
1457
      <xs:union memberTypes="xs:boolean blackboardType"/>
1458
    </xs:simpleType>
1459
    <xs:simpleType name="integerOrBlackboardType">
1460
      <xs:union memberTypes="xs:integer blackboardType"/>
1461
    </xs:simpleType>
1462
    <xs:simpleType name="decimalOrBlackboardType">
1463
      <xs:union memberTypes="xs:decimal blackboardType"/>
1464
    </xs:simpleType>
1465
    <xs:simpleType name="stringOrBlackboardType">
1466
      <xs:union memberTypes="xs:string blackboardType"/>
1467
    </xs:simpleType>
1468
    <xs:simpleType name="descriptionType">
1469
        <xs:restriction base="xs:string">
1470
          <xs:whiteSpace value="preserve"/>
1471
        </xs:restriction>
1472
    </xs:simpleType>
1473
    <xs:complexType name="inputPortType">
1474
      <xs:simpleContent>
1475
        <xs:extension base="xs:string">
1476
          <xs:attribute name="name" type="xs:string" use="required"/>
1477
          <xs:attribute name="type" type="xs:string" use="optional"/>
1478
          <xs:attribute name="default" type="xs:string" use="optional"/>
1479
        </xs:extension>
1480
      </xs:simpleContent>
1481
    </xs:complexType>
1482
    <xs:complexType name="outputPortType">
1483
      <xs:simpleContent>
1484
        <xs:extension base="xs:string">
1485
          <xs:attribute name="name" type="xs:string" use="required"/>
1486
          <xs:attribute name="type" type="xs:string" use="optional"/>
1487
        </xs:extension>
1488
      </xs:simpleContent>
1489
    </xs:complexType>
1490
    <xs:attributeGroup name="preconditionAttributeGroup">
1491
      <xs:attribute name="_failureIf" type="xs:string" use="optional"/>
1492
      <xs:attribute name="_skipIf" type="xs:string" use="optional"/>
1493
      <xs:attribute name="_successIf" type="xs:string" use="optional"/>
1494
      <xs:attribute name="_while" type="xs:string" use="optional"/>
1495
    </xs:attributeGroup>
1496
    <xs:attributeGroup name="postconditionAttributeGroup">
1497
      <xs:attribute name="_onSuccess" type="xs:string" use="optional"/>
1498
      <xs:attribute name="_onFailure" type="xs:string" use="optional"/>
1499
      <xs:attribute name="_post" type="xs:string" use="optional"/>
1500
      <xs:attribute name="_onHalted" type="xs:string" use="optional"/>
1501
    </xs:attributeGroup>)");
1502

1503
  // Common attributes
1504
  // Note that we do not add the `ID` attribute because we do not
1505
  // support the explicit notation (e.g. <Action ID="Saysomething">).
1506
  // Cf. https://www.behaviortree.dev/docs/learn-the-basics/xml_format/#compact-vs-explicit-representation
1507
  // There is no way to check attribute validity with the explicit notation with XSD.
1508
  // The `ID` attribute for `<SubTree>` is handled separately.
1509
  parse_and_insert(schema_element, R"(
×
1510
    <xs:attributeGroup name="commonAttributeGroup">
1511
      <xs:attribute name="name" type="xs:string" use="optional"/>
1512
      <xs:attributeGroup ref="preconditionAttributeGroup"/>
1513
      <xs:attributeGroup ref="postconditionAttributeGroup"/>
1514
    </xs:attributeGroup>)");
1515

1516
  // Basic node types
1517
  parse_and_insert(schema_element, R"(
×
1518
    <xs:complexType name="treeNodesModelNodeType">
1519
      <xs:sequence>
1520
        <xs:choice minOccurs="0" maxOccurs="unbounded">
1521
          <xs:element name="input_port" type="inputPortType"/>
1522
          <xs:element name="output_port" type="outputPortType"/>
1523
        </xs:choice>
1524
        <xs:element name="description" type="descriptionType" minOccurs="0" maxOccurs="1"/>
1525
      </xs:sequence>
1526
      <xs:attribute name="ID" type="xs:string" use="required"/>
1527
    </xs:complexType>
1528
    <xs:group name="treeNodesModelNodeGroup">
1529
      <xs:choice>
1530
        <xs:element name="Action" type="treeNodesModelNodeType"/>
1531
        <xs:element name="Condition" type="treeNodesModelNodeType"/>
1532
        <xs:element name="Control" type="treeNodesModelNodeType"/>
1533
        <xs:element name="Decorator" type="treeNodesModelNodeType"/>
1534
      </xs:choice>
1535
    </xs:group>
1536
    )");
1537

1538
  // `root` element
1539
  const auto root_element_xsd = R"(
×
1540
    <xs:element name="root">
1541
      <xs:complexType>
1542
        <xs:sequence>
1543
          <xs:choice minOccurs="0" maxOccurs="unbounded">
1544
            <xs:element ref="include"/>
1545
            <xs:element ref="BehaviorTree"/>
1546
          </xs:choice>
1547
          <xs:element ref="TreeNodesModel" minOccurs="0" maxOccurs="1"/>
1548
        </xs:sequence>
1549
        <xs:attribute name="BTCPP_format" type="xs:string" use="required"/>
1550
        <xs:attribute name="main_tree_to_execute" type="xs:string" use="optional"/>
1551
      </xs:complexType>
1552
    </xs:element>
1553
  )";
1554
  parse_and_insert(schema_element, root_element_xsd);
×
1555

1556
  // Group definition for a single node of any of the existing node types.
1557
  XMLElement* one_node_group = doc.NewElement("xs:group");
×
1558
  {
1559
    one_node_group->SetAttribute("name", "oneNodeGroup");
×
1560
    std::ostringstream xsd;
×
1561
    xsd << "<xs:choice>";
×
1562
    for(const auto& [registration_id, model] : ordered_models)
×
1563
    {
1564
      xsd << "<xs:element name=\"" << registration_id << "\" type=\"" << registration_id
1565
          << "Type\"/>";
×
1566
    }
1567
    xsd << "</xs:choice>";
×
1568
    parse_and_insert(one_node_group, xsd.str().c_str());
×
1569
    schema_element->InsertEndChild(one_node_group);
×
1570
  }
×
1571

1572
  // `include` element
1573
  parse_and_insert(schema_element, R"(
×
1574
    <xs:element name="include">
1575
      <xs:complexType>
1576
        <xs:attribute name="path" type="xs:string" use="required"/>
1577
        <xs:attribute name="ros_pkg" type="xs:string" use="optional"/>
1578
      </xs:complexType>
1579
    </xs:element>
1580
  )");
1581

1582
  // `BehaviorTree` element
1583
  parse_and_insert(schema_element, R"(
×
1584
  <xs:element name="BehaviorTree">
1585
    <xs:complexType>
1586
      <xs:group ref="oneNodeGroup"/>
1587
      <xs:attribute name="ID" type="xs:string" use="required"/>
1588
    </xs:complexType>
1589
  </xs:element>
1590
  )");
1591

1592
  // `TreeNodesModel` element
1593
  parse_and_insert(schema_element, R"(
×
1594
    <xs:element name="TreeNodesModel">
1595
      <xs:complexType>
1596
          <xs:group ref="treeNodesModelNodeGroup" minOccurs="0" maxOccurs="unbounded"/>
1597
      </xs:complexType>
1598
    </xs:element>
1599
  )");
1600

1601
  // Definitions for all node types.
1602
  for(const auto& [registration_id, model] : ordered_models)
×
1603
  {
1604
    XMLElement* type = doc.NewElement("xs:complexType");
×
1605
    type->SetAttribute("name", (model->registration_ID + "Type").c_str());
×
1606
    if((model->type == NodeType::ACTION) || (model->type == NodeType::CONDITION) ||
×
1607
       (model->type == NodeType::SUBTREE))
×
1608
    {
1609
      /* No children, nothing to add. */
1610
    }
1611
    else if(model->type == NodeType::DECORATOR)
×
1612
    {
1613
      /* One child. */
1614
      // <xs:group ref="oneNodeGroup" minOccurs="1" maxOccurs="1"/>
1615
      XMLElement* group = doc.NewElement("xs:group");
×
1616
      group->SetAttribute("ref", "oneNodeGroup");
×
1617
      group->SetAttribute("minOccurs", "1");
×
1618
      group->SetAttribute("maxOccurs", "1");
×
1619
      type->InsertEndChild(group);
×
1620
    }
1621
    else
1622
    {
1623
      /* NodeType::CONTROL. */
1624
      // TODO: check the code, the doc says 1..N but why not 0..N?
1625
      // <xs:group ref="oneNodeGroup" minOccurs="0" maxOccurs="unbounded"/>
1626
      XMLElement* group = doc.NewElement("xs:group");
×
1627
      group->SetAttribute("ref", "oneNodeGroup");
×
1628
      group->SetAttribute("minOccurs", "0");
×
1629
      group->SetAttribute("maxOccurs", "unbounded");
×
1630
      type->InsertEndChild(group);
×
1631
    }
1632
    XMLElement* common_attr_group = doc.NewElement("xs:attributeGroup");
×
1633
    common_attr_group->SetAttribute("ref", "commonAttributeGroup");
×
1634
    type->InsertEndChild(common_attr_group);
×
1635
    for(const auto& [port_name, port_info] : model->ports)
×
1636
    {
1637
      XMLElement* attr = doc.NewElement("xs:attribute");
×
1638
      attr->SetAttribute("name", port_name.c_str());
×
1639
      const auto xsd_attribute_type = xsdAttributeType(port_info);
×
1640
      if(!xsd_attribute_type.empty())
×
1641
      {
1642
        attr->SetAttribute("type", xsd_attribute_type.c_str());
×
1643
      }
1644
      if(!port_info.defaultValue().empty())
×
1645
      {
1646
        attr->SetAttribute("default", port_info.defaultValueString().c_str());
×
1647
      }
1648
      else
1649
      {
1650
        attr->SetAttribute("use", "required");
×
1651
      }
1652
      type->InsertEndChild(attr);
×
1653
    }
×
1654
    if(model->registration_ID == "SubTree")
×
1655
    {
1656
      parse_and_insert(type, R"(
×
1657
        <xs:attribute name="ID" type="xs:string" use="required"/>
1658
        <xs:anyAttribute processContents="skip"/>
1659
      )");
1660
    }
1661
    schema_element->InsertEndChild(type);
×
1662
  }
1663

1664
  XMLPrinter printer;
×
1665
  doc.Print(&printer);
×
1666
  return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
×
1667
}
×
1668

1669
std::string WriteTreeToXML(const Tree& tree, bool add_metadata, bool add_builtin_models)
27✔
1670
{
1671
  XMLDocument doc;
27✔
1672

1673
  XMLElement* rootXML = doc.NewElement("root");
27✔
1674
  rootXML->SetAttribute("BTCPP_format", 4);
27✔
1675
  doc.InsertFirstChild(rootXML);
27✔
1676

1677
  addTreeToXML(tree, doc, rootXML, add_metadata, add_builtin_models);
27✔
1678

1679
  XMLPrinter printer;
27✔
1680
  doc.Print(&printer);
27✔
1681
  return std::string(printer.CStr(), size_t(printer.CStrSize() - 1));
54✔
1682
}
27✔
1683

1684
}  // namespace BT
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc