Skip to content

Feature/bt impl#15

Merged
kimkulling merged 3 commits into
mainfrom
feature/bt_impl
Jul 1, 2026
Merged

Feature/bt impl#15
kimkulling merged 3 commits into
mainfrom
feature/bt_impl

Conversation

@kimkulling

@kimkulling kimkulling commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Behavior trees now support building AI logic from JSON configuration.
    • Added support for sequence, selector, action, and condition nodes.
  • Bug Fixes

    • Initialization now fails cleanly when the behavior-tree configuration is missing or uses an invalid node type.
    • Improved behavior-tree execution so nodes return meaningful results instead of placeholder values.

@kimkulling kimkulling self-assigned this Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR implements a functional behavior tree node system in behavior_tree.h, adding NodeType/NodeStatus enums and concrete SequenceNode, SelectorNode, ConditionNode, and ActionNode implementations with child ownership and tick logic. A new BehaviorTreeNodeFactory in behavior_tree.cpp constructs nodes from JSON, wired into BehaviorTree::init. An unrelated minor edit changes a variable declaration in app.cpp.

Changes

Behavior Tree Node Implementation

Layer / File(s) Summary
Node type/status contracts and node class definitions
src/runtime/ai/behavior_tree.h
Adds json alias, expands NodeStatus (with INVALID, COUNT) and adds NodeType enum; reworks BehaviorTreeNode into a stateful base class with addChild, type/status getters, and implements concrete tick logic for SequenceNode, SelectorNode, ConditionNode, and ActionNode, including child ownership and destructors.
Node factory and tree initialization wiring
src/runtime/ai/behavior_tree.cpp
Adds BehaviorTreeNodeFactory::createNode to construct node instances based on a JSON "type" field, returning nullptr for missing/unrecognized types; updates BehaviorTree::init to use the factory for mRootNode and fail initialization when the root is nullptr.

App Init Minor Edit

Layer / File(s) Summary
SDL init message variable change
src/runtime/application/app.cpp
Changes a local error-message variable from explicit std::string to auto-deduced type, with a trailing whitespace adjustment.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the changes, but it is too vague to convey the main behavior-tree implementation. Use a more descriptive title like "Implement behavior tree node creation and execution".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/bt_impl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/runtime/ai/behavior_tree.h (3)

72-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Empty virtual method lacks explanatory comment.

SonarCloud flags this empty body. A short comment clarifying that the default is a no-op (leaf nodes have no children) would address it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` at line 72, The empty default implementation
of addChild in BehaviorTreeNode should be documented as an intentional no-op for
nodes that cannot have children. Add a short explanatory comment near addChild
in BehaviorTreeNode clarifying that leaf nodes ignore child attachment by
design, so the empty body is intentional and not missing logic.

Source: Linters/SAST tools


139-139: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Pass func by const reference.

ActionNode(std::function<NodeStatus()> func) takes the callable by value, incurring an unnecessary copy.

♻️ Proposed fix
-explicit ActionNode(std::function<NodeStatus()> func) : mFunc(func) {
+explicit ActionNode(const std::function<NodeStatus()>& func) : mFunc(func) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` at line 139, ActionNode currently takes its
std::function<NodeStatus()> callable by value, which creates an unnecessary
copy; update the ActionNode constructor signature to take func by const
reference and keep the existing mFunc initialization in place so callers can
pass the callable without extra copying.

Source: Linters/SAST tools


71-179: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Public API lacks Doxygen documentation.

BehaviorTreeNode and its derived classes (SequenceNode, ConditionNode, ActionNode, SelectorNode) and their public methods have no @brief/@param/@return comments, unlike the rest of the codebase (e.g. ArgumentParser, FileArchive).

