Skip to content

Commit 2e63a35

Browse files
facontidavidelukicdarkooAglargilclaude
authored
Add _onlyIf and _onStart preconditions (#820, #895) (#1261)
* Add _onlyIf and _onStart preconditions (#820, #895) - `_onlyIf="<script>"`: if false when the node is IDLE, return SKIPPED (the opposite of `_skipIf`, more readable in most cases). - `_onStart="<script>"`: execute a script right before the first tick() of the node, i.e. only if no other precondition returned a status. Useful to initialize or reset blackboard entries. Both are evaluated only when the node is IDLE/SKIPPED, like `_skipIf`. ONLY_IF and ON_START are appended at the end of PreCond, so the values of the existing ones don't change. Appending ONLY_IF after WHILE_TRUE keeps the semantics: SKIP_IF, WHILE_TRUE and ONLY_IF all return SKIPPED. Behavior change: the attribute names `_onlyIf` and `_onStart` are now reserved. Before, they were stored silently as non-port attributes and did nothing. Supersedes #821 and #932. Co-Authored-By: Darko Lukic <2135826+lukicdarkoo@users.noreply.github.com> Co-Authored-By: Aglargil <34728006+Aglargil@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Preconditions: static_assert on name arrays, fix _onStart comment, document _onStart/_onlyIf caveats Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: cover _onlyIf/_onStart edge cases (while-interrupt, SubTree, ReactiveSequence, WriteTreeToXML) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Darko Lukic <2135826+lukicdarkoo@users.noreply.github.com> Co-authored-by: Aglargil <34728006+Aglargil@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4690bea commit 2e63a35

6 files changed

Lines changed: 373 additions & 30 deletions

File tree

‎docs/name_validation_rules.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The following ASCII characters are forbidden in **Model Names** and **Port Names
3939
### Port Name
4040
- **Cannot be empty**
4141
- **Cannot start with a digit**
42-
- **Cannot be a reserved attribute**: `ID`, `name`, `_description`, `_skipIf`, `_successIf`, `_failureIf`, `_while`, `_onSuccess`, `_onFailure`, `_onHalted`, `_post`, `_autoremap`, `__shared_blackboard`
42+
- **Cannot be a reserved attribute**: `ID`, `name`, `_description`, `_skipIf`, `_successIf`, `_failureIf`, `_while`, `_onlyIf`, `_onStart`, `_onSuccess`, `_onFailure`, `_onHalted`, `_post`, `_autoremap`, `__shared_blackboard`
4343
- No forbidden characters (see table above)
4444

4545
### Instance Name

‎docs/pre_postconditions.md‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Pre-conditions are evaluated **before** a node's `tick()` method is called. They
1414
| `_successIf` | IDLE only | If true, return SUCCESS without calling tick() |
1515
| `_skipIf` | IDLE only | If true, return SKIPPED without calling tick() |
1616
| `_while` | IDLE and RUNNING | If false when IDLE, return SKIPPED. If false when RUNNING, halt node and return SKIPPED |
17+
| `_onlyIf` | IDLE only | If false, return SKIPPED without calling tick() (the opposite of `_skipIf`) |
18+
| `_onStart` | IDLE only | Not a condition: execute the script right before the first tick() |
1719

1820
### Evaluation Order
1921

@@ -22,12 +24,16 @@ When a node has multiple pre-conditions, they are evaluated in this order:
2224
2. `_successIf`
2325
3. `_skipIf`
2426
4. `_while`
27+
5. `_onlyIf`
28+
6. `_onStart`
2529

26-
The first condition that triggers will determine the result.
30+
The first condition that triggers will determine the result. `_onStart` comes last,
31+
so its script is executed only if no other pre-condition has short-circuited the node.
32+
If both `_skipIf` and `_onlyIf` are present, either one can skip the node.
2733

2834
### Important: One-time Evaluation
2935

30-
**`_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.
36+
**`_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.
3137

3238
This means if you have:
3339
```xml
@@ -48,10 +54,25 @@ If `battery_ok` becomes false while `MyAction` is running, the action is interru
4854

4955
### When to Use Each Pre-condition
5056

51-
- **`_skipIf`**: Skip a node without failing the parent (useful in Sequences)
57+
- **`_skipIf`** / **`_onlyIf`**: Skip a node without failing the parent (useful in Sequences)
5258
- **`_failureIf`**: Fail early based on a condition (useful in Fallbacks)
5359
- **`_successIf`**: Succeed early based on a condition
5460
- **`_while`**: Guard that must remain true for the entire execution
61+
- **`_onStart`**: Initialize or reset blackboard entries when the node starts,
62+
e.g. `<MyAction _onStart="attempts := 0"/>`
63+
64+
### Notes on `_onStart`
65+
66+
- It runs **every time the node starts from IDLE**, not once per tree execution. Inside a
67+
`Retry` or `Repeat`, the child goes back to IDLE after each iteration, so
68+
`_onStart="attempts := 0"` is re-executed on every iteration.
69+
Likewise, a node placed before a RUNNING sibling in a `ReactiveSequence` is halted and
70+
re-ticked from IDLE on every tick, so its `_onStart` runs on every tick.
71+
- It runs just before `tick()`, but a pre-tick callback (see `setPreTickFunction()`, used for
72+
instance by Groot2 breakpoints and status injection) may still substitute the result after
73+
`_onStart` has run, in which case `tick()` is not called.
74+
- `ThreadedAction` does not evaluate any pre-condition, `_onlyIf` and `_onStart` included:
75+
it runs `tick()` in its own thread, bypassing the pre/post-condition checks.
5576

5677
### The `<Precondition>` Decorator
5778

‎include/behaviortree_cpp/tree_node.h‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ using NonPortAttributes = std::unordered_map<std::string, std::string>;
4848
* @brief Pre-conditions that can be attached to any node via XML attributes.
4949
*
5050
* Pre-conditions are evaluated in the order defined by this enum (FAILURE_IF first,
51-
* then SUCCESS_IF, then SKIP_IF, then WHILE_TRUE).
51+
* then SUCCESS_IF, SKIP_IF, WHILE_TRUE, ONLY_IF and finally ON_START).
5252
*
53-
* **Important**: FAILURE_IF, SUCCESS_IF, and SKIP_IF are evaluated **only once**
53+
* **Important**: all of them except WHILE_TRUE are evaluated **only once**
5454
* when the node transitions from IDLE (or SKIPPED) to another state.
5555
* They are NOT re-evaluated while the node is RUNNING.
5656
*
@@ -62,6 +62,11 @@ using NonPortAttributes = std::unordered_map<std::string, std::string>;
6262
* If false when IDLE, return SKIPPED. If false when RUNNING, halt the node
6363
* and return SKIPPED. This is the only pre-condition that can interrupt
6464
* a running node.
65+
* - `_onlyIf="<script>"`: If false when node is IDLE, return SKIPPED immediately
66+
* (the opposite of `_skipIf`).
67+
* - `_onStart="<script>"`: Not a condition: the script is executed right before
68+
* the first tick() of the node, i.e. only if none of the pre-conditions above
69+
* returned a status.
6570
*
6671
* If you need a condition to be re-evaluated on every tick, use the
6772
* `<Precondition>` decorator node with `else="RUNNING"` instead of these attributes.
@@ -72,12 +77,17 @@ enum class PreCond
7277
SUCCESS_IF,
7378
SKIP_IF,
7479
WHILE_TRUE,
80+
// new values are appended to the end to preserve ABI
81+
ONLY_IF,
82+
ON_START,
7583
COUNT_
7684
};
7785

78-
static const std::array<std::string, 4> PreCondNames = { //
79-
"_failureIf", "_successIf", "_skipIf", "_while"
86+
static const std::array<std::string, 6> PreCondNames = { //
87+
"_failureIf", "_successIf", "_skipIf", "_while", "_onlyIf", "_onStart"
8088
};
89+
static_assert(PreCondNames.size() == size_t(PreCond::COUNT_),
90+
"PreCondNames must have one entry per PreCond");
8191

8292
enum class PostCond
8393
{
@@ -92,6 +102,8 @@ enum class PostCond
92102
static const std::array<std::string, 4> PostCondNames = { //
93103
"_onHalted", "_onFailure", "_onSuccess", "_post"
94104
};
105+
static_assert(PostCondNames.size() == size_t(PostCond::COUNT_),
106+
"PostCondNames must have one entry per PostCond");
95107

96108
template <>
97109
[[nodiscard]] std::string toStr<BT::PostCond>(const BT::PostCond& cond);

‎src/tree_node.cpp‎

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,8 @@ Expected<NodeStatus> TreeNode::checkPreConditions()
215215
{
216216
Ast::Environment env = { config().blackboard, config().enums };
217217

218-
// Check pre-conditions in order: FAILURE_IF, SUCCESS_IF, SKIP_IF, WHILE_TRUE.
219-
// IMPORTANT: _failureIf, _successIf, and _skipIf are evaluated ONLY when the
218+
// Check pre-conditions in the order of the PreCond enum.
219+
// IMPORTANT: all of them, except _while, are evaluated ONLY when the
220220
// node is IDLE or SKIPPED. They are NOT re-evaluated while the node is RUNNING.
221221
for(size_t index = 0; index < size_t(PreCond::COUNT_); index++)
222222
{
@@ -227,31 +227,45 @@ Expected<NodeStatus> TreeNode::checkPreConditions()
227227
}
228228

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

231-
// _failureIf, _successIf, _skipIf: only checked when IDLE or SKIPPED
232232
// _while: checked here AND also when RUNNING (see below)
233233
if(_p->status == NodeStatus::IDLE || _p->status == NodeStatus::SKIPPED)
234234
{
235-
// what to do if the condition is true
236-
if(parse_executor(env).cast<bool>())
235+
switch(preID)
237236
{
238-
if(preID == PreCond::FAILURE_IF)
239-
{
240-
return NodeStatus::FAILURE;
241-
}
242-
if(preID == PreCond::SUCCESS_IF)
243-
{
244-
return NodeStatus::SUCCESS;
245-
}
246-
if(preID == PreCond::SKIP_IF)
247-
{
248-
return NodeStatus::SKIPPED;
249-
}
250-
}
251-
// if the conditions is false
252-
else if(preID == PreCond::WHILE_TRUE)
253-
{
254-
return NodeStatus::SKIPPED;
237+
case PreCond::FAILURE_IF:
238+
if(is_true())
239+
{
240+
return NodeStatus::FAILURE;
241+
}
242+
break;
243+
case PreCond::SUCCESS_IF:
244+
if(is_true())
245+
{
246+
return NodeStatus::SUCCESS;
247+
}
248+
break;
249+
case PreCond::SKIP_IF:
250+
if(is_true())
251+
{
252+
return NodeStatus::SKIPPED;
253+
}
254+
break;
255+
case PreCond::WHILE_TRUE:
256+
case PreCond::ONLY_IF:
257+
if(!is_true())
258+
{
259+
return NodeStatus::SKIPPED;
260+
}
261+
break;
262+
case PreCond::ON_START:
263+
// the last one: no precondition returned a status, so the node proceeds
264+
// to tick(). A pre-tick callback (e.g. Groot2) may still substitute the result.
265+
parse_executor(env);
266+
break;
267+
case PreCond::COUNT_:
268+
break;
255269
}
256270
}
257271
else if(_p->status == NodeStatus::RUNNING && preID == PreCond::WHILE_TRUE)

‎src/xml_parsing.cpp‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,8 @@ std::string writeTreeXSD(const BehaviorTreeFactory& factory)
16481648
</xs:complexType>
16491649
<xs:attributeGroup name="preconditionAttributeGroup">
16501650
<xs:attribute name="_failureIf" type="xs:string" use="optional"/>
1651+
<xs:attribute name="_onlyIf" type="xs:string" use="optional"/>
1652+
<xs:attribute name="_onStart" type="xs:string" use="optional"/>
16511653
<xs:attribute name="_skipIf" type="xs:string" use="optional"/>
16521654
<xs:attribute name="_successIf" type="xs:string" use="optional"/>
16531655
<xs:attribute name="_while" type="xs:string" use="optional"/>

0 commit comments

Comments
 (0)