Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/name_validation_rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The following ASCII characters are forbidden in **Model Names** and **Port Names
### Port Name
- **Cannot be empty**
- **Cannot start with a digit**
- **Cannot be a reserved attribute**: `ID`, `name`, `_description`, `_skipIf`, `_successIf`, `_failureIf`, `_while`, `_onSuccess`, `_onFailure`, `_onHalted`, `_post`, `_autoremap`, `__shared_blackboard`
- **Cannot be a reserved attribute**: `ID`, `name`, `_description`, `_skipIf`, `_successIf`, `_failureIf`, `_while`, `_onlyIf`, `_onStart`, `_onSuccess`, `_onFailure`, `_onHalted`, `_post`, `_autoremap`, `__shared_blackboard`
- No forbidden characters (see table above)

### Instance Name
Expand Down
13 changes: 10 additions & 3 deletions docs/pre_postconditions.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Pre-conditions are evaluated **before** a node's `tick()` method is called. They
| `_successIf` | IDLE only | If true, return SUCCESS without calling tick() |
| `_skipIf` | IDLE only | If true, return SKIPPED without calling tick() |
| `_while` | IDLE and RUNNING | If false when IDLE, return SKIPPED. If false when RUNNING, halt node and return SKIPPED |
| `_onlyIf` | IDLE only | If false, return SKIPPED without calling tick() (the opposite of `_skipIf`) |
| `_onStart` | IDLE only | Not a condition: execute the script right before the first tick() |

### Evaluation Order

Expand All @@ -22,12 +24,15 @@ When a node has multiple pre-conditions, they are evaluated in this order:
2. `_successIf`
3. `_skipIf`
4. `_while`
5. `_onlyIf`
6. `_onStart`

The first condition that triggers will determine the result.
The first condition that triggers will determine the result. `_onStart` comes last,
so its script is executed only if the node is actually going to tick.

### Important: One-time Evaluation

**`_failureIf`, `_successIf`, and `_skipIf` are evaluated only once** when the node transitions from IDLE (or SKIPPED) to another state. They are **NOT re-evaluated** while the node is RUNNING.
**`_failureIf`, `_successIf`, `_skipIf`, `_onlyIf` and `_onStart` are evaluated only once** when the node transitions from IDLE (or SKIPPED) to another state. They are **NOT re-evaluated** while the node is RUNNING.

