Compare commits

..

No commits in common. "d72c2e06482a53a6dc4ee3c8f4f4cd0813de0840" and "d78adabf7c650010bccc0b085c91fe9213b5e59a" have entirely different histories.

10 changed files with 74 additions and 248 deletions

View File

@ -11,6 +11,15 @@
#include "bb_node.h"
#ifdef LIMBOAI_MODULE
#include "core/error/error_macros.h"
#include "scene/main/node.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/node.hpp>
#endif // LIMBOAI_GDEXTENSION
Variant BBNode::get_value(Node *p_scene_root, const Ref<Blackboard> &p_blackboard, const Variant &p_default) {
ERR_FAIL_NULL_V_MSG(p_scene_root, Variant(), "BBNode: get_value() failed - scene_root is null.");
ERR_FAIL_NULL_V_MSG(p_blackboard, Variant(), "BBNode: get_value() failed - blackboard is null.");
@ -24,10 +33,13 @@ Variant BBNode::get_value(Node *p_scene_root, const Ref<Blackboard> &p_blackboar
if (val.get_type() == Variant::NODE_PATH) {
return p_scene_root->get_node_or_null(val);
} else if (val.get_type() == Variant::OBJECT || val.get_type() == Variant::NIL) {
return val;
} else {
WARN_PRINT("BBNode: Unexpected variant type: " + Variant::get_type_name(val.get_type()) + ". Returning default value.");
return p_default;
Object *obj = val;
if (unlikely(obj == nullptr && val.get_type() != Variant::NIL)) {
WARN_PRINT("BBNode: Unexpected variant type of a blackboard variable.");
return p_default;
} else {
return obj;
}
}
}

View File

@ -43,15 +43,12 @@ void BTForEach::_enter() {
}
BT::Status BTForEach::_tick(double p_delta) {
ERR_FAIL_COND_V_MSG(get_child_count() == 0, FAILURE, "BTForEach: Decorator has no child.");
ERR_FAIL_COND_V_MSG(save_var == StringName(), FAILURE, "BTForEach: Save variable is not set.");
ERR_FAIL_COND_V_MSG(array_var == StringName(), FAILURE, "BTForEach: Array variable is not set.");
ERR_FAIL_COND_V_MSG(get_child_count() == 0, FAILURE, "ForEach decorator has no child.");
ERR_FAIL_COND_V_MSG(save_var == StringName(), FAILURE, "ForEach save variable is not set.");
ERR_FAIL_COND_V_MSG(array_var == StringName(), FAILURE, "ForEach array variable is not set.");
Array arr = get_blackboard()->get_var(array_var, Variant());
if (current_idx >= arr.size()) {
if (current_idx != 0) {
WARN_PRINT("BTForEach: Array size changed during iteration.");
}
if (arr.size() == 0) {
return SUCCESS;
}
Variant elem = arr[current_idx];

View File

@ -46,9 +46,7 @@ String BTSetAgentProperty::_generate_name() {
return "SetAgentProperty ???";
}
return vformat("Set agent.%s %s= %s",
property,
LimboUtility::get_singleton()->get_operation_string(operation),
return vformat("Set agent.%s = %s", property,
value.is_valid() ? Variant(value) : Variant("???"));
}

View File

@ -809,7 +809,7 @@ void LimboAIEditor::_on_visibility_changed() {
}
void LimboAIEditor::_on_header_pressed() {
task_tree->clear_selection();
task_tree->deselect();
#ifdef LIMBOAI_MODULE
if (task_tree->get_bt().is_valid()) {
task_tree->get_bt()->editor_set_section_unfold("blackboard_plan", true);
@ -842,50 +842,35 @@ void LimboAIEditor::_on_history_forward() {
EDIT_RESOURCE(history[idx_history]);
}
void LimboAIEditor::_on_tasks_dragged(const TypedArray<BTTask> &p_tasks, Ref<BTTask> p_to_task, int p_to_pos) {
ERR_FAIL_COND(p_to_task.is_null());
if (p_tasks.is_empty()) {
void LimboAIEditor::_on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type) {
ERR_FAIL_COND(p_type < -1 || p_type > 1);
ERR_FAIL_COND(p_type != 0 && p_to_task->get_parent().is_null());
if (p_task == p_to_task) {
return;
}
// Remove descendants of selected.
Vector<Ref<BTTask>> tasks_list;
for (int i = 0; i < p_tasks.size(); i++) {
Ref<BTTask> task = p_tasks[i];
bool remove = false;
for (int s_idx = 0; s_idx < p_tasks.size(); s_idx++) {
Ref<BTTask> selected = p_tasks[s_idx];
if (task->is_descendant_of(selected)) {
remove = true;
break;
}
}
if (!remove) {
tasks_list.push_back(task);
}
}
EditorUndoRedoManager *undo_redo = _new_undo_redo_action(TTR("Drag BT Task"));
undo_redo->add_do_method(p_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
// Remove all tasks first so adding ordering is stable.
int before_pos = 0;
for (const Ref<BTTask> task : tasks_list) {
if (task->get_parent() == p_to_task && p_to_pos > task->get_index()) {
before_pos += 1;
if (p_type == 0) {
undo_redo->add_do_method(p_to_task.ptr(), LW_NAME(add_child), p_task);
undo_redo->add_undo_method(p_to_task.ptr(), LW_NAME(remove_child), p_task);
} else {
int drop_idx = p_to_task->get_index();
if (p_to_task->get_parent() == p_task->get_parent() && drop_idx > p_task->get_index()) {
drop_idx -= 1;
}
if (p_type == -1) {
undo_redo->add_do_method(p_to_task->get_parent().ptr(), LW_NAME(add_child_at_index), p_task, drop_idx);
undo_redo->add_undo_method(p_to_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
} else if (p_type == 1) {
undo_redo->add_do_method(p_to_task->get_parent().ptr(), LW_NAME(add_child_at_index), p_task, drop_idx + 1);
undo_redo->add_undo_method(p_to_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
}
undo_redo->add_do_method(task->get_parent().ptr(), LW_NAME(remove_child), task);
}
for (int i = 0; i < tasks_list.size(); i++) {
Ref<BTTask> task = tasks_list[i];
undo_redo->add_do_method(p_to_task.ptr(), LW_NAME(add_child_at_index), task, p_to_pos + i - before_pos);
undo_redo->add_undo_method(p_to_task.ptr(), LW_NAME(remove_child), task);
}
// Re-add tasks in later undo action so indexes match the old order.
for (const Ref<BTTask> task : tasks_list) {
undo_redo->add_undo_method(task->get_parent().ptr(), LW_NAME(add_child_at_index), task, task->get_index());
}
undo_redo->add_undo_method(p_task->get_parent().ptr(), "add_child_at_index", p_task, p_task->get_index());
_commit_action_with_update(undo_redo);
}
@ -1398,7 +1383,7 @@ void LimboAIEditor::_notification(int p_what) {
load_btn->connect(LW_NAME(pressed), callable_mp(this, &LimboAIEditor::_popup_file_dialog).bind(load_dialog));
task_tree->connect("rmb_pressed", callable_mp(this, &LimboAIEditor::_on_tree_rmb));
task_tree->connect("task_selected", callable_mp(this, &LimboAIEditor::_on_tree_task_selected));
task_tree->connect("tasks_dragged", callable_mp(this, &LimboAIEditor::_on_tasks_dragged));
task_tree->connect("task_dragged", callable_mp(this, &LimboAIEditor::_on_task_dragged));
task_tree->connect("task_activated", callable_mp(this, &LimboAIEditor::_on_tree_task_activated));
task_tree->connect("probability_clicked", callable_mp(this, &LimboAIEditor::_action_selected).bind(ACTION_EDIT_PROBABILITY));
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed));

View File

@ -239,7 +239,7 @@ private:
void _on_save_pressed();
void _on_history_back();
void _on_history_forward();
void _on_tasks_dragged(const TypedArray<BTTask> &p_tasks, Ref<BTTask> p_to_task, int p_to_pos);
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
void _on_resources_reload(const PackedStringArray &p_resources);
void _on_filesystem_changed();
void _on_new_script_pressed();

View File

@ -21,18 +21,11 @@
#ifdef LIMBOAI_MODULE
#include "core/object/script_language.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/label.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/label.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION
@ -108,7 +101,10 @@ void TaskTree::_update_item(TreeItem *p_item) {
}
void TaskTree::_update_tree() {
Vector<Ref<BTTask>> selection = get_selected_tasks();
Ref<BTTask> sel;
if (tree->get_selected()) {
sel = tree->get_selected()->get_metadata(0);
}
tree->clear();
if (bt.is_null()) {
@ -121,8 +117,9 @@ void TaskTree::_update_tree() {
updating_tree = false;
}
for (const Ref<BTTask> &task : selection) {
add_selection(task);
TreeItem *item = _find_item(sel);
if (item) {
item->select(0);
}
}
@ -228,22 +225,6 @@ void TaskTree::update_task(const Ref<BTTask> &p_task) {
}
}
void TaskTree::add_selection(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
item->select(0);
}
}
void TaskTree::remove_selection(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
item->deselect(0);
}
}
Ref<BTTask> TaskTree::get_selected() const {
if (tree->get_selected()) {
return tree->get_selected()->get_metadata(0);
@ -251,22 +232,11 @@ Ref<BTTask> TaskTree::get_selected() const {
return nullptr;
}
Vector<Ref<BTTask>> TaskTree::get_selected_tasks() const {
Vector<Ref<BTTask>> selected_tasks;
TreeItem *next = tree->get_next_selected(nullptr);
while (next) {
Ref<BTTask> task = next->get_metadata(0);
if (task.is_valid()) {
selected_tasks.push_back(task);
}
next = tree->get_next_selected(next);
void TaskTree::deselect() {
TreeItem *sel = tree->get_selected();
if (sel) {
sel->deselect(0);
}
return selected_tasks;
}
void TaskTree::clear_selection() {
tree->deselect_all();
}
Rect2 TaskTree::get_selected_probability_rect() const {
@ -310,50 +280,9 @@ bool TaskTree::selected_has_probability() const {
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
if (editable && tree->get_item_at_position(p_point)) {
TypedArray<BTTask> selected_tasks;
Vector<Ref<Texture2D>> icons;
TreeItem *next = tree->get_next_selected(nullptr);
while (next) {
Ref<BTTask> task = next->get_metadata(0);
if (task.is_valid()) {
selected_tasks.push_back(task);
icons.push_back(next->get_icon(0));
}
next = tree->get_next_selected(next);
}
if (selected_tasks.is_empty()) {
return Variant();
}
VBoxContainer *vb = memnew(VBoxContainer);
int list_max = 10;
float opacity_step = 1.0f / list_max;
float opacity_item = 1.0f;
for (int i = 0; i < selected_tasks.size(); i++) {
Ref<BTTask> task = Object::cast_to<BTTask>(selected_tasks[i]);
if (i < list_max) {
HBoxContainer *hb = memnew(HBoxContainer);
TextureRect *tf = memnew(TextureRect);
int icon_size = get_theme_constant(LW_NAME(class_icon_size), LW_NAME(Editor));
tf->set_custom_minimum_size(Size2(icon_size, icon_size));
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
tf->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
tf->set_texture(icons[i]);
hb->add_child(tf);
Label *label = memnew(Label);
label->set_text(task->get_task_name());
hb->add_child(label);
vb->add_child(hb);
hb->set_modulate(Color(1, 1, 1, opacity_item));
opacity_item -= opacity_step;
}
}
set_drag_preview(vb);
Dictionary drag_data;
drag_data["type"] = "task";
drag_data["tasks"] = selected_tasks;
drag_data["task"] = tree->get_item_at_position(p_point)->get_metadata(0);
tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN | Tree::DROP_MODE_ON_ITEM);
return drag_data;
}
@ -366,7 +295,7 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
}
Dictionary d = p_data;
if (!d.has("type") || !d.has("tasks")) {
if (!d.has("type") || !d.has("task")) {
return false;
}
@ -376,98 +305,27 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
return false;
}
if (!item->get_parent() && section < 0) { // Before root item.
if (!item->get_parent() && section != 0) { // before/after root item
return false;
}
if (String(d["type"]) == "task") {
TypedArray<BTTask> tasks = d["tasks"];
if (tasks.is_empty()) {
return false; // No tasks.
}
Ref<BTTask> to_task = item->get_metadata(0);
int to_pos = -1;
int type = tree->get_drop_section_at_position(p_point);
_normalize_drop(item, type, to_pos, to_task);
if (to_task.is_null()) {
return false; // Outside root.
}
for (int i = 0; i < tasks.size(); i++) {
Ref<BTTask> task = tasks[i];
if (to_task->is_descendant_of(task) || task == to_task) {
return false; // Don't drop as child of selected tasks.
}
Ref<BTTask> task = d["task"];
const Ref<BTTask> to_task = item->get_metadata(0);
if (task != to_task && !to_task->is_descendant_of(task)) {
return true;
}
}
return true;
return false;
}
void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) {
Dictionary d = p_data;
if (!d.has("tasks")) {
return;
}
TreeItem *item = tree->get_item_at_position(p_point);
int type = tree->get_drop_section_at_position(p_point);
ERR_FAIL_NULL(item);
ERR_FAIL_COND(type < -1 || type > 1);
// The drop behavior depends on the TreeItem's state.
// Normalize and emit the parent task and position instead of exposing TreeItem.
int to_pos = -1;
Ref<BTTask> to_task = item->get_metadata(0);
ERR_FAIL_COND(to_task.is_null());
_normalize_drop(item, type, to_pos, to_task);
emit_signal(LW_NAME(tasks_dragged), d["tasks"], to_task, to_pos);
}
void TaskTree::_normalize_drop(TreeItem *item, int type, int &to_pos, Ref<BTTask> &to_task) const {
switch (type) {
case 0: // Drop as last child of target.
to_pos = to_task->get_child_count();
break;
case -1: // Drop above target.
ERR_FAIL_COND_MSG(to_task->get_parent().is_null(), "Cannot perform drop above the root task!");
to_pos = to_task->get_index();
{
Vector<Ref<BTTask>> selected = get_selected_tasks();
if (to_task == selected[selected.size()-1]) {
to_pos += 1;
}
}
to_task = to_task->get_parent();
break;
case 1: // Drop below target.
if (item->get_child_count() == 0) {
to_pos = to_task->get_index() + 1;
if (to_task == tree->get_next_selected(nullptr)->get_metadata(0)) {
to_pos -= 1;
}
to_task = to_task->get_parent();
break;
}
if (to_task->get_parent().is_null() || !item->is_collapsed()) { // Insert as first child of target.
to_pos = 0;
} else { // Insert as sibling of target.
TreeItem *lower_sibling = nullptr;
for (int i = to_task->get_index() + 1; i < to_task->get_parent()->get_child_count(); i++) {
TreeItem *c = item->get_parent()->get_child(i);
if (c->is_visible_in_tree()) {
lower_sibling = c;
break;
}
}
if (lower_sibling) {
to_pos = lower_sibling->get_index();
}
to_task = to_task->get_parent();
}
break;
if (item && d.has("task")) {
Ref<BTTask> task = d["task"];
emit_signal(LW_NAME(task_dragged), task, item->get_metadata(0), tree->get_drop_section_at_position(p_point));
}
}
@ -527,7 +385,7 @@ void TaskTree::_notification(int p_what) {
case NOTIFICATION_READY: {
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
// Note: CONNECT_DEFERRED is needed to avoid double updates with set_allow_reselect(true), which breaks folding/unfolding.
tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED);
tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected), CONNECT_DEFERRED);
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
} break;
@ -543,10 +401,8 @@ void TaskTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_bt"), &TaskTree::get_bt);
ClassDB::bind_method(D_METHOD("update_tree"), &TaskTree::update_tree);
ClassDB::bind_method(D_METHOD("update_task", "task"), &TaskTree::update_task);
ClassDB::bind_method(D_METHOD("add_selection", "task"), &TaskTree::add_selection);
ClassDB::bind_method(D_METHOD("remove_selection", "task"), &TaskTree::remove_selection);
ClassDB::bind_method(D_METHOD("get_selected"), &TaskTree::get_selected);
ClassDB::bind_method(D_METHOD("clear_selection"), &TaskTree::clear_selection);
ClassDB::bind_method(D_METHOD("deselect"), &TaskTree::deselect);
ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &TaskTree::_get_drag_data_fw);
ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TaskTree::_can_drop_data_fw);
@ -557,7 +413,8 @@ void TaskTree::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("task_activated"));
ADD_SIGNAL(MethodInfo("probability_clicked"));
ADD_SIGNAL(MethodInfo("tasks_dragged", PropertyInfo(Variant::ARRAY, "tasks", PROPERTY_HINT_ARRAY_TYPE, RESOURCE_TYPE_HINT("BTTask")),
ADD_SIGNAL(MethodInfo("task_dragged",
PropertyInfo(Variant::OBJECT, "task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::OBJECT, "to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::INT, "type")));
}
@ -575,7 +432,6 @@ TaskTree::TaskTree() {
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
tree->set_allow_rmb_select(true);
tree->set_allow_reselect(true);
tree->set_select_mode(Tree::SelectMode::SELECT_MULTI);
tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw));
}

View File

@ -75,7 +75,6 @@ private:
Variant _get_drag_data_fw(const Point2 &p_point);
bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const;
void _drop_data_fw(const Point2 &p_point, const Variant &p_data);
void _normalize_drop(TreeItem *item, int type, int &to_pos, Ref<BTTask> &to_task) const;
void _draw_probability(Object *item_obj, Rect2 rect);
@ -91,11 +90,8 @@ public:
Ref<BehaviorTree> get_bt() const { return bt; }
void update_tree() { _update_tree(); }
void update_task(const Ref<BTTask> &p_task);
void add_selection(const Ref<BTTask> &p_task);
void remove_selection(const Ref<BTTask> &p_task);
Ref<BTTask> get_selected() const;
Vector<Ref<BTTask>> get_selected_tasks() const;
void clear_selection();
void deselect();
Rect2 get_selected_probability_rect() const;
double get_selected_probability_weight() const;

View File

@ -96,22 +96,6 @@ TEST_CASE("[Modules][LimboAI] BTForEach") {
CHECK_ENTRIES_TICKS_EXITS(task, 3, 6, 3);
CHECK(blackboard->get_var("element", "wetgoop") == "mushroom");
}
SUBCASE("Shouldn't crash if elements are removed during iteration") {
CHECK(fe->execute(0.01666) == BTTask::RUNNING);
CHECK(task->get_status() == BTTask::SUCCESS);
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1);
CHECK(blackboard->get_var("element", "wetgoop") == "apple");
arr.clear();
ERR_PRINT_OFF;
CHECK(fe->execute(0.01666) == BTTask::SUCCESS); // Returns SUCCESS and prints a warning without executing child task.
ERR_PRINT_ON;
CHECK(task->get_status() == BTTask::SUCCESS); // Same status.
CHECK_ENTRIES_TICKS_EXITS(task, 1, 1, 1); // Task is not re-executed as there is not enough elements to continue iteration.
CHECK(blackboard->get_var("element", "wetgoop") == "apple"); // Not changed.
}
}
} //namespace TestForEach

View File

@ -78,7 +78,6 @@ LimboStringNames::LimboStringNames() {
HeaderSmall = SN("HeaderSmall");
Help = SN("Help");
icon_max_width = SN("icon_max_width");
class_icon_size = SN("class_icon_size");
id_pressed = SN("id_pressed");
Info = SN("Info");
item_collapsed = SN("item_collapsed");
@ -128,7 +127,7 @@ LimboStringNames::LimboStringNames() {
task_activated = SN("task_activated");
task_button_pressed = SN("task_button_pressed");
task_button_rmb = SN("task_button_rmb");
tasks_dragged = SN("tasks_dragged");
task_dragged = SN("task_dragged");
task_meta = SN("task_meta");
task_selected = SN("task_selected");
text_changed = SN("text_changed");

View File

@ -94,7 +94,6 @@ public:
StringName HeaderSmall;
StringName Help;
StringName icon_max_width;
StringName class_icon_size;
StringName id_pressed;
StringName Info;
StringName item_collapsed;
@ -144,7 +143,7 @@ public:
StringName task_activated;
StringName task_button_pressed;
StringName task_button_rmb;
StringName tasks_dragged;
StringName task_dragged;
StringName task_meta;
StringName task_selected;
StringName text_changed;