diff --git a/behaviortree_cpp_pluginlib/CMakeLists.txt b/behaviortree_cpp_pluginlib/CMakeLists.txt
index c186f6d..9ccd3b6 100644
--- a/behaviortree_cpp_pluginlib/CMakeLists.txt
+++ b/behaviortree_cpp_pluginlib/CMakeLists.txt
@@ -43,6 +43,7 @@ target_link_libraries(${PROJECT_NAME}
pluginlib::pluginlib
PRIVATE
rcutils::rcutils
+ ament_index_cpp::ament_index_cpp
)
# Install and export resources
diff --git a/behaviortree_cpp_pluginlib/README.md b/behaviortree_cpp_pluginlib/README.md
index bae61d9..bc188c9 100644
--- a/behaviortree_cpp_pluginlib/README.md
+++ b/behaviortree_cpp_pluginlib/README.md
@@ -68,6 +68,38 @@ Simple as 1, 2, 3: depend on this package, register your library as a plugin pro
}
```
+## Registering Subtrees
+
+You can also ship reusable subtrees as `.xml` files. They are discovered and registered by `BT::PluginAwareFactory` automatically, so any loaded tree can reference them via `` without loading files by hand.
+
+1. Write a subtree XML with a single `` (the `ID` is the name you reference as a ``):
+
+ ```xml
+
+
+ ...
+
+
+ ```
+
+2. `CMakeLists.txt` - register the file(s). Use the `SUBTREES` keyword to ship them alongside a plugin library (e.g. one that provides the nodes the subtree uses):
+
+ ```cmake
+ register_behaviortree_cpp_plugin(my_plugin_library
+ SUBTREES trees/go_and_beep.xml
+ )
+ ```
+
+ Or, for a package that ships subtrees but builds no plugin library, use the standalone function:
+
+ ```cmake
+ register_behaviortree_cpp_subtrees(FILES trees/go_and_beep.xml)
+ # Pass a distinct NAME when calling more than once in a single package:
+ # register_behaviortree_cpp_subtrees(NAME navigation FILES trees/go_and_beep.xml)
+ ```
+
+Note: a subtree may only reference built-in nodes or nodes provided by a loaded plugin. Referencing a node that is registered manually after the factory is constructed is not supported — such a subtree is logged and skipped at load time.
+
## Loading Plugins
To load all registered plugins, link against the exported library target and use the `BT::PluginAwareFactory`
@@ -90,6 +122,25 @@ To load all registered plugins, link against the exported library target and use
...
```
+3. All registered nodes _and_ subtrees are now available. Reference a shipped subtree by its `ID` from any tree you load - no need to load its file yourself:
+
+ ```c++
+ factory.registerBehaviorTreeFromText(R"(
+
+
+
+
+
+
+ )");
+
+ // Or load the tree definition from a file instead:
+ // factory.registerBehaviorTreeFromFile("path/to/main.xml");
+
+ auto tree = factory.createTree("Main");
+ tree.tickWhileRunning();
+ ```
+
# Implementation Details
For more information about what's happening under the hood to enable these usage patterns, see [DEVELOPING.md](./DEVELOPING.md)
diff --git a/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake b/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake
index 24865b4..cb7eb7d 100644
--- a/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake
+++ b/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake
@@ -17,13 +17,19 @@
#
# Example usage:
# register_behaviortree_cpp_plugin(my_library)
+# register_behaviortree_cpp_plugin(my_library SUBTREES trees/patrol.xml trees/dock.xml)
#
# :param TARGET: name of a valid CMake shared library target to export plugins from
# :type TARGET: string
+# :param SUBTREES: optional list of subtree XML files to ship with this plugin. Each file is
+# installed and registered so that BT::PluginAwareFactory loads it automatically, making its
+# definitions available to any loaded tree via .
+# :type SUBTREES: list of files
#
# @public
#
function(register_behaviortree_cpp_plugin arg_TARGET)
+ cmake_parse_arguments(ARG "" "" "SUBTREES" ${ARGN})
if(NOT arg_TARGET)
message(FATAL_ERROR "register_behaviortree_cpp_plugin() called without TARGET argument")
endif()
@@ -57,4 +63,65 @@ function(register_behaviortree_cpp_plugin arg_TARGET)
)
list(APPEND __PLUGINLIB_PLUGIN_CATEGORIES "behaviortree_cpp")
set(__PLUGINLIB_PLUGIN_CATEGORIES "${__PLUGINLIB_PLUGIN_CATEGORIES}" PARENT_SCOPE)
+
+ # Ship subtree XML alongside this plugin, keyed by the (package-unique) target name.
+ if(ARG_SUBTREES)
+ _register_behaviortree_cpp_subtrees("${arg_TARGET}" ${ARG_SUBTREES})
+ endif()
+endfunction()
+
+#
+# Register BehaviorTree.CPP subtree XML files without a C++ plugin target.
+#
+# Use this for packages that ship reusable subtrees but build no node plugin library. The subtrees
+# are installed and registered so that BT::PluginAwareFactory loads them automatically.
+#
+# Example usage:
+# register_behaviortree_cpp_subtrees(FILES trees/patrol.xml trees/dock.xml)
+# register_behaviortree_cpp_subtrees(NAME navigation FILES trees/patrol.xml)
+#
+# :param NAME: optional group suffix, used to build a unique resource marker. Defaults to "subtrees".
+# Pass distinct NAMEs when calling this more than once in a single package.
+# :type NAME: string
+# :param FILES: list of subtree XML files to install and register.
+# :type FILES: list of files
+#
+# @public
+#
+function(register_behaviortree_cpp_subtrees)
+ cmake_parse_arguments(ARG "" "NAME" "FILES" ${ARGN})
+ if(NOT ARG_FILES)
+ message(FATAL_ERROR "register_behaviortree_cpp_subtrees() called without FILES argument")
+ endif()
+ set(marker_suffix "subtrees")
+ if(ARG_NAME)
+ set(marker_suffix "${ARG_NAME}")
+ endif()
+ _register_behaviortree_cpp_subtrees("${marker_suffix}" ${ARG_FILES})
+endfunction()
+
+#
+# Internal helper: install subtree XML files and register them in the "behaviortree_cpp_subtrees"
+# ament resource index category for BT::PluginAwareFactory to discover at runtime.
+#
+# The marker is named "__" so repeated calls in one package don't
+# collide (ament_index_register_resource uses file(GENERATE), which errors on a reused path).
+#
+function(_register_behaviortree_cpp_subtrees marker_suffix)
+ set(marker_content "")
+ foreach(subtree_xml ${ARGN})
+ get_filename_component(subtree_abs "${subtree_xml}" ABSOLUTE)
+ if(NOT EXISTS "${subtree_abs}")
+ message(FATAL_ERROR "register subtrees: file does not exist: ${subtree_abs}")
+ endif()
+ get_filename_component(subtree_name "${subtree_abs}" NAME)
+ install(FILES "${subtree_abs}" DESTINATION share/${PROJECT_NAME}/behaviortree_subtrees)
+ # Share-relative path including the package folder, resolved at runtime as /share/.
+ string(APPEND marker_content "${PROJECT_NAME}/behaviortree_subtrees/${subtree_name}\n")
+ endforeach()
+
+ ament_index_register_resource("behaviortree_cpp_subtrees"
+ CONTENT "${marker_content}"
+ PACKAGE_NAME "${PROJECT_NAME}__${marker_suffix}"
+ )
endfunction()
diff --git a/behaviortree_cpp_pluginlib/package.xml b/behaviortree_cpp_pluginlib/package.xml
index e09de7e..f9876d9 100644
--- a/behaviortree_cpp_pluginlib/package.xml
+++ b/behaviortree_cpp_pluginlib/package.xml
@@ -11,6 +11,7 @@
ament_cmake
ament_cmake_auto
+ ament_index_cpp
behaviortree_cpp
pluginlib
rcutils
diff --git a/behaviortree_cpp_pluginlib/src/factory.cpp b/behaviortree_cpp_pluginlib/src/factory.cpp
index bac0bc7..8427917 100644
--- a/behaviortree_cpp_pluginlib/src/factory.cpp
+++ b/behaviortree_cpp_pluginlib/src/factory.cpp
@@ -14,10 +14,14 @@
#include "behaviortree_cpp_pluginlib/factory.hpp"
+#include
#include
+#include
#include
#include
+#include "ament_index_cpp/get_resource.hpp"
+#include "ament_index_cpp/get_resources.hpp"
#include "behaviortree_cpp/bt_factory.h"
#include "behaviortree_cpp_pluginlib/plugin.hpp"
#include "rcutils/logging_macros.h"
@@ -40,10 +44,42 @@ PluginAwareFactory::PluginAwareFactory(const std::vector & plugin_x
class_library_path.c_str());
plugin->registerTypes(*this);
}
+
+ // Load shipped subtree XML only after every node type is registered above: BT.CPP verifies
+ // XML at registration time and rejects subtrees referencing an unregistered node.
+ // Content is a newline-separated list of share-relative paths registered by the
+ // register_behaviortree_cpp_subtrees() CMake helper; the prefix comes from the resource, so it
+ // resolves under both merged and isolated installs.
+ const std::string subtree_resource = "behaviortree_cpp_subtrees";
+ for (const auto & [marker_name, install_prefix] : ament_index_cpp::get_resources(subtree_resource)) {
+ std::string content;
+ if (!ament_index_cpp::get_resource(subtree_resource, marker_name, content)) {
+ continue;
+ }
+ std::istringstream stream(content);
+ std::string relative_path;
+ while (std::getline(stream, relative_path)) {
+ if (relative_path.empty()) {
+ continue;
+ }
+ const std::filesystem::path subtree_path = std::filesystem::path(install_prefix) / "share" / relative_path;
+ try {
+ registerBehaviorTreeFromFile(subtree_path);
+ RCUTILS_LOG_INFO_NAMED("behaviortree_cpp_pluginlib", "Registered subtree(s) from %s", subtree_path.c_str());
+ } catch (const std::exception & e) {
+ // Skip a malformed subtree, or one referencing a node no loaded plugin provides,
+ // rather than failing construction.
+ RCUTILS_LOG_ERROR_NAMED(
+ "behaviortree_cpp_pluginlib", "Failed to register subtree from %s: %s", subtree_path.c_str(), e.what());
+ }
+ }
+ }
}
PluginAwareFactory::~PluginAwareFactory()
{
+ clearRegisteredBehaviorTrees();
+
// First grab all the IDs, since unregistering them modifies the map and invalidates iterators
std::vector ids_to_unregister;
for (const auto & [id, _] : builders()) {
diff --git a/behaviortree_cpp_pluginlib_tests/CMakeLists.txt b/behaviortree_cpp_pluginlib_tests/CMakeLists.txt
index b281899..a1336e8 100644
--- a/behaviortree_cpp_pluginlib_tests/CMakeLists.txt
+++ b/behaviortree_cpp_pluginlib_tests/CMakeLists.txt
@@ -35,12 +35,18 @@ if(BUILD_TESTING)
# Create test libraries
add_library(test_plugin_a SHARED test/plugin_a.cpp)
target_link_libraries(test_plugin_a PUBLIC behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib)
- register_behaviortree_cpp_plugin(test_plugin_a)
+ # Subtree referencing CustomNodeA1, shipped via the SUBTREES keyword.
+ register_behaviortree_cpp_plugin(test_plugin_a
+ SUBTREES ${CMAKE_CURRENT_SOURCE_DIR}/test/subtrees/subtree_uses_a.xml)
add_library(test_plugin_b SHARED test/plugin_b.cpp)
target_link_libraries(test_plugin_b PRIVATE behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib)
register_behaviortree_cpp_plugin(test_plugin_b)
+ # Exercise the standalone subtree registration path (and a second marker in one package).
+ register_behaviortree_cpp_subtrees(NAME standalone
+ FILES ${CMAKE_CURRENT_SOURCE_DIR}/test/subtrees/standalone_subtree.xml)
+
# In the special case of a test-only package that isn't normally installed on target systems,
# we may install the test targets to run install-space testing on them (pluginlib loading)
install(
diff --git a/behaviortree_cpp_pluginlib_tests/test/subtrees/standalone_subtree.xml b/behaviortree_cpp_pluginlib_tests/test/subtrees/standalone_subtree.xml
new file mode 100644
index 0000000..80e0951
--- /dev/null
+++ b/behaviortree_cpp_pluginlib_tests/test/subtrees/standalone_subtree.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/behaviortree_cpp_pluginlib_tests/test/subtrees/subtree_uses_a.xml b/behaviortree_cpp_pluginlib_tests/test/subtrees/subtree_uses_a.xml
new file mode 100644
index 0000000..4852e24
--- /dev/null
+++ b/behaviortree_cpp_pluginlib_tests/test/subtrees/subtree_uses_a.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp b/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp
index 9164541..ec4d6ee 100644
--- a/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp
+++ b/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+#include
#include
#include
#include
@@ -29,3 +30,32 @@ TEST(Factory, AutofactoryEndToEnd)
ASSERT_NO_THROW(builders.at("CustomNodeB2"));
ASSERT_THROW(builders.at("NonexistentNode"), std::out_of_range);
}
+
+TEST(Factory, SubtreesShippedByPluginsAreRegistered)
+{
+ BT::PluginAwareFactory factory;
+
+ const auto trees = factory.registeredBehaviorTrees();
+ // Shipped via the SUBTREES keyword on test_plugin_a.
+ ASSERT_NE(std::find(trees.begin(), trees.end(), "SubtreeUsesA"), trees.end());
+ // Shipped via the standalone register_behaviortree_cpp_subtrees() path.
+ ASSERT_NE(std::find(trees.begin(), trees.end(), "StandaloneSubtree"), trees.end());
+
+ // Each subtree instantiates directly: its concrete node was registered before it was loaded.
+ ASSERT_NO_THROW(factory.createTree("SubtreeUsesA"));
+ ASSERT_NO_THROW(factory.createTree("StandaloneSubtree"));
+}
+
+TEST(Factory, ShippedSubtreeIsUsableFromAnotherTree)
+{
+ BT::PluginAwareFactory factory;
+
+ // A tree loaded later can reference the shipped subtree "for free" via .
+ factory.registerBehaviorTreeFromText(
+ R"(
+
+
+
+ )");
+ ASSERT_NO_THROW(factory.createTree("Main"));
+}