As per coding guidelines, "**/*.h: Use Doxygen-style comments in header files for public APIs (@brief, @param, @return, etc.)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 71 - 179, Add Doxygen-style
documentation to the public API in BehaviorTreeNode and its derived classes so
the header matches the project’s comment guidelines. Document each class and
public method in SequenceNode, ConditionNode, ActionNode, and SelectorNode with
`@brief` plus `@param/`@return where applicable, and include the base
BehaviorTreeNode methods like addChild, getNodeType, and getNodeStatus. Use the
existing public class/method names to place the comments directly above their
declarations.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/ai/behavior_tree.cpp`:
- Around line 45-51: The node factory in behavior_tree.cpp is using raw new for
SequenceNode, SelectorNode, ActionNode, and ConditionNode, which matches the
fragile ownership pattern called out in behavior_tree.h. Update the node
creation path so these branches return and store
std::unique_ptr<BehaviorTreeNode> instead of raw pointers, and adjust the
related BehaviorTree / node ownership flow to use std::make_unique where the
nodes are instantiated. Keep the existing class names and factory logic intact,
but remove manual delete-based ownership from the destructor path by making the
tree manage node lifetime automatically.
- Around line 36-56: `createNode` in `BehaviorTree` only instantiates the
current node from `"type"` and never builds its descendants, so JSON-loaded
trees stay flat. Update the factory to read any `"children"` collection from
`nodeData`, recursively call `createNode` for each child, and attach the results
with `addChild()` on the created `SequenceNode`/`SelectorNode`/other composite
node types. Keep the recursion in `createNode` (and any helper it uses) so
`BehaviorTree::init` produces a full hierarchy instead of only the root node.
- Line 41: The behavior-tree loader is calling get<std::string>() on
nodeData["type"] without verifying the value is actually a string. In
behavior_tree.cpp, update the node parsing logic around nodeData and nodeType to
validate the "type" field before conversion, using the existing JSON checks in
that flow to reject or skip non-string values safely. Make the fix in the code
path that constructs nodeType so behavior-tree loading does not throw
nlohmann::json::type_error on malformed input.

In `@src/runtime/ai/behavior_tree.h`:
- Around line 132-149: `ActionNode(const json&)` leaves `mFunc` unset, so
`tick()` can call an empty function and throw when nodes are built through
`BehaviorTreeNodeFactory::createNode`. Update the JSON-based constructor in
`ActionNode` to initialize `mFunc` the same way the function-based constructor
does, or otherwise ensure factory-created nodes receive a valid callable before
`tick()` runs; apply the same fix pattern used for `ConditionNode`.
- Around line 82-112: SequenceNode and SelectorNode currently own raw-pointer
children in mChildren and delete them manually, but they still allow
copying/moving, which can shallow-copy the pointer list and lead to double-free.
Update the behavior tree node classes by either switching mChildren to
std::unique_ptr ownership or, at minimum, explicitly deleting the copy
constructor, copy assignment operator, move constructor, and move assignment
operator on SequenceNode and SelectorNode so the ownership model is unambiguous
and safe.
- Around line 34-50: The enum members in NodeStatus and NodeType do not follow
the PascalCase convention used elsewhere in the codebase. Update the enum values
in behavior_tree.h so NodeStatus and NodeType use PascalCase names consistently
(matching patterns like ModuleState and LogType), and make sure all references
to those symbols are updated accordingly wherever they are used.
- Around line 74-179: `BehaviorTreeNode`’s `mNodeType` and `mNodeStatus` are
never initialized or updated by `SequenceNode`, `ConditionNode`, `ActionNode`,
or `SelectorNode`, so the getters always return `INVALID`. Add a way for derived
nodes to set the base state in the `BehaviorTreeNode` hierarchy (either via
protected setters or a base constructor), then update each derived constructor
to assign the correct `NodeType` and each `tick()` override to store the
returned `NodeStatus` before returning it.
- Around line 114-130: ConditionNode is wired incorrectly: it should be a leaf
condition, not a SequenceNode, and its callable is never initialized. Update
ConditionNode to derive from BehaviorTreeNode (or the appropriate leaf base),
remove inherited child-sequence behavior, and ensure the JSON constructor or
factory path assigns a valid predicate to mFunc. Also fix tick() to evaluate
mFunc only once, storing the result before returning SUCCESS/FAILURE, and make
sure the JSON name-to-callable registry in behavior_tree.cpp supplies the
function used by ConditionNode.

---

Nitpick comments:
In `@src/runtime/ai/behavior_tree.h`:
- Line 72: The empty default implementation of addChild in BehaviorTreeNode
should be documented as an intentional no-op for nodes that cannot have
children. Add a short explanatory comment near addChild in BehaviorTreeNode
clarifying that leaf nodes ignore child attachment by design, so the empty body
is intentional and not missing logic.
- Line 139: ActionNode currently takes its std::function<NodeStatus()> callable
by value, which creates an unnecessary copy; update the ActionNode constructor
signature to take func by const reference and keep the existing mFunc
initialization in place so callers can pass the callable without extra copying.
- Around line 71-179: Add Doxygen-style documentation to the public API in
BehaviorTreeNode and its derived classes so the header matches the project’s
comment guidelines. Document each class and public method in SequenceNode,
ConditionNode, ActionNode, and SelectorNode with `@brief` plus `@param/`@return
where applicable, and include the base BehaviorTreeNode methods like addChild,
getNodeType, and getNodeStatus. Use the existing public class/method names to
place the comments directly above their declarations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8ba1775-df87-4736-b21f-510f32eaa182

📥 Commits

Reviewing files that changed from the base of the PR and between d9e8e47 and c0eba2b.

📒 Files selected for processing (3)
  • src/runtime/ai/behavior_tree.cpp
  • src/runtime/ai/behavior_tree.h
  • src/runtime/application/app.cpp

Comment on lines +36 to +56
static BehaviorTreeNode* createNode(const json& nodeData) {
if (!nodeData.contains("type")) {
return nullptr;
}

std::string nodeType = nodeData["type"].get<std::string>();

// Create the appropriate node based on the type
if (nodeType == "Sequence") {
return new SequenceNode(nodeData);
} else if (nodeType == "Selector") {
return new SelectorNode(nodeData);
} else if (nodeType == "Action") {
return new ActionNode(nodeData);
} else if (nodeType == "Condition") {
return new ConditionNode(nodeData);
} else {
// Unknown node type
return nullptr;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Factory builds only the root node type — never recurses into children, so JSON-driven tree construction doesn't actually build a tree.

createNode instantiates a single node based on "type" but never reads/iterates a "children" (or similar) array in nodeData to recursively construct and addChild() sub-nodes. As written, any tree loaded via BehaviorTree::init will only ever consist of the single root node — SequenceNode/SelectorNode instances built this way will always have an empty mChildren, making tick() trivially return SUCCESS/FAILURE with no actual behavior-tree traversal.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 47-47: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PO&open=AZ8amkQftSRAbF5Z45PO&pullRequest=15


[failure] 45-45: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PN&open=AZ8amkQftSRAbF5Z45PN&pullRequest=15


[failure] 51-51: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PQ&open=AZ8amkQftSRAbF5Z45PQ&pullRequest=15


[failure] 49-49: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PP&open=AZ8amkQftSRAbF5Z45PP&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.cpp` around lines 36 - 56, `createNode` in
`BehaviorTree` only instantiates the current node from `"type"` and never builds
its descendants, so JSON-loaded trees stay flat. Update the factory to read any
`"children"` collection from `nodeData`, recursively call `createNode` for each
child, and attach the results with `addChild()` on the created
`SequenceNode`/`SelectorNode`/other composite node types. Keep the recursion in
`createNode` (and any helper it uses) so `BehaviorTree::init` produces a full
hierarchy instead of only the root node.

