Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/PORT_CONNECTION_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ InputPort<Point2D>("pointE", R"(json:{"x":9,"y":10})", "description")

**Test reference:** `gtest_ports.cpp`

## SubTree Model Defaults with `_autoremap`

A `<SubTree>` entry in `<TreeNodesModel>` can declare an `input_port` with a literal `default`. With `_autoremap="true"`, that default is applied **only if no entry with that name exists in the parent blackboard chain at the moment the tree is created**. If the key is missing at that point, the literal default is stored in the SubTree's own blackboard and shadows the parent: a value written to the parent later (for instance by a `<Script>` that runs before the SubTree) is **not** seen by the SubTree. To make the parent value win, create the key in the blackboard before `createTree()` (e.g. `bb->set("in_value", 7)`), or remap the port explicitly (`in_value="{in_value}"`). Output and bidirectional ports and `{key}` defaults are not affected.

**Test reference:** `gtest_subtree.cpp` - `SubtreeModelDefaultWithAutoremap`, `SubtreeModelDefaultWithAutoremapParentWins`, `SubtreeModelDefaultWithAutoremapCreationTimeRule` tests

## Validation Timeline

### At Tree Creation (XML Parsing)
Expand Down
12 changes: 12 additions & 0 deletions src/xml_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,18 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(
for(const auto& [port_name, port_info] : subtree_model_ports)
{
auto it = subtree_remapping.find(port_name);
const auto& default_value = port_info.defaultValueString();
// _autoremap shares the port with the parent key of the same name; if
// that key doesn't exist (yet), an input port gets its literal default
// (#1271). NOTE: checked at creation time, so a parent key created
// later (e.g. by a Script) is shadowed by the default.
if(it == subtree_remapping.end() && do_autoremap &&
port_info.direction() == PortDirection::INPUT && !default_value.empty() &&
!TreeNode::isBlackboardPointer(default_value) &&
!new_bb->getEntry(port_name))
{
subtree_remapping.insert({ port_name, default_value });
}
// don't override existing remapping
if(it == subtree_remapping.end() && !do_autoremap)
{
Expand Down
127 changes: 127 additions & 0 deletions tests/gtest_subtree.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,133 @@ TEST(SubTree, SubtreeModels)
tree.tickWhileRunning();
}

TEST(SubTree, SubtreeModelDefault)
{
// clang-format off

static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" BTCPP_format="4">
<TreeNodesModel>
<SubTree ID="MySub">
<input_port name="in_value" default="42"/>
</SubTree>
</TreeNodesModel>

<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="MySub" />
</Sequence>
</BehaviorTree>

<BehaviorTree ID="MySub">
<Sequence>
<ScriptCondition code="in_value==42" />
</Sequence>
</BehaviorTree>
</root>
)";

// clang-format on

BehaviorTreeFactory factory;
auto tree = factory.createTreeFromText(xml_text);
tree.tickWhileRunning();
}

TEST(SubTree, SubtreeModelDefaultWithAutoremap)
{
// clang-format off

static const char* xml_text = R"(
<root main_tree_to_execute = "MainTree" BTCPP_format="4">
<TreeNodesModel>
<SubTree ID="MySub">
<input_port name="in_value" default="42"/>
</SubTree>
</TreeNodesModel>

<BehaviorTree ID="MainTree">
<Sequence>
<SubTree ID="MySub" _autoremap="true" />
</Sequence>
</BehaviorTree>

<BehaviorTree ID="MySub">
<Sequence>
<ScriptCondition code="in_value==42" />
</Sequence>
</BehaviorTree>
</root>
)";

// clang-format on

BehaviorTreeFactory factory;
auto tree = factory.createTreeFromText(xml_text);
tree.tickWhileRunning();
}

// With _autoremap, a parent key of the same name wins over the model default
// (#1271), and the default stays local to the SubTree.
TEST(SubTree, SubtreeModelDefaultWithAutoremapParentWins)
{
static const char* xml_text = R"(
<root main_tree_to_execute="MainTree" BTCPP_format="4">
<TreeNodesModel>
<SubTree ID="MySub">
<input_port name="in_value" default="42"/>
<input_port name="other" default="43"/>
</SubTree>
</TreeNodesModel>

<BehaviorTree ID="MainTree">
<SubTree ID="MySub" _autoremap="true" />
</BehaviorTree>

<BehaviorTree ID="MySub">
<ScriptCondition code="in_value==7 &amp;&amp; other==43" />
</BehaviorTree>
</root>)";

BehaviorTreeFactory factory;
auto root_bb = Blackboard::create();
root_bb->set("in_value", 7);
auto tree = factory.createTreeFromText(xml_text, root_bb);
EXPECT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
EXPECT_FALSE(root_bb->getEntry("other"));
}

// Deliberate creation-time rule (#1271): the model default is applied only if no
// entry named "in_value" exists in the parent blackboard chain when the tree is
// CREATED. A key written later (here by a Script) does not win; to make the parent
// value win, set the key in the blackboard before createTree (see test above).
TEST(SubTree, SubtreeModelDefaultWithAutoremapCreationTimeRule)
{
static const char* xml_text = R"(
<root main_tree_to_execute="MainTree" BTCPP_format="4">
<TreeNodesModel>
<SubTree ID="MySub">
<input_port name="in_value" default="42"/>
</SubTree>
</TreeNodesModel>

<BehaviorTree ID="MainTree">
<Sequence>
<Script code="in_value:=7" />
<SubTree ID="MySub" _autoremap="true" />
</Sequence>
</BehaviorTree>

<BehaviorTree ID="MySub">
<ScriptCondition code="in_value==42" />
</BehaviorTree>
</root>)";

BehaviorTreeFactory factory;
auto tree = factory.createTreeFromText(xml_text);
EXPECT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
}

class PrintToConsole : public BT::SyncActionNode
{
public:
Expand Down
Loading