This means if you have:
```xml
Expand All @@ -48,10 +53,12 @@ If `battery_ok` becomes false while `MyAction` is running, the action is interru

### When to Use Each Pre-condition

- **`_skipIf`**: Skip a node without failing the parent (useful in Sequences)
- **`_skipIf`** / **`_onlyIf`**: Skip a node without failing the parent (useful in Sequences)
- **`_failureIf`**: Fail early based on a condition (useful in Fallbacks)
- **`_successIf`**: Succeed early based on a condition
- **`_while`**: Guard that must remain true for the entire execution
- **`_onStart`**: Initialize or reset blackboard entries when the node starts,
e.g. `<MyAction _onStart="attempts := 0"/>`

### The `<Precondition>` Decorator

Expand Down
16 changes: 12 additions & 4 deletions include/behaviortree_cpp/tree_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ using NonPortAttributes = std::unordered_map<std::string, std::string>;
* @brief Pre-conditions that can be attached to any node via XML attributes.
*
* Pre-conditions are evaluated in the order defined by this enum (FAILURE_IF first,
* then SUCCESS_IF, then SKIP_IF, then WHILE_TRUE).
* then SUCCESS_IF, SKIP_IF, WHILE_TRUE, ONLY_IF and finally ON_START).
*
* **Important**: FAILURE_IF, SUCCESS_IF, and SKIP_IF are evaluated **only once**
* **Important**: all of them except WHILE_TRUE are evaluated **only once**
* when the node transitions from IDLE (or SKIPPED) to another state.
* They are NOT re-evaluated while the node is RUNNING.
*
Expand All @@ -62,6 +62,11 @@ using NonPortAttributes = std::unordered_map<std::string, std::string>;
* If false when IDLE, return SKIPPED. If false when RUNNING, halt the node
* and return SKIPPED. This is the only pre-condition that can interrupt
* a running node.
* - `_onlyIf="<script>"`: If false when node is IDLE, return SKIPPED immediately
* (the opposite of `_skipIf`).
* - `_onStart="<script>"`: Not a condition: the script is executed right before
* the first tick() of the node, i.e. only if none of the pre-conditions above
* returned a status.
*
* If you need a condition to be re-evaluated on every tick, use the
* `<Precondition>` decorator node with `else="RUNNING"` instead of these attributes.
Expand All @@ -72,11 +77,14 @@ enum class PreCond
SUCCESS_IF,
SKIP_IF,
WHILE_TRUE,
// new values are appended to the end to preserve ABI
ONLY_IF,
ON_START,
COUNT_
};

static const std::array<std::string, 4> PreCondNames = { //
"_failureIf", "_successIf", "_skipIf", "_while"
static const std::array<std::string, 6> PreCondNames = { //
"_failureIf", "_successIf", "_skipIf", "_while", "_onlyIf", "_onStart"
};

enum class PostCond
Expand Down
57 changes: 35 additions & 22 deletions src/tree_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,8 @@
{
Ast::Environment env = { config().blackboard, config().enums };

// Check pre-conditions in order: FAILURE_IF, SUCCESS_IF, SKIP_IF, WHILE_TRUE.
// IMPORTANT: _failureIf, _successIf, and _skipIf are evaluated ONLY when the
// Check pre-conditions in the order of the PreCond enum.
// IMPORTANT: all of them, except _while, are evaluated ONLY when the
// node is IDLE or SKIPPED. They are NOT re-evaluated while the node is RUNNING.
for(size_t index = 0; index < size_t(PreCond::COUNT_); index++)
{
Expand All @@ -217,31 +217,44 @@
}

const auto preID = static_cast<PreCond>(index);
auto is_true = [&]() { return parse_executor(env).cast<bool>(); };

// _failureIf, _successIf, _skipIf: only checked when IDLE or SKIPPED
// _while: checked here AND also when RUNNING (see below)
if(_p->status == NodeStatus::IDLE || _p->status == NodeStatus::SKIPPED)
{
// what to do if the condition is true
if(parse_executor(env).cast<bool>())
switch(preID)
{
if(preID == PreCond::FAILURE_IF)
{
return NodeStatus::FAILURE;
}
if(preID == PreCond::SUCCESS_IF)
{
return NodeStatus::SUCCESS;
}
if(preID == PreCond::SKIP_IF)
{
return NodeStatus::SKIPPED;
}
}
// if the conditions is false
else if(preID == PreCond::WHILE_TRUE)
{
return NodeStatus::SKIPPED;
case PreCond::FAILURE_IF:
if(is_true())
{
return NodeStatus::FAILURE;
}
break;
case PreCond::SUCCESS_IF:
if(is_true())

Check failure on line 234 in src/tree_node.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=BehaviorTree_BehaviorTree.CPP&issues=AaF0l0p_7mwb80X4oYeT&open=AaF0l0p_7mwb80X4oYeT&pullRequest=1261
{
return NodeStatus::SUCCESS;
}
break;
case PreCond::SKIP_IF:
if(is_true())

Check failure on line 240 in src/tree_node.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=BehaviorTree_BehaviorTree.CPP&issues=AaF0l0p_7mwb80X4oYeU&open=AaF0l0p_7mwb80X4oYeU&pullRequest=1261
{
return NodeStatus::SKIPPED;
}
break;
case PreCond::WHILE_TRUE:
case PreCond::ONLY_IF:
if(!is_true())

Check failure on line 247 in src/tree_node.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=BehaviorTree_BehaviorTree.CPP&issues=AaF0l0p_7mwb80X4oYeV&open=AaF0l0p_7mwb80X4oYeV&pullRequest=1261
{
return NodeStatus::SKIPPED;
}
break;
case PreCond::ON_START:
// the last one: reaching it means that the node is going to tick
parse_executor(env);
break;
case PreCond::COUNT_:
break;
}
}
else if(_p->status == NodeStatus::RUNNING && preID == PreCond::WHILE_TRUE)
Expand Down
2 changes: 2 additions & 0 deletions src/xml_parsing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1582,6 +1582,8 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
</xs:complexType>
<xs:attributeGroup name="preconditionAttributeGroup">
<xs:attribute name="_failureIf" type="xs:string" use="optional"/>
<xs:attribute name="_onlyIf" type="xs:string" use="optional"/>
<xs:attribute name="_onStart" type="xs:string" use="optional"/>
<xs:attribute name="_skipIf" type="xs:string" use="optional"/>
<xs:attribute name="_successIf" type="xs:string" use="optional"/>
<xs:attribute name="_while" type="xs:string" use="optional"/>
Expand Down
120 changes: 120 additions & 0 deletions tests/gtest_preconditions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "behaviortree_cpp/basic_types.h"
#include "behaviortree_cpp/bt_factory.h"
#include "behaviortree_cpp/xml_parsing.h"

#include <string>

Expand Down Expand Up @@ -508,3 +509,122 @@ TEST(Preconditions, SkippedSequence)
status = tree.tickWhileRunning();
ASSERT_EQ(status, BT::NodeStatus::SUCCESS);
}

TEST(Preconditions, OnStart)
{
BehaviorTreeFactory factory;
std::array<int, 3> counters{};
RegisterTestTick(factory, "Test", counters);

// _onStart runs after the other preconditions, and only if the node starts:
// TestA is not skipped by the A==1 its own _onStart writes,
// TestB and TestC are short-circuited, so their script must not run.
const std::string xml_text = R"(
<root BTCPP_format="4" >
<BehaviorTree ID="MainTree">
<Sequence>
<Script code="A:=0; B:=0; C:=0"/>
<TestA _skipIf="A==1" _onStart="A:=1"/>
<TestB _skipIf="A==1" _onStart="B:=1"/>
<TestC _successIf="true" _onStart="C:=1"/>
</Sequence>
</BehaviorTree>
</root>)";

auto tree = factory.createTreeFromText(xml_text);
ASSERT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
ASSERT_EQ(counters[0], 1);
ASSERT_EQ(counters[1], 0);
ASSERT_EQ(counters[2], 0);
ASSERT_EQ(tree.rootBlackboard()->get<int>("A"), 1);
ASSERT_EQ(tree.rootBlackboard()->get<int>("B"), 0);
ASSERT_EQ(tree.rootBlackboard()->get<int>("C"), 0);
}

TEST(Preconditions, OnStartRunsOncePerExecution)
{
static constexpr auto xml_text = R"(
<root BTCPP_format="4">
<BehaviorTree>
<KeepRunning _onStart="count += 1"/>
</BehaviorTree>
</root>
)";

BehaviorTreeFactory factory;
factory.registerNodeType<KeepRunning>("KeepRunning");
auto tree = factory.createTreeFromText(xml_text);
tree.rootBlackboard()->set("count", 0);

ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
ASSERT_EQ(tree.rootBlackboard()->get<int>("count"), 1);

// after a halt the node is IDLE again, so the script runs once more
tree.haltTree();
ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
ASSERT_EQ(tree.rootBlackboard()->get<int>("count"), 2);
}

TEST(Preconditions, OnlyIf)
{
BehaviorTreeFactory factory;
std::array<int, 3> counters{};
RegisterTestTick(factory, "Test", counters);

// _onlyIf is the negation of _skipIf; when both are present, either one
// can skip the node
const std::string xml_text = R"(
<root BTCPP_format="4" >
<BehaviorTree ID="MainTree">
<Sequence>
<Script code="A:=1"/>
<TestA _onlyIf="A==1"/>
<TestB _onlyIf="A==2"/>
<TestC _onlyIf="A==1" _skipIf="A==1"/>
</Sequence>
</BehaviorTree>
</root>)";

auto tree = factory.createTreeFromText(xml_text);
ASSERT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
ASSERT_EQ(counters[0], 1);
ASSERT_EQ(counters[1], 0);
ASSERT_EQ(counters[2], 0);
}

TEST(Preconditions, OnlyIfNotCheckedWhenRunning)
{
static constexpr auto xml_text = R"(
<root BTCPP_format="4">
<BehaviorTree>
<KeepRunning _onlyIf="check"/>
</BehaviorTree>
</root>
)";

BehaviorTreeFactory factory;
factory.registerNodeType<KeepRunning>("KeepRunning");
auto tree = factory.createTreeFromText(xml_text);

tree.rootBlackboard()->set("check", false);
ASSERT_EQ(tree.tickOnce(), NodeStatus::SKIPPED);

tree.rootBlackboard()->set("check", true);
ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);

// unlike _while, _onlyIf is ignored while the node is RUNNING
tree.rootBlackboard()->set("check", false);
ASSERT_EQ(tree.tickOnce(), NodeStatus::RUNNING);
}

TEST(Preconditions, NewAttributesAreReserved)
{
EXPECT_TRUE(IsReservedAttribute("_onlyIf"));
EXPECT_TRUE(IsReservedAttribute("_onStart"));

BehaviorTreeFactory factory;
const auto xsd = writeTreeXSD(factory);
EXPECT_NE(xsd.find(R"(name="_onlyIf")"), std::string::npos);
EXPECT_NE(xsd.find(R"(name="_onStart")"), std::string::npos);
}
Loading