return nullptr;
}

std::string nodeType = nodeData["type"].get<std::string>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and surrounding context.
git ls-files 'src/runtime/ai/behavior_tree.cpp' 'CMakeLists.txt' 'vcpkg.json' 'conanfile.*' 'package.json' '**/json.hpp' '**/nlohmann*' | sed -n '1,200p'

echo
echo '--- behavior_tree.cpp ---'
nl -ba src/runtime/ai/behavior_tree.cpp | sed -n '1,120p'

echo
echo '--- JSON dependency/version hints ---'
rg -n --hidden --glob '!**/.git/**' 'nlohmann|json.hpp|json version|JSON_VERSION|NLOHMANN_JSON' .

Repository: kimkulling/Segfault

Length of output: 283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- behavior_tree.cpp (selected range) ---'
sed -n '1,120p' src/runtime/ai/behavior_tree.cpp

echo
echo '--- CMakeLists.txt ---'
sed -n '1,220p' CMakeLists.txt

echo
echo '--- vcpkg.json ---'
sed -n '1,220p' vcpkg.json

echo
echo '--- nlohmann/json references ---'
rg -n --hidden --glob '!**/.git/**' 'nlohmann|json.hpp|NLOHMANN_JSON|JSON_VERSION|version' .

Repository: kimkulling/Segfault

Length of output: 6588


Guard type before get<std::string>() contains("type") only checks presence, so a non-string value still throws nlohmann::json::type_error here and can abort behavior-tree loading.

Suggested fix
-			std::string nodeType = nodeData["type"].get<std::string>();
+			if (!nodeData["type"].is_string()) {
+				return nullptr;
+			}
+			std::string nodeType = nodeData["type"].get<std::string>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
std::string nodeType = nodeData["type"].get<std::string>();
if (!nodeData["type"].is_string()) {
return nullptr;
}
std::string nodeType = nodeData["type"].get<std::string>();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.cpp` at line 41, The behavior-tree loader is
calling get<std::string>() on nodeData["type"] without verifying the value is
actually a string. In behavior_tree.cpp, update the node parsing logic around
nodeData and nodeType to validate the "type" field before conversion, using the
existing JSON checks in that flow to reject or skip non-string values safely.
Make the fix in the code path that constructs nodeType so behavior-tree loading
does not throw nlohmann::json::type_error on malformed input.

Source: Coding guidelines

Comment on lines +45 to +51
return new SequenceNode(nodeData);
} else if (nodeType == "Selector") {
return new SelectorNode(nodeData);
} else if (nodeType == "Action") {
return new ActionNode(nodeData);
} else if (nodeType == "Condition") {
return new ConditionNode(nodeData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Raw new for node ownership — pairs with missing smart-pointer ownership in the header.

SonarCloud flags each new SequenceNode/SelectorNode/ActionNode/ConditionNode(...) call. Combined with the manual delete in the corresponding destructors (see behavior_tree.h), this is a fragile raw-pointer ownership chain. Switching node creation to std::make_unique/std::unique_ptr<BehaviorTreeNode> would resolve this at the source.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 47-47: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PO&open=AZ8amkQftSRAbF5Z45PO&pullRequest=15


[failure] 45-45: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PN&open=AZ8amkQftSRAbF5Z45PN&pullRequest=15


[failure] 51-51: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PQ&open=AZ8amkQftSRAbF5Z45PQ&pullRequest=15


[failure] 49-49: Replace the use of "new" with an operation that automatically manages the memory.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkQftSRAbF5Z45PP&open=AZ8amkQftSRAbF5Z45PP&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.cpp` around lines 45 - 51, The node factory in
behavior_tree.cpp is using raw new for SequenceNode, SelectorNode, ActionNode,
and ConditionNode, which matches the fragile ownership pattern called out in
behavior_tree.h. Update the node creation path so these branches return and
store std::unique_ptr<BehaviorTreeNode> instead of raw pointers, and adjust the
related BehaviorTree / node ownership flow to use std::make_unique where the
nodes are instantiated. Keep the existing class names and factory logic intact,
but remove manual delete-based ownership from the destructor path by making the
tree manage node lifetime automatically.

Source: Linters/SAST tools

Comment on lines 34 to +50
enum class NodeStatus {
INVALID = -1,
IDLE = 0,
RUNNING = 1,
SUCCESS = 2,
FAILURE = 3,
SKIPPED = 4,
COUNT
};

enum class NodeType {
INVALID = -1,
SEQUENCE,
SELECTOR,
CONDITION,
ACTION,
Count

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Enum values violate PascalCase naming convention.

NodeStatus uses ALL_CAPS (INVALID, IDLE, RUNNING, ...) and NodeType mixes ALL_CAPS values with one PascalCase value (Count). The existing codebase convention for similar enums (e.g. ModuleState, LogType in segfault.h) is PascalCase (Invalid, Init, Running, ...).

♻️ Proposed fix
 enum class NodeStatus {
-    INVALID = -1,
-    IDLE = 0,
-    RUNNING = 1,
-    SUCCESS = 2,
-    FAILURE = 3,
-    SKIPPED = 4,
-    COUNT
+    Invalid = -1,
+    Idle = 0,
+    Running = 1,
+    Success = 2,
+    Failure = 3,
+    Skipped = 4,
+    Count
 };

 enum class NodeType {
-    INVALID = -1,
-    SEQUENCE,
-    SELECTOR,
-    CONDITION,
-    ACTION,
+    Invalid = -1,
+    Sequence,
+    Selector,
+    Condition,
+    Action,
     Count
 };

As per coding guidelines, "Name classes, functions, enums, enum values, and typedefs in PascalCase".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
enum class NodeStatus {
INVALID = -1,
IDLE = 0,
RUNNING = 1,
SUCCESS = 2,
FAILURE = 3,
SKIPPED = 4,
COUNT
};
enum class NodeType {
INVALID = -1,
SEQUENCE,
SELECTOR,
CONDITION,
ACTION,
Count
enum class NodeStatus {
Invalid = -1,
Idle = 0,
Running = 1,
Success = 2,
Failure = 3,
Skipped = 4,
Count
};
enum class NodeType {
Invalid = -1,
Sequence,
Selector,
Condition,
Action,
Count
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 34 - 50, The enum members in
NodeStatus and NodeType do not follow the PascalCase convention used elsewhere
in the codebase. Update the enum values in behavior_tree.h so NodeStatus and
NodeType use PascalCase names consistently (matching patterns like ModuleState
and LogType), and make sure all references to those symbols are updated
accordingly wherever they are used.

Source: Coding guidelines

Comment on lines +74 to +179
NodeType getNodeType() const { return mNodeType; }
NodeStatus getNodeStatus() const { return mNodeStatus; }

private:
NodeType mNodeType{ NodeType::INVALID };
NodeStatus mNodeStatus{ NodeStatus::INVALID };
};

class LeafNode : public BehaviorTreeNode {
class SequenceNode : public BehaviorTreeNode {
public:
explicit SequenceNode(const json& nodeData) {
// empty
}

NodeStatus tick() override {
// Implementation of the leaf node's behavior
return NodeStatus::IDLE;
for (auto *child : mChildren) {
assert(child != nullptr);
auto status = child->tick();
if (status != NodeStatus::SUCCESS) {
return status;
}
}
return NodeStatus::SUCCESS;
}

~SequenceNode() override {
for (auto child : mChildren) {
delete child;
}
mChildren.clear();
}

void addChild(BehaviorTreeNode* node) override {
mChildren.push_back(node);
}

private:
std::vector< BehaviorTreeNode*> mChildren;
};

class SequenceNode : public BehaviorTreeNode {
class ConditionNode : public SequenceNode {
public:
explicit ConditionNode(const json& nodeData) : SequenceNode(nodeData) {
// empty
}

NodeStatus tick() override {
// Implementation of the sequence node's behavior
return NodeStatus::IDLE;
if (mFunc()) {
return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}

return NodeStatus::FAILURE;
}

private:
std::function<bool()> mFunc;
};

class ConditionNode : public LeafNode {
class ActionNode : public BehaviorTreeNode {
public:
explicit ActionNode(const json& nodeData) {
// empty
}


explicit ActionNode(std::function<NodeStatus()> func) : mFunc(func) {
// empty
}

NodeStatus tick() override {
// Implementation of the condition node's behavior
return NodeStatus::IDLE;
return mFunc();
}

private:
std::function<NodeStatus()> mFunc;
};

class ActionNode : public LeafNode {
class SelectorNode : public BehaviorTreeNode {
public:
explicit SelectorNode(const json& nodeData) {
// empty
}

~SelectorNode() override {
for (auto* child : mChildren) {
delete child;
}
mChildren.clear();
}

void addChild(BehaviorTreeNode* node) override {
mChildren.push_back(node);
}

NodeStatus tick() override {
// Implementation of the action node's behavior
return NodeStatus::IDLE;
for (auto* child : mChildren) {
assert(child != nullptr);

auto status = child->tick();
if (status != NodeStatus::FAILURE) return status;
}
return NodeStatus::FAILURE;
}

private:
std::vector< BehaviorTreeNode*> mChildren;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

mNodeType/mNodeStatus are never set by any derived node — getters always report INVALID.

mNodeType and mNodeStatus are private with no setter and no parameterized base constructor, so SequenceNode, ConditionNode, ActionNode, and SelectorNode have no way to set them. getNodeType()/getNodeStatus() will therefore always return NodeType::INVALID/NodeStatus::INVALID regardless of the actual node, and tick() overrides never update mNodeStatus either. This makes both getters effectively dead/incorrect for every instantiated node.

🐛 Suggested fix sketch
 	private:
-		NodeType mNodeType{ NodeType::INVALID };
-		NodeStatus mNodeStatus{ NodeStatus::INVALID };
+		NodeType mNodeType{ NodeType::Invalid };
+		NodeStatus mNodeStatus{ NodeStatus::Invalid };
+
+	protected:
+		void setNodeType(NodeType type) { mNodeType = type; }
+		void setNodeStatus(NodeStatus status) { mNodeStatus = status; }
 	};

Each derived constructor would then call setNodeType(NodeType::Sequence) etc., and each tick() override would call setNodeStatus(status) before returning.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 159-159: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PZ&open=AZ8amkSstSRAbF5Z45PZ&pullRequest=15


[warning] 139-139: Pass large object "func" by reference to const.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PX&open=AZ8amkSstSRAbF5Z45PX&pullRequest=15


[warning] 134-134: Remove the unused parameter "nodeData", make it unnamed, or declare it "[[maybe_unused]]".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PW&open=AZ8amkSstSRAbF5Z45PW&pullRequest=15


[warning] 114-114: Add a using-declaration to this derived class to inherit the constructors of "SequenceNode", and remove the ones you manually duplicated.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PU&open=AZ8amkSstSRAbF5Z45PU&pullRequest=15


[warning] 84-84: Remove the unused parameter "nodeData", make it unnamed, or declare it "[[maybe_unused]]".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PS&open=AZ8amkSstSRAbF5Z45PS&pullRequest=15


[failure] 101-101: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PT&open=AZ8amkSstSRAbF5Z45PT&pullRequest=15


[warning] 153-153: Remove the unused parameter "nodeData", make it unnamed, or declare it "[[maybe_unused]]".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PY&open=AZ8amkSstSRAbF5Z45PY&pullRequest=15


[warning] 120-120: Reduce verbosity with "using enum" for "segfault::ai::NodeStatus".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PV&open=AZ8amkSstSRAbF5Z45PV&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 74 - 179, `BehaviorTreeNode`’s
`mNodeType` and `mNodeStatus` are never initialized or updated by
`SequenceNode`, `ConditionNode`, `ActionNode`, or `SelectorNode`, so the getters
always return `INVALID`. Add a way for derived nodes to set the base state in
the `BehaviorTreeNode` hierarchy (either via protected setters or a base
constructor), then update each derived constructor to assign the correct
`NodeType` and each `tick()` override to store the returned `NodeStatus` before
returning it.

Comment on lines +82 to 112
class SequenceNode : public BehaviorTreeNode {
public:
explicit SequenceNode(const json& nodeData) {
// empty
}

NodeStatus tick() override {
// Implementation of the leaf node's behavior
return NodeStatus::IDLE;
for (auto *child : mChildren) {
assert(child != nullptr);
auto status = child->tick();
if (status != NodeStatus::SUCCESS) {
return status;
}
}
return NodeStatus::SUCCESS;
}

~SequenceNode() override {
for (auto child : mChildren) {
delete child;
}
mChildren.clear();
}

void addChild(BehaviorTreeNode* node) override {
mChildren.push_back(node);
}

private:
std::vector< BehaviorTreeNode*> mChildren;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Raw-pointer child ownership without deleted copy/move ops risks double-free.

SequenceNode/SelectorNode own children via std::vector<BehaviorTreeNode*> and manually delete them in the destructor (flagged by SonarCloud at lines 101 and 159), but neither class deletes its copy constructor/assignment or move operations. Copying either node (e.g. by value) would shallow-copy the pointer vector, and both destructors would then double-delete the same children.

🔒 Suggested fix
 class SequenceNode : public BehaviorTreeNode {
 public:
+    SequenceNode(const SequenceNode&) = delete;
+    SequenceNode& operator=(const SequenceNode&) = delete;
     explicit SequenceNode(const json& nodeData) {

Consider switching mChildren to std::vector<std::unique_ptr<BehaviorTreeNode>> instead, which removes the need for manual delete entirely and resolves the SonarCloud findings at the root.

As per coding guidelines, "delete copy/move operations where appropriate".

Also applies to: 151-179

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 84-84: Remove the unused parameter "nodeData", make it unnamed, or declare it "[[maybe_unused]]".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PS&open=AZ8amkSstSRAbF5Z45PS&pullRequest=15


[failure] 101-101: Rewrite the code so that you no longer need this "delete".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PT&open=AZ8amkSstSRAbF5Z45PT&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 82 - 112, SequenceNode and
SelectorNode currently own raw-pointer children in mChildren and delete them
manually, but they still allow copying/moving, which can shallow-copy the
pointer list and lead to double-free. Update the behavior tree node classes by
either switching mChildren to std::unique_ptr ownership or, at minimum,
explicitly deleting the copy constructor, copy assignment operator, move
constructor, and move assignment operator on SequenceNode and SelectorNode so
the ownership model is unambiguous and safe.

Source: Linters/SAST tools

Comment on lines +114 to 130
class ConditionNode : public SequenceNode {
public:
explicit ConditionNode(const json& nodeData) : SequenceNode(nodeData) {
// empty
}

NodeStatus tick() override {
// Implementation of the sequence node's behavior
return NodeStatus::IDLE;
if (mFunc()) {
return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}

return NodeStatus::FAILURE;
}

private:
std::function<bool()> mFunc;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

ConditionNode is fundamentally broken: inherits from SequenceNode, double-evaluates mFunc(), and mFunc is never initialized.

Three compounding issues:

  1. ConditionNode inherits SequenceNode (gaining a public addChild/mChildren it never ticks), rather than deriving directly from BehaviorTreeNode as a leaf condition node should.
  2. tick() calls mFunc() to check truthiness, then calls mFunc() again inside the ternary — redundant and unsafe if the predicate has side effects.
  3. mFunc is a default-constructed std::function<bool()> with no target; the only constructor (ConditionNode(const json&)) never assigns it. Since the factory only ever uses this constructor, every ConditionNode produced via JSON will throw std::bad_function_call the first time tick() is invoked.
🐛 Minimal fix for the double-call (does not fix the unset `mFunc` root cause)
 NodeStatus tick() override {
-    if (mFunc()) {
-        return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
-    }
-    
-    return NodeStatus::FAILURE;
+    return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
 }
The deeper issue — JSON-constructed condition/action nodes never get a real callable wired up — needs an actual registry mapping JSON-declared names to C++ callables; see the related comment in `behavior_tree.cpp`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class ConditionNode : public SequenceNode {
public:
explicit ConditionNode(const json& nodeData) : SequenceNode(nodeData) {
// empty
}
NodeStatus tick() override {
// Implementation of the sequence node's behavior
return NodeStatus::IDLE;
if (mFunc()) {
return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
return NodeStatus::FAILURE;
}
private:
std::function<bool()> mFunc;
};
class ConditionNode : public SequenceNode {
public:
explicit ConditionNode(const json& nodeData) : SequenceNode(nodeData) {
// empty
}
NodeStatus tick() override {
return mFunc() ? NodeStatus::SUCCESS : NodeStatus::FAILURE;
}
private:
std::function<bool()> mFunc;
};
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 114-114: Add a using-declaration to this derived class to inherit the constructors of "SequenceNode", and remove the ones you manually duplicated.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PU&open=AZ8amkSstSRAbF5Z45PU&pullRequest=15


[warning] 120-120: Reduce verbosity with "using enum" for "segfault::ai::NodeStatus".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PV&open=AZ8amkSstSRAbF5Z45PV&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 114 - 130, ConditionNode is
wired incorrectly: it should be a leaf condition, not a SequenceNode, and its
callable is never initialized. Update ConditionNode to derive from
BehaviorTreeNode (or the appropriate leaf base), remove inherited child-sequence
behavior, and ensure the JSON constructor or factory path assigns a valid
predicate to mFunc. Also fix tick() to evaluate mFunc only once, storing the
result before returning SUCCESS/FAILURE, and make sure the JSON name-to-callable
registry in behavior_tree.cpp supplies the function used by ConditionNode.

Comment on lines +132 to 149
class ActionNode : public BehaviorTreeNode {
public:
explicit ActionNode(const json& nodeData) {
// empty
}


explicit ActionNode(std::function<NodeStatus()> func) : mFunc(func) {
// empty
}

NodeStatus tick() override {
// Implementation of the condition node's behavior
return NodeStatus::IDLE;
return mFunc();
}

private:
std::function<NodeStatus()> mFunc;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

ActionNode(const json&) never sets mFunc; ticking a factory-built ActionNode throws.

The JSON-only constructor (the one the factory uses) leaves mFunc default-constructed (empty). tick() unconditionally calls mFunc(), which throws std::bad_function_call for any node built through BehaviorTreeNodeFactory::createNode. This mirrors the same gap in ConditionNode.

🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 139-139: Pass large object "func" by reference to const.

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PX&open=AZ8amkSstSRAbF5Z45PX&pullRequest=15


[warning] 134-134: Remove the unused parameter "nodeData", make it unnamed, or declare it "[[maybe_unused]]".

See more on https://sonarcloud.io/project/issues?id=kimkulling_Segfault&issues=AZ8amkSstSRAbF5Z45PW&open=AZ8amkSstSRAbF5Z45PW&pullRequest=15

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/ai/behavior_tree.h` around lines 132 - 149, `ActionNode(const
json&)` leaves `mFunc` unset, so `tick()` can call an empty function and throw
when nodes are built through `BehaviorTreeNodeFactory::createNode`. Update the
JSON-based constructor in `ActionNode` to initialize `mFunc` the same way the
function-based constructor does, or otherwise ensure factory-created nodes
receive a valid callable before `tick()` runs; apply the same fix pattern used
for `ConditionNode`.

@kimkulling kimkulling merged commit d7874ef into main Jul 1, 2026
5 checks passed
@kimkulling kimkulling deleted the feature/bt_impl branch July 1, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant