Merge branch 'categories+filtering'

This commit is contained in:
Serhii Snitsaruk 2023-08-28 13:28:44 +02:00
commit f89b7897f6
66 changed files with 1443 additions and 951 deletions

6
SCsub
View File

@ -10,10 +10,12 @@ module_env.add_source_files(env.modules_sources, "blackboard/*.cpp")
module_env.add_source_files(env.modules_sources, "blackboard/bb_param/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/blackboard/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/composites/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/actions/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/decorators/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/conditions/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/misc/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/scene/*.cpp")
module_env.add_source_files(env.modules_sources, "bt/tasks/utility/*.cpp")
if env.editor_build:
module_env.add_source_files(env.modules_sources, "editor/*.cpp")
module_env.add_source_files(env.modules_sources, "editor/debugger/*.cpp")

View File

@ -9,8 +9,6 @@
* =============================================================================
*/
/* bt_check_trigger.h */
#ifndef BT_CHECK_TRIGGER_H
#define BT_CHECK_TRIGGER_H
@ -20,6 +18,7 @@
class BTCheckTrigger : public BTCondition {
GDCLASS(BTCheckTrigger, BTCondition);
TASK_CATEGORY(Blackboard);
private:
String variable;

View File

@ -19,6 +19,7 @@
class BTCheckVar : public BTCondition {
GDCLASS(BTCheckVar, BTCondition);
TASK_CATEGORY(Blackboard);
private:
String variable;

View File

@ -8,7 +8,6 @@
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
/* bt_set_var.h */
#ifndef BT_SET_VAR_H
#define BT_SET_VAR_H
@ -21,6 +20,7 @@
class BTSetVar : public BTAction {
GDCLASS(BTSetVar, BTAction);
TASK_CATEGORY(Blackboard);
private:
String variable;

View File

@ -8,7 +8,6 @@
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
/* bt_comment.h */
#ifndef BT_COMMENT_H
#define BT_COMMENT_H
@ -17,8 +16,8 @@
class BTComment : public BTTask {
GDCLASS(BTComment, BTTask);
TASK_CATEGORY(Utility);
private:
public:
virtual Ref<BTTask> clone() const override;
virtual PackedStringArray get_configuration_warnings() const override;

View File

@ -13,6 +13,7 @@
#define BTTASK_H
#include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/util/limbo_task_db.h"
#include "core/io/resource.h"
#include "core/object/object.h"

View File

@ -16,6 +16,7 @@
class BTDynamicSelector : public BTComposite {
GDCLASS(BTDynamicSelector, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -16,6 +16,7 @@
class BTDynamicSequence : public BTComposite {
GDCLASS(BTDynamicSequence, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -16,6 +16,7 @@
class BTParallel : public BTComposite {
GDCLASS(BTParallel, BTComposite);
TASK_CATEGORY(Composites);
private:
int num_successes_required = 1;

View File

@ -18,6 +18,7 @@
class BTRandomSelector : public BTComposite {
GDCLASS(BTRandomSelector, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -18,6 +18,7 @@
class BTRandomSequence : public BTComposite {
GDCLASS(BTRandomSequence, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -16,6 +16,7 @@
class BTSelector : public BTComposite {
GDCLASS(BTSelector, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -16,6 +16,7 @@
class BTSequence : public BTComposite {
GDCLASS(BTSequence, BTComposite);
TASK_CATEGORY(Composites);
private:
int last_running_idx = 0;

View File

@ -16,6 +16,7 @@
class BTAlwaysFail : public BTDecorator {
GDCLASS(BTAlwaysFail, BTDecorator);
TASK_CATEGORY(Decorators);
protected:
virtual int _tick(double p_delta) override;

View File

@ -16,6 +16,7 @@
class BTAlwaysSucceed : public BTDecorator {
GDCLASS(BTAlwaysSucceed, BTDecorator);
TASK_CATEGORY(Decorators);
protected:
virtual int _tick(double p_delta) override;

View File

@ -18,6 +18,7 @@
class BTCooldown : public BTDecorator {
GDCLASS(BTCooldown, BTDecorator);
TASK_CATEGORY(Decorators);
private:
double duration = 10.0;

View File

@ -16,6 +16,7 @@
class BTDelay : public BTDecorator {
GDCLASS(BTDelay, BTDecorator);
TASK_CATEGORY(Decorators);
private:
double seconds = 1.0;

View File

@ -16,6 +16,7 @@
class BTForEach : public BTDecorator {
GDCLASS(BTForEach, BTDecorator);
TASK_CATEGORY(Decorators);
private:
String array_var;

View File

@ -16,6 +16,7 @@
class BTInvert : public BTDecorator {
GDCLASS(BTInvert, BTDecorator);
TASK_CATEGORY(Decorators);
protected:
virtual int _tick(double p_delta) override;

View File

@ -16,6 +16,7 @@
class BTNewScope : public BTDecorator {
GDCLASS(BTNewScope, BTDecorator);
TASK_CATEGORY(Decorators);
private:
Dictionary blackboard_data;

View File

@ -16,6 +16,7 @@
class BTProbability : public BTDecorator {
GDCLASS(BTProbability, BTDecorator);
TASK_CATEGORY(Decorators);
private:
float run_chance = 0.5;

View File

@ -16,6 +16,7 @@
class BTRepeat : public BTDecorator {
GDCLASS(BTRepeat, BTDecorator);
TASK_CATEGORY(Decorators);
private:
bool forever = false;

View File

@ -16,6 +16,7 @@
class BTRepeatUntilFailure : public BTDecorator {
GDCLASS(BTRepeatUntilFailure, BTDecorator);
TASK_CATEGORY(Decorators);
protected:
virtual int _tick(double p_delta) override;

View File

@ -16,6 +16,7 @@
class BTRepeatUntilSuccess : public BTDecorator {
GDCLASS(BTRepeatUntilSuccess, BTDecorator);
TASK_CATEGORY(Decorators);
protected:
virtual int _tick(double p_delta) override;

View File

@ -16,6 +16,7 @@
class BTRunLimit : public BTDecorator {
GDCLASS(BTRunLimit, BTDecorator);
TASK_CATEGORY(Decorators);
private:
int run_limit = 1;

View File

@ -18,6 +18,7 @@
class BTSubtree : public BTNewScope {
GDCLASS(BTSubtree, BTNewScope);
TASK_CATEGORY(Decorators);
private:
Ref<BehaviorTree> subtree;

View File

@ -16,6 +16,7 @@
class BTTimeLimit : public BTDecorator {
GDCLASS(BTTimeLimit, BTDecorator);
TASK_CATEGORY(Decorators);
private:
double time_limit = 5.0;

View File

@ -20,6 +20,7 @@
class BTAwaitAnimation : public BTAction {
GDCLASS(BTAwaitAnimation, BTAction);
TASK_CATEGORY(Scene);
private:
Ref<BBNode> animation_player_param;

View File

@ -18,6 +18,7 @@
class BTCallMethod : public BTAction {
GDCLASS(BTCallMethod, BTAction);
TASK_CATEGORY(Scene);
private:
StringName method_name;

View File

@ -21,6 +21,7 @@
class BTCheckAgentProperty : public BTCondition {
GDCLASS(BTCheckAgentProperty, BTCondition);
TASK_CATEGORY(Scene);
private:
StringName property;

View File

@ -20,6 +20,7 @@
class BTPauseAnimation : public BTAction {
GDCLASS(BTPauseAnimation, BTAction);
TASK_CATEGORY(Scene);
private:
Ref<BBNode> animation_player_param;

View File

@ -20,6 +20,7 @@
class BTPlayAnimation : public BTAction {
GDCLASS(BTPlayAnimation, BTAction);
TASK_CATEGORY(Scene);
private:
Ref<BBNode> animation_player_param;

View File

@ -18,6 +18,7 @@
class BTSetAgentProperty : public BTAction {
GDCLASS(BTSetAgentProperty, BTAction);
TASK_CATEGORY(Scene);
private:
StringName property;

View File

@ -20,6 +20,7 @@
class BTStopAnimation : public BTAction {
GDCLASS(BTStopAnimation, BTAction);
TASK_CATEGORY(Scene);
private:
Ref<BBNode> animation_player_param;

View File

@ -18,6 +18,7 @@
class BTConsolePrint : public BTAction {
GDCLASS(BTConsolePrint, BTAction);
TASK_CATEGORY(Utility);
private:
String text;

View File

@ -16,6 +16,7 @@
class BTFail : public BTAction {
GDCLASS(BTFail, BTAction);
TASK_CATEGORY(Utility);
protected:
virtual int _tick(double p_delta) override;

View File

@ -16,6 +16,7 @@
class BTRandomWait : public BTAction {
GDCLASS(BTRandomWait, BTAction);
TASK_CATEGORY(Utility);
private:
double min_duration = 1.0;

View File

@ -16,6 +16,7 @@
class BTWait : public BTAction {
GDCLASS(BTWait, BTAction);
TASK_CATEGORY(Utility);
private:
double duration = 1.0;

View File

@ -16,6 +16,7 @@
class BTWaitTicks : public BTAction {
GDCLASS(BTWaitTicks, BTAction);
TASK_CATEGORY(Utility);
private:
int num_ticks = 1;

View File

@ -14,773 +14,24 @@
#include "limbo_ai_editor_plugin.h"
#include "action_banner.h"
#include "modules/limboai/bt/behavior_tree.h"
#include "modules/limboai/bt/tasks/bt_action.h"
#include "modules/limboai/bt/tasks/bt_comment.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_parallel.h"
#include "modules/limboai/bt/tasks/composites/bt_selector.h"
#include "modules/limboai/bt/tasks/composites/bt_sequence.h"
#include "modules/limboai/editor/debugger/limbo_debugger_plugin.h"
#include "modules/limboai/util/limbo_utility.h"
#include "core/config/project_settings.h"
#include "core/error/error_list.h"
#include "core/error/error_macros.h"
#include "core/io/config_file.h"
#include "core/io/dir_access.h"
#include "core/io/image_loader.h"
#include "core/io/resource.h"
#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/math/math_defs.h"
#include "core/math/vector2.h"
#include "core/object/callable_method_pointer.h"
#include "core/object/class_db.h"
#include "core/object/object.h"
#include "core/object/script_language.h"
#include "core/object/undo_redo.h"
#include "core/os/memory.h"
#include "core/string/print_string.h"
#include "core/string/string_name.h"
#include "core/string/ustring.h"
#include "core/templates/list.h"
#include "core/templates/vector.h"
#include "core/typedefs.h"
#include "core/variant/array.h"
#include "core/variant/callable.h"
#include "core/variant/dictionary.h"
#include "core/variant/variant.h"
#include "editor/debugger/editor_debugger_node.h"
#include "editor/debugger/script_editor_debugger.h"
#include "editor/editor_file_system.h"
#include "editor/editor_help.h"
#include "editor/editor_inspector.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/editor_plugin.h"
#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/inspector_dock.h"
#include "editor/plugins/script_editor_plugin.h"
#include "editor/project_settings_editor.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/control.h"
#include "scene/gui/file_dialog.h"
#include "scene/gui/flow_container.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/popup_menu.h"
#include "scene/gui/scroll_container.h"
#include "scene/gui/separator.h"
#include "scene/gui/split_container.h"
#include "scene/gui/tree.h"
#include "servers/display_server.h"
//**** TaskTree
TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx) {
ERR_FAIL_COND_V(p_task.is_null(), nullptr);
TreeItem *item = tree->create_item(p_parent, p_idx);
item->set_metadata(0, p_task);
// p_task->connect("changed"...)
for (int i = 0; i < p_task->get_child_count(); i++) {
_create_tree(p_task->get_child(i), item);
}
_update_item(item);
return item;
}
void TaskTree::_update_item(TreeItem *p_item) {
if (p_item == nullptr) {
return;
}
Ref<BTTask> task = p_item->get_metadata(0);
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
p_item->set_text(0, task->get_task_name());
if (task->is_class_ptr(BTComment::get_class_ptr_static())) {
p_item->set_custom_font(0, (get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts"))));
p_item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
} else if (task->get_custom_name().is_empty()) {
p_item->set_custom_font(0, nullptr);
p_item->clear_custom_color(0);
} else {
p_item->set_custom_font(0, (get_theme_font(SNAME("bold"), SNAME("EditorFonts"))));
// p_item->set_custom_color(0, get_theme_color(SNAME("warning_color"), SNAME("Editor")));
}
String type_arg;
if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) {
type_arg = task->get_script_instance()->get_script()->get_path();
} else {
type_arg = task->get_class();
}
p_item->set_icon(0, LimboUtility::get_singleton()->get_task_icon(type_arg));
p_item->set_icon_max_width(0, 16 * EDSCALE);
p_item->set_editable(0, false);
for (int i = 0; i < p_item->get_button_count(0); i++) {
p_item->erase_button(0, i);
}
PackedStringArray warnings = task->get_configuration_warnings();
String warning_text;
for (int j = 0; j < warnings.size(); j++) {
if (j > 0) {
warning_text += "\n";
}
warning_text += warnings[j];
}
if (!warning_text.is_empty()) {
p_item->add_button(0, get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons")), 0, false, warning_text);
}
// TODO: Update probabilities.
}
void TaskTree::_update_tree() {
Ref<BTTask> sel;
if (tree->get_selected()) {
sel = tree->get_selected()->get_metadata(0);
}
tree->clear();
if (bt.is_null()) {
return;
}
if (bt->get_root_task().is_valid()) {
_create_tree(bt->get_root_task(), nullptr);
}
TreeItem *item = _find_item(sel);
if (item) {
item->select(0);
}
}
TreeItem *TaskTree::_find_item(const Ref<BTTask> &p_task) const {
if (p_task.is_null()) {
return nullptr;
}
TreeItem *item = tree->get_root();
List<TreeItem *> stack;
while (item && item->get_metadata(0) != p_task) {
if (item->get_child_count() > 0) {
stack.push_back(item->get_first_child());
}
item = item->get_next();
if (item == nullptr && !stack.is_empty()) {
item = stack.front()->get();
stack.pop_front();
}
}
return item;
}
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, int p_button_index) {
if (p_button_index == 2) {
emit_signal(SNAME("rmb_pressed"), get_screen_position() + p_pos);
}
}
void TaskTree::_on_item_selected() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid()) {
update_task(last_selected);
if (last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
}
last_selected = get_selected();
last_selected->connect("changed", on_task_changed);
emit_signal(SNAME("task_selected"), last_selected);
}
void TaskTree::_on_item_double_clicked() {
emit_signal(SNAME("task_double_clicked"));
}
void TaskTree::_on_task_changed() {
_update_item(tree->get_selected());
}
void TaskTree::load_bt(const Ref<BehaviorTree> &p_behavior_tree) {
ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "Tried to load a null tree.");
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
bt = p_behavior_tree;
tree->clear();
if (bt->get_root_task().is_valid()) {
_create_tree(bt->get_root_task(), nullptr);
}
}
void TaskTree::unload() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
bt->unreference();
tree->clear();
}
void TaskTree::update_task(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
_update_item(item);
}
}
Ref<BTTask> TaskTree::get_selected() const {
if (tree->get_selected()) {
return tree->get_selected()->get_metadata(0);
}
return nullptr;
}
void TaskTree::deselect() {
TreeItem *sel = tree->get_selected();
if (sel) {
sel->deselect(0);
}
}
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
if (editable && tree->get_item_at_position(p_point)) {
Dictionary drag_data;
drag_data["type"] = "task";
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;
}
return Variant();
}
bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const {
if (!editable) {
return false;
}
Dictionary d = p_data;
if (!d.has("type") || !d.has("task")) {
return false;
}
int section = tree->get_drop_section_at_position(p_point);
TreeItem *item = tree->get_item_at_position(p_point);
if (!item || section < -1 || (section == -1 && !item->get_parent())) {
return false;
}
if (String(d["type"]) == "task") {
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 false;
}
void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) {
Dictionary d = p_data;
TreeItem *item = tree->get_item_at_position(p_point);
if (item && d.has("task")) {
Ref<BTTask> task = d["task"];
emit_signal(SNAME("task_dragged"), task, item->get_metadata(0), tree->get_drop_section_at_position(p_point));
}
}
void TaskTree::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
_update_tree();
} break;
}
}
void TaskTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_bt", "p_behavior_tree"), &TaskTree::load_bt);
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", "p_task"), &TaskTree::update_task);
ClassDB::bind_method(D_METHOD("get_selected"), &TaskTree::get_selected);
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);
ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TaskTree::_drop_data_fw);
ADD_SIGNAL(MethodInfo("rmb_pressed"));
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("task_double_clicked"));
ADD_SIGNAL(MethodInfo("task_dragged",
PropertyInfo(Variant::OBJECT, "p_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::OBJECT, "p_to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::INT, "p_type")));
}
TaskTree::TaskTree() {
editable = true;
tree = memnew(Tree);
add_child(tree);
tree->set_columns(2);
tree->set_column_expand(0, true);
tree->set_column_expand(1, false);
tree->set_column_custom_minimum_width(1, 64);
tree->set_anchor(SIDE_RIGHT, ANCHOR_END);
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
tree->set_allow_rmb_select(true);
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected));
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_double_clicked));
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));
}
TaskTree::~TaskTree() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
}
//**** TaskTree ^
//**** TaskButton
Control *TaskButton::make_custom_tooltip(const String &p_text) const {
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1));
String help_text;
if (!p_text.is_empty()) {
help_text = p_text;
} else {
help_text = "[i]" + TTR("No description.") + "[/i]";
}
help_bit->set_text(help_text);
return help_bit;
}
//**** TaskButton ^
//**** TaskSection
void TaskSection::_on_task_button_pressed(const String &p_task) {
emit_signal(SNAME("task_button_pressed"), p_task);
}
void TaskSection::_on_task_button_gui_input(const Ref<InputEvent> &p_event, const String &p_task) {
if (!p_event->is_pressed()) {
return;
}
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT) {
emit_signal(SNAME("task_button_rmb"), p_task);
}
}
void TaskSection::_on_header_pressed() {
set_collapsed(!is_collapsed());
}
void TaskSection::set_filter(String p_filter_text) {
int num_hidden = 0;
if (p_filter_text.is_empty()) {
for (int i = 0; i < tasks_container->get_child_count(); i++) {
Object::cast_to<Button>(tasks_container->get_child(i))->show();
}
set_visible(tasks_container->get_child_count() > 0);
} else {
for (int i = 0; i < tasks_container->get_child_count(); i++) {
Button *btn = Object::cast_to<Button>(tasks_container->get_child(i));
btn->set_visible(btn->get_text().findn(p_filter_text) != -1);
num_hidden += !btn->is_visible();
}
set_visible(num_hidden < tasks_container->get_child_count());
}
}
void TaskSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta) {
TaskButton *btn = memnew(TaskButton);
btn->set_text(p_name);
btn->set_icon(icon);
btn->set_tooltip_text(p_tooltip);
btn->add_theme_constant_override(SNAME("icon_max_width"), 16 * EDSCALE); // Force user icons to be of the proper size.
btn->connect(SNAME("pressed"), callable_mp(this, &TaskSection::_on_task_button_pressed).bind(p_meta));
btn->connect(SNAME("gui_input"), callable_mp(this, &TaskSection::_on_task_button_gui_input).bind(p_meta));
tasks_container->add_child(btn);
}
void TaskSection::set_collapsed(bool p_collapsed) {
tasks_container->set_visible(!p_collapsed);
section_header->set_icon(p_collapsed ? get_theme_icon(SNAME("GuiTreeArrowRight"), SNAME("EditorIcons")) : get_theme_icon(SNAME("GuiTreeArrowDown"), SNAME("EditorIcons")));
}
bool TaskSection::is_collapsed() const {
return !tasks_container->is_visible();
}
void TaskSection::_notification(int p_what) {
if (p_what == NOTIFICATION_THEME_CHANGED) {
section_header->set_icon(is_collapsed() ? get_theme_icon(SNAME("GuiTreeArrowRight"), SNAME("EditorIcons")) : get_theme_icon(SNAME("GuiTreeArrowDown"), SNAME("EditorIcons")));
section_header->add_theme_font_override(SNAME("font"), get_theme_font(SNAME("bold"), SNAME("EditorFonts")));
}
}
void TaskSection::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_button_pressed"));
ADD_SIGNAL(MethodInfo("task_button_rmb"));
}
TaskSection::TaskSection(String p_category_name) {
section_header = memnew(Button);
add_child(section_header);
section_header->set_text(p_category_name);
section_header->set_focus_mode(FOCUS_NONE);
section_header->connect("pressed", callable_mp(this, &TaskSection::_on_header_pressed));
tasks_container = memnew(HFlowContainer);
add_child(tasks_container);
}
TaskSection::~TaskSection() {
}
//**** TaskSection ^
//**** TaskPanel
void TaskPanel::_menu_action_selected(int p_id) {
ERR_FAIL_COND(context_task.is_empty());
switch (p_id) {
case MENU_OPEN_DOC: {
String help_class;
if (context_task.begins_with("res://")) {
Ref<Script> s = ResourceLoader::load(context_task, "Script");
help_class = s->get_language()->get_global_class_name(context_task);
}
if (help_class.is_empty()) {
// Assuming context task is core class.
help_class = context_task;
}
ScriptEditor::get_singleton()->goto_help("class_name:" + help_class);
EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
} break;
case MENU_EDIT_SCRIPT: {
ERR_FAIL_COND(!context_task.begins_with("res://"));
ScriptEditor::get_singleton()->open_file(context_task);
} break;
case MENU_FAVORITE: {
PackedStringArray favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
if (favorite_tasks.has(context_task)) {
favorite_tasks.erase(context_task);
} else {
favorite_tasks.append(context_task);
}
ProjectSettings::get_singleton()->set_setting("limbo_ai/behavior_tree/favorite_tasks", favorite_tasks);
ProjectSettings::get_singleton()->save();
emit_signal(SNAME("favorite_tasks_changed"));
} break;
}
}
void TaskPanel::_on_task_button_pressed(const String &p_task) {
emit_signal(SNAME("task_selected"), p_task);
}
void TaskPanel::_on_task_button_rmb(const String &p_task) {
ERR_FAIL_COND(p_task.is_empty());
context_task = p_task;
menu->clear();
menu->add_icon_item(get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), TTR("Edit Script"), MENU_EDIT_SCRIPT);
menu->set_item_disabled(MENU_EDIT_SCRIPT, !context_task.begins_with("res://"));
menu->add_icon_item(get_theme_icon(SNAME("Help"), SNAME("EditorIcons")), TTR("Open Documentation"), MENU_OPEN_DOC);
menu->add_separator();
Array favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
if (favorite_tasks.has(context_task)) {
menu->add_icon_item(get_theme_icon(SNAME("NonFavorite"), SNAME("EditorIcons")), TTR("Remove from Favorites"), MENU_FAVORITE);
} else {
menu->add_icon_item(get_theme_icon(SNAME("Favorites"), SNAME("EditorIcons")), TTR("Add to Favorites"), MENU_FAVORITE);
}
menu->reset_size();
menu->set_position(get_screen_position() + get_local_mouse_position());
menu->popup();
}
void TaskPanel::_apply_filter(const String &p_text) {
for (int i = 0; i < sections->get_child_count(); i++) {
TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i));
ERR_FAIL_COND(sec == nullptr);
sec->set_filter(p_text);
}
}
void TaskPanel::refresh() {
filter_edit->set_right_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
HashSet<String> collapsed_sections;
if (sections->get_child_count() == 0) {
// Restore collapsed state from config.
ConfigFile conf;
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
if (conf.load(conf_path) == OK) {
Variant value = conf.get_value("bt_editor", "collapsed_sections", Array());
if (value.is_array()) {
Array arr = value;
for (int i = 0; i < arr.size(); i++) {
if (arr[i].get_type() == Variant::STRING) {
collapsed_sections.insert(arr[i]);
}
}
}
}
} else {
for (int i = 0; i < sections->get_child_count(); i++) {
TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i));
if (sec->is_collapsed()) {
collapsed_sections.insert(sec->get_category_name());
}
sections->get_child(i)->queue_free();
}
}
HashMap<String, List<String>> categorized_tasks;
categorized_tasks["Composites"] = List<String>();
_populate_core_tasks_from_class("BTComposite", &categorized_tasks["Composites"]);
categorized_tasks["Actions"] = List<String>();
_populate_core_tasks_from_class("BTAction", &categorized_tasks["Actions"]);
categorized_tasks["Decorators"] = List<String>();
_populate_core_tasks_from_class("BTDecorator", &categorized_tasks["Decorators"]);
categorized_tasks["Conditions"] = List<String>();
_populate_core_tasks_from_class("BTCondition", &categorized_tasks["Conditions"]);
categorized_tasks["Uncategorized"] = List<String>();
String dir1 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1");
_populate_from_user_dir(dir1, &categorized_tasks);
String dir2 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_2");
_populate_from_user_dir(dir2, &categorized_tasks);
String dir3 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_3");
_populate_from_user_dir(dir3, &categorized_tasks);
List<String> categories;
for (KeyValue<String, List<String>> &K : categorized_tasks) {
K.value.sort();
categories.push_back(K.key);
}
categories.sort();
for (String cat : categories) {
List<String> tasks = categorized_tasks.get(cat);
if (tasks.size() == 0) {
continue;
}
TaskSection *sec = memnew(TaskSection(cat));
for (String task_meta : tasks) {
Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta);
String tname;
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E;
if (task_meta.begins_with("res:")) {
tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case();
E = dd->class_list.find(vformat("\"%s\"", task_meta.trim_prefix("res://")));
if (!E) {
E = dd->class_list.find(tname);
}
} else {
tname = task_meta.trim_prefix("BT");
E = dd->class_list.find(task_meta);
}
String descr;
if (E) {
if (E->value.description.is_empty() || E->value.description.length() > 1000) {
descr = DTR(E->value.brief_description);
} else {
descr = DTR(E->value.description);
}
}
sec->add_task_button(tname, icon, descr, task_meta);
}
sec->set_filter("");
sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPanel::_on_task_button_pressed));
sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPanel::_on_task_button_rmb));
sections->add_child(sec);
sec->set_collapsed(collapsed_sections.has(cat));
}
if (!filter_edit->get_text().is_empty()) {
_apply_filter(filter_edit->get_text());
}
}
void TaskPanel::_populate_core_tasks_from_class(const StringName &p_base_class, List<String> *p_task_classes) {
List<StringName> inheriters;
ClassDB::get_inheriters_from_class(p_base_class, &inheriters);
for (StringName cl : inheriters) {
p_task_classes->push_back(cl);
}
}
void TaskPanel::_populate_from_user_dir(String p_path, HashMap<String, List<String>> *p_categories) {
if (p_path.is_empty()) {
return;
}
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
if (dir->change_dir(p_path) == OK) {
dir->list_dir_begin();
String fn = dir->get_next();
while (!fn.is_empty()) {
if (dir->current_is_dir() && fn != "..") {
String full_path;
String category;
if (fn == ".") {
full_path = p_path;
category = "Uncategorized";
} else {
full_path = p_path.path_join(fn);
category = fn.capitalize();
}
if (!p_categories->has(category)) {
p_categories->insert(category, List<String>());
}
_populate_scripted_tasks_from_dir(full_path, &p_categories->get(category));
}
fn = dir->get_next();
}
dir->list_dir_end();
} else {
ERR_FAIL_MSG(vformat("Failed to list \"%s\" directory.", p_path));
}
}
void TaskPanel::_populate_scripted_tasks_from_dir(String p_path, List<String> *p_task_classes) {
if (p_path.is_empty()) {
return;
}
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
if (dir->change_dir(p_path) == OK) {
dir->list_dir_begin();
String fn = dir->get_next();
while (!fn.is_empty()) {
if (fn.ends_with(".gd")) {
String full_path = p_path.path_join(fn);
p_task_classes->push_back(full_path);
}
fn = dir->get_next();
}
dir->list_dir_end();
} else {
ERR_FAIL_MSG(vformat("Failed to list \"%s\" directory.", p_path));
}
}
void TaskPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_EXIT_TREE: {
if (sections->get_child_count() == 0) {
return;
}
Array collapsed_sections;
for (int i = 0; i < sections->get_child_count(); i++) {
TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i));
if (sec->is_collapsed()) {
collapsed_sections.push_back(sec->get_category_name());
}
}
ConfigFile conf;
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
conf.load(conf_path);
conf.set_value("bt_editor", "collapsed_sections", collapsed_sections);
conf.save(conf_path);
} break;
case NOTIFICATION_THEME_CHANGED: {
refresh_btn->set_icon(get_theme_icon(SNAME("Reload"), SNAME("EditorIcons")));
if (is_visible_in_tree()) {
refresh();
}
} break;
}
}
void TaskPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &TaskPanel::refresh);
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("favorite_tasks_changed"));
}
TaskPanel::TaskPanel() {
VBoxContainer *vb = memnew(VBoxContainer);
add_child(vb);
HBoxContainer *hb = memnew(HBoxContainer);
vb->add_child(hb);
filter_edit = memnew(LineEdit);
filter_edit->set_clear_button_enabled(true);
filter_edit->set_placeholder(TTR("Filter tasks"));
filter_edit->connect("text_changed", callable_mp(this, &TaskPanel::_apply_filter));
filter_edit->set_h_size_flags(SIZE_EXPAND_FILL);
hb->add_child(filter_edit);
refresh_btn = memnew(Button);
refresh_btn->set_tooltip_text(TTR("Refresh tasks"));
refresh_btn->set_flat(true);
refresh_btn->set_focus_mode(FocusMode::FOCUS_NONE);
refresh_btn->connect("pressed", callable_mp(this, &TaskPanel::refresh));
hb->add_child(refresh_btn);
ScrollContainer *sc = memnew(ScrollContainer);
sc->set_h_size_flags(SIZE_EXPAND_FILL);
sc->set_v_size_flags(SIZE_EXPAND_FILL);
vb->add_child(sc);
sections = memnew(VBoxContainer);
sections->set_h_size_flags(SIZE_EXPAND_FILL);
sections->set_v_size_flags(SIZE_EXPAND_FILL);
sc->add_child(sections);
menu = memnew(PopupMenu);
add_child(menu);
menu->connect("id_pressed", callable_mp(this, &TaskPanel::_menu_action_selected));
}
TaskPanel::~TaskPanel() {
}
//**** TaskPanel ^
//**** LimboAIEditor
@ -926,7 +177,7 @@ void LimboAIEditor::edit_bt(Ref<BehaviorTree> p_behavior_tree, bool p_force_refr
usage_hint->hide();
task_tree->show();
task_panel->show();
task_palette->show();
_update_history_buttons();
_update_header();
@ -1245,7 +496,7 @@ void LimboAIEditor::_on_tree_task_double_clicked() {
}
void LimboAIEditor::_on_visibility_changed() {
if (task_tree->is_visible()) {
if (task_tree->is_visible_in_tree()) {
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) {
EditorNode::get_singleton()->edit_resource(sel);
@ -1253,7 +504,7 @@ void LimboAIEditor::_on_visibility_changed() {
EditorNode::get_singleton()->edit_resource(task_tree->get_bt());
}
task_panel->refresh();
task_palette->refresh();
}
_update_favorite_tasks();
}
@ -1485,18 +736,20 @@ void LimboAIEditor::_update_banners() {
void LimboAIEditor::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
ConfigFile conf;
Ref<ConfigFile> cf;
cf.instantiate();
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
if (conf.load(conf_path) == OK) {
hsc->set_split_offset(conf.get_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset()));
if (cf->load(conf_path) == OK) {
hsc->set_split_offset(cf->get_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset()));
}
} break;
case NOTIFICATION_EXIT_TREE: {
ConfigFile conf;
Ref<ConfigFile> cf;
cf.instantiate();
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
conf.load(conf_path);
conf.set_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset());
conf.save(conf_path);
cf->load(conf_path);
cf->set_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset());
cf->save(conf_path);
} break;
case NOTIFICATION_THEME_CHANGED: {
new_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("New"), SNAME("EditorIcons")));
@ -1570,21 +823,12 @@ LimboAIEditor::LimboAIEditor() {
PackedStringArray favorite_tasks_default;
favorite_tasks_default.append("BTSelector");
favorite_tasks_default.append("BTSequence");
favorite_tasks_default.append("BTParallel");
favorite_tasks_default.append("BTComment");
GLOBAL_DEF(PropertyInfo(Variant::PACKED_STRING_ARRAY, "limbo_ai/behavior_tree/favorite_tasks", PROPERTY_HINT_ARRAY_TYPE, "String"), favorite_tasks_default);
fav_tasks_hbox = memnew(HBoxContainer);
toolbar->add_child(fav_tasks_hbox);
comment_btn = memnew(Button);
comment_btn->set_text(TTR("Comment"));
comment_btn->set_icon(LimboUtility::get_singleton()->get_task_icon("BTComment"));
comment_btn->set_tooltip_text(TTR("Add a BTComment task."));
comment_btn->set_flat(true);
comment_btn->set_focus_mode(Control::FOCUS_NONE);
comment_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_add_task_by_class_or_path).bind("BTComment"));
toolbar->add_child(comment_btn);
toolbar->add_child(memnew(VSeparator));
new_btn = memnew(Button);
@ -1683,12 +927,12 @@ LimboAIEditor::LimboAIEditor() {
usage_label->set_text(TTR("Create a new or load an existing behavior tree."));
usage_hint->add_child(usage_label);
task_panel = memnew(TaskPanel());
task_palette = memnew(TaskPalette());
hsc->set_split_offset(-300);
task_panel->connect("task_selected", callable_mp(this, &LimboAIEditor::_add_task_by_class_or_path));
task_panel->connect("favorite_tasks_changed", callable_mp(this, &LimboAIEditor::_update_favorite_tasks));
task_panel->hide();
hsc->add_child(task_panel);
task_palette->connect("task_selected", callable_mp(this, &LimboAIEditor::_add_task_by_class_or_path));
task_palette->connect("favorite_tasks_changed", callable_mp(this, &LimboAIEditor::_update_favorite_tasks));
task_palette->hide();
hsc->add_child(task_palette);
banners = memnew(VBoxContainer);
vbox->add_child(banners);

View File

@ -15,6 +15,8 @@
#include "modules/limboai/bt/behavior_tree.h"
#include "modules/limboai/bt/tasks/bt_task.h"
#include "task_palette.h"
#include "task_tree.h"
#include "core/object/class_db.h"
#include "core/object/object.h"
@ -33,122 +35,6 @@
#include "scene/gui/tree.h"
#include "scene/resources/texture.h"
class TaskTree : public Control {
GDCLASS(TaskTree, Control);
private:
Tree *tree;
Ref<BehaviorTree> bt;
Ref<BTTask> last_selected;
bool editable;
TreeItem *_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx = -1);
void _update_item(TreeItem *p_item);
void _update_tree();
TreeItem *_find_item(const Ref<BTTask> &p_task) const;
void _on_item_selected();
void _on_item_double_clicked();
void _on_item_mouse_selected(const Vector2 &p_pos, int p_button_index);
void _on_task_changed();
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);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void load_bt(const Ref<BehaviorTree> &p_behavior_tree);
void unload();
Ref<BehaviorTree> get_bt() const { return bt; }
void update_tree() { _update_tree(); }
void update_task(const Ref<BTTask> &p_task);
Ref<BTTask> get_selected() const;
void deselect();
virtual bool editor_can_reload_from_file() { return false; }
TaskTree();
~TaskTree();
};
class TaskButton : public Button {
GDCLASS(TaskButton, Button);
public:
virtual Control *make_custom_tooltip(const String &p_text) const override;
};
class TaskSection : public VBoxContainer {
GDCLASS(TaskSection, VBoxContainer);
private:
FlowContainer *tasks_container;
Button *section_header;
void _on_task_button_pressed(const String &p_task);
void _on_task_button_gui_input(const Ref<InputEvent> &p_event, const String &p_task);
void _on_header_pressed();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_filter(String p_filter);
void add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta);
void set_collapsed(bool p_collapsed);
bool is_collapsed() const;
String get_category_name() const { return section_header->get_text(); }
TaskSection(String p_category_name);
~TaskSection();
};
class TaskPanel : public PanelContainer {
GDCLASS(TaskPanel, PanelContainer)
private:
enum MenuAction {
MENU_EDIT_SCRIPT,
MENU_OPEN_DOC,
MENU_FAVORITE,
};
LineEdit *filter_edit;
VBoxContainer *sections;
PopupMenu *menu;
Button *refresh_btn;
String context_task;
void _populate_core_tasks_from_class(const StringName &p_base_class, List<String> *p_task_classes);
void _populate_from_user_dir(String p_path, HashMap<String, List<String>> *p_categories);
void _populate_scripted_tasks_from_dir(String p_path, List<String> *p_task_classes);
void _menu_action_selected(int p_id);
void _on_task_button_pressed(const String &p_task);
void _on_task_button_rmb(const String &p_task);
void _apply_filter(const String &p_text);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void refresh();
TaskPanel();
~TaskPanel();
};
class LimboAIEditor : public Control {
GDCLASS(LimboAIEditor, Control);
@ -185,10 +71,9 @@ private:
FileDialog *load_dialog;
Button *history_back;
Button *history_forward;
TaskPanel *task_panel;
TaskPalette *task_palette;
HBoxContainer *fav_tasks_hbox;
Button *comment_btn;
Button *new_btn;
Button *load_btn;
Button *save_btn;

657
editor/task_palette.cpp Normal file
View File

@ -0,0 +1,657 @@
/**
* task_palette.cpp
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#include "task_palette.h"
#include "modules/limboai/util/limbo_task_db.h"
#include "modules/limboai/util/limbo_utility.h"
#include "core/config/project_settings.h"
#include "editor/editor_help.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/editor_scale.h"
#include "editor/plugins/script_editor_plugin.h"
#include "scene/gui/check_box.h"
//**** TaskButton
Control *TaskButton::make_custom_tooltip(const String &p_text) const {
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1));
String help_text;
if (!p_text.is_empty()) {
help_text = p_text;
} else {
help_text = "[i]" + TTR("No description.") + "[/i]";
}
help_bit->set_text(help_text);
return help_bit;
}
//**** TaskButton ^
//**** TaskPaletteSection
void TaskPaletteSection::_on_task_button_pressed(const String &p_task) {
emit_signal(SNAME("task_button_pressed"), p_task);
}
void TaskPaletteSection::_on_task_button_gui_input(const Ref<InputEvent> &p_event, const String &p_task) {
if (!p_event->is_pressed()) {
return;
}
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT) {
emit_signal(SNAME("task_button_rmb"), p_task);
}
}
void TaskPaletteSection::_on_header_pressed() {
set_collapsed(!is_collapsed());
}
void TaskPaletteSection::set_filter(String p_filter_text) {
int num_hidden = 0;
if (p_filter_text.is_empty()) {
for (int i = 0; i < tasks_container->get_child_count(); i++) {
Object::cast_to<Button>(tasks_container->get_child(i))->show();
}
set_visible(tasks_container->get_child_count() > 0);
} else {
for (int i = 0; i < tasks_container->get_child_count(); i++) {
Button *btn = Object::cast_to<Button>(tasks_container->get_child(i));
btn->set_visible(btn->get_text().findn(p_filter_text) != -1);
num_hidden += !btn->is_visible();
}
set_visible(num_hidden < tasks_container->get_child_count());
}
}
void TaskPaletteSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta) {
TaskButton *btn = memnew(TaskButton);
btn->set_text(p_name);
btn->set_icon(icon);
btn->set_tooltip_text(p_tooltip);
btn->add_theme_constant_override(SNAME("icon_max_width"), 16 * EDSCALE); // Force user icons to be of the proper size.
btn->connect(SNAME("pressed"), callable_mp(this, &TaskPaletteSection::_on_task_button_pressed).bind(p_meta));
btn->connect(SNAME("gui_input"), callable_mp(this, &TaskPaletteSection::_on_task_button_gui_input).bind(p_meta));
tasks_container->add_child(btn);
}
void TaskPaletteSection::set_collapsed(bool p_collapsed) {
tasks_container->set_visible(!p_collapsed);
section_header->set_icon(p_collapsed ? get_theme_icon(SNAME("GuiTreeArrowRight"), SNAME("EditorIcons")) : get_theme_icon(SNAME("GuiTreeArrowDown"), SNAME("EditorIcons")));
}
bool TaskPaletteSection::is_collapsed() const {
return !tasks_container->is_visible();
}
void TaskPaletteSection::_notification(int p_what) {
if (p_what == NOTIFICATION_THEME_CHANGED) {
section_header->set_icon(is_collapsed() ? get_theme_icon(SNAME("GuiTreeArrowRight"), SNAME("EditorIcons")) : get_theme_icon(SNAME("GuiTreeArrowDown"), SNAME("EditorIcons")));
section_header->add_theme_font_override(SNAME("font"), get_theme_font(SNAME("bold"), SNAME("EditorFonts")));
}
}
void TaskPaletteSection::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_button_pressed"));
ADD_SIGNAL(MethodInfo("task_button_rmb"));
}
TaskPaletteSection::TaskPaletteSection(String p_category_name) {
section_header = memnew(Button);
add_child(section_header);
section_header->set_text(p_category_name);
section_header->set_focus_mode(FOCUS_NONE);
section_header->connect("pressed", callable_mp(this, &TaskPaletteSection::_on_header_pressed));
tasks_container = memnew(HFlowContainer);
add_child(tasks_container);
}
TaskPaletteSection::~TaskPaletteSection() {
}
//**** TaskPaletteSection ^
//**** TaskPalette
void TaskPalette::_menu_action_selected(int p_id) {
ERR_FAIL_COND(context_task.is_empty());
switch (p_id) {
case MENU_OPEN_DOC: {
String help_class;
if (context_task.begins_with("res://")) {
Ref<Script> s = ResourceLoader::load(context_task, "Script");
help_class = s->get_language()->get_global_class_name(context_task);
}
if (help_class.is_empty()) {
// Assuming context task is core class.
help_class = context_task;
}
ScriptEditor::get_singleton()->goto_help("class_name:" + help_class);
EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
} break;
case MENU_EDIT_SCRIPT: {
ERR_FAIL_COND(!context_task.begins_with("res://"));
ScriptEditor::get_singleton()->open_file(context_task);
} break;
case MENU_FAVORITE: {
PackedStringArray favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
if (favorite_tasks.has(context_task)) {
favorite_tasks.erase(context_task);
} else {
favorite_tasks.append(context_task);
}
ProjectSettings::get_singleton()->set_setting("limbo_ai/behavior_tree/favorite_tasks", favorite_tasks);
ProjectSettings::get_singleton()->save();
emit_signal(SNAME("favorite_tasks_changed"));
} break;
}
}
void TaskPalette::_on_task_button_pressed(const String &p_task) {
emit_signal(SNAME("task_selected"), p_task);
}
void TaskPalette::_on_task_button_rmb(const String &p_task) {
ERR_FAIL_COND(p_task.is_empty());
context_task = p_task;
menu->clear();
menu->add_icon_item(get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), TTR("Edit Script"), MENU_EDIT_SCRIPT);
menu->set_item_disabled(MENU_EDIT_SCRIPT, !context_task.begins_with("res://"));
menu->add_icon_item(get_theme_icon(SNAME("Help"), SNAME("EditorIcons")), TTR("Open Documentation"), MENU_OPEN_DOC);
menu->add_separator();
Array favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
if (favorite_tasks.has(context_task)) {
menu->add_icon_item(get_theme_icon(SNAME("NonFavorite"), SNAME("EditorIcons")), TTR("Remove from Favorites"), MENU_FAVORITE);
} else {
menu->add_icon_item(get_theme_icon(SNAME("Favorites"), SNAME("EditorIcons")), TTR("Add to Favorites"), MENU_FAVORITE);
}
menu->reset_size();
menu->set_position(get_screen_position() + get_local_mouse_position());
menu->popup();
}
void TaskPalette::_apply_filter(const String &p_text) {
for (int i = 0; i < sections->get_child_count(); i++) {
TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i));
ERR_FAIL_COND(sec == nullptr);
sec->set_filter(p_text);
}
}
void TaskPalette::_update_filter_popup() {
switch (filter_settings.type_filter) {
case FilterSettings::TypeFilter::TYPE_ALL: {
type_all->set_pressed(true);
} break;
case FilterSettings::TypeFilter::TYPE_CORE: {
type_core->set_pressed(true);
} break;
case FilterSettings::TypeFilter::TYPE_USER: {
type_user->set_pressed(true);
} break;
}
switch (filter_settings.category_filter) {
case FilterSettings::CategoryFilter::CATEGORY_ALL: {
category_all->set_pressed(true);
} break;
case FilterSettings::CategoryFilter::CATEGORY_INCLUDE: {
category_include->set_pressed(true);
} break;
case FilterSettings::CategoryFilter::CATEGORY_EXCLUDE: {
category_exclude->set_pressed(true);
} break;
}
while (category_list->get_child_count() > 0) {
Node *item = category_list->get_child(0);
category_list->remove_child(item);
item->queue_free();
}
for (String &cat : LimboTaskDB::get_categories()) {
CheckBox *category_item = memnew(CheckBox);
category_item->set_text(cat);
category_item->set_focus_mode(FocusMode::FOCUS_NONE);
category_item->set_pressed_no_signal(LOGICAL_XOR(
filter_settings.excluded_categories.has(cat),
filter_settings.category_filter == FilterSettings::CategoryFilter::CATEGORY_INCLUDE));
category_item->connect(SNAME("toggled"), callable_mp(this, &TaskPalette::_category_item_toggled).bind(cat));
category_list->add_child(category_item);
}
category_list->reset_size();
Size2 size = category_list->get_size() + Size2(8, 8);
size.width = MIN(size.width, 400 * EDSCALE);
size.height = MIN(size.height, 600 * EDSCALE);
category_scroll->set_custom_minimum_size(size);
category_choice->set_visible(filter_settings.category_filter != FilterSettings::CATEGORY_ALL);
}
void TaskPalette::_show_filter_popup() {
_update_filter_popup();
tool_filters->set_pressed_no_signal(true);
Rect2i rect = tool_filters->get_screen_rect();
rect.position.y += rect.size.height;
rect.size.height = 0;
filter_popup->reset_size();
filter_popup->popup(rect);
}
void TaskPalette::_category_filter_changed() {
if (category_all->is_pressed()) {
filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_ALL;
} else if (category_include->is_pressed()) {
filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_INCLUDE;
} else if (category_exclude->is_pressed()) {
filter_settings.category_filter = FilterSettings::CategoryFilter::CATEGORY_EXCLUDE;
}
for (int i = 0; i < category_list->get_child_count(); i++) {
CheckBox *item = Object::cast_to<CheckBox>(category_list->get_child(i));
item->set_pressed_no_signal(LOGICAL_XOR(
filter_settings.excluded_categories.has(item->get_text()),
filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE));
}
category_choice->set_visible(filter_settings.category_filter != FilterSettings::CATEGORY_ALL);
filter_popup->reset_size();
_filter_data_changed();
}
void TaskPalette::_set_all_filter_categories(bool p_selected) {
for (int i = 0; i < category_list->get_child_count(); i++) {
CheckBox *item = Object::cast_to<CheckBox>(category_list->get_child(i));
item->set_pressed_no_signal(p_selected);
bool excluded = LOGICAL_XOR(p_selected, filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE);
_set_category_excluded(item->get_text(), excluded);
}
_filter_data_changed();
}
void TaskPalette::_type_filter_changed() {
if (type_all->is_pressed()) {
filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_ALL;
} else if (type_core->is_pressed()) {
filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_CORE;
} else if (type_user->is_pressed()) {
filter_settings.type_filter = FilterSettings::TypeFilter::TYPE_USER;
}
_filter_data_changed();
}
void TaskPalette::_category_item_toggled(bool p_pressed, const String &p_category) {
bool excluded = LOGICAL_XOR(p_pressed, filter_settings.category_filter == FilterSettings::CATEGORY_INCLUDE);
_set_category_excluded(p_category, excluded);
_filter_data_changed();
}
void TaskPalette::_filter_data_changed() {
call_deferred(SNAME("refresh"));
_update_filter_button();
}
void TaskPalette::_draw_filter_popup_background() {
category_choice_background->draw(category_choice->get_canvas_item(), Rect2(Point2(), category_choice->get_size()));
}
void TaskPalette::_update_filter_button() {
tool_filters->set_pressed_no_signal(filter_popup->is_visible() ||
filter_settings.type_filter != FilterSettings::TYPE_ALL ||
(filter_settings.category_filter != FilterSettings::CATEGORY_ALL && !filter_settings.excluded_categories.is_empty()));
}
void TaskPalette::refresh() {
filter_edit->set_right_icon(get_theme_icon(SNAME("Search"), SNAME("EditorIcons")));
HashSet<String> collapsed_sections;
if (sections->get_child_count() == 0) {
// Restore collapsed state from config.
Ref<ConfigFile> cf;
cf.instantiate();
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
if (cf->load(conf_path) == OK) {
Variant value = cf->get_value("task_palette", "collapsed_sections", Array());
if (value.is_array()) {
Array arr = value;
for (int i = 0; i < arr.size(); i++) {
if (arr[i].get_type() == Variant::STRING) {
collapsed_sections.insert(arr[i]);
}
}
}
}
} else {
for (int i = 0; i < sections->get_child_count(); i++) {
TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i));
if (sec->is_collapsed()) {
collapsed_sections.insert(sec->get_category_name());
}
sections->get_child(i)->queue_free();
}
}
LimboTaskDB::scan_user_tasks();
List<String> categories = LimboTaskDB::get_categories();
categories.sort();
for (String cat : categories) {
if (filter_settings.category_filter != FilterSettings::CATEGORY_ALL && filter_settings.excluded_categories.has(cat)) {
continue;
}
List<String> tasks = LimboTaskDB::get_tasks_in_category(cat);
if (tasks.size() == 0) {
continue;
}
TaskPaletteSection *sec = memnew(TaskPaletteSection(cat));
for (String task_meta : tasks) {
Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta);
String tname;
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E;
if (task_meta.begins_with("res:")) {
if (filter_settings.type_filter == FilterSettings::TYPE_CORE) {
continue;
}
tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case();
E = dd->class_list.find(vformat("\"%s\"", task_meta.trim_prefix("res://")));
if (!E) {
E = dd->class_list.find(tname);
}
} else {
if (filter_settings.type_filter == FilterSettings::TYPE_USER) {
continue;
}
tname = task_meta.trim_prefix("BT");
E = dd->class_list.find(task_meta);
}
String descr;
if (E) {
if (E->value.description.is_empty() || E->value.description.length() > 1000) {
descr = DTR(E->value.brief_description);
} else {
descr = DTR(E->value.description);
}
}
sec->add_task_button(tname, icon, descr, task_meta);
}
sec->set_filter("");
sec->connect(SNAME("task_button_pressed"), callable_mp(this, &TaskPalette::_on_task_button_pressed));
sec->connect(SNAME("task_button_rmb"), callable_mp(this, &TaskPalette::_on_task_button_rmb));
sections->add_child(sec);
sec->set_collapsed(collapsed_sections.has(cat));
}
if (!filter_edit->get_text().is_empty()) {
_apply_filter(filter_edit->get_text());
}
}
void TaskPalette::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE: {
Ref<ConfigFile> cf;
cf.instantiate();
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
if (cf->load(conf_path) == OK) {
Variant value = cf->get_value("task_palette", "type_filter", FilterSettings::TypeFilter(0));
if (value.is_num()) {
filter_settings.type_filter = (FilterSettings::TypeFilter)(int)value;
}
value = cf->get_value("task_palette", "category_filter", FilterSettings::CategoryFilter(0));
if (value.is_num()) {
filter_settings.category_filter = (FilterSettings::CategoryFilter)(int)value;
}
value = cf->get_value("task_palette", "excluded_categories", Array());
if (value.is_array()) {
Array arr = value;
for (int i = 0; i < arr.size(); i++) {
if (arr[i].get_type() == Variant::STRING) {
filter_settings.excluded_categories.insert(arr[i]);
}
}
}
}
_update_filter_button();
} break;
case NOTIFICATION_EXIT_TREE: {
Ref<ConfigFile> cf;
cf.instantiate();
String conf_path = EditorPaths::get_singleton()->get_project_settings_dir().path_join("limbo_ai.cfg");
cf->load(conf_path);
Array collapsed_sections;
for (int i = 0; i < sections->get_child_count(); i++) {
TaskPaletteSection *sec = Object::cast_to<TaskPaletteSection>(sections->get_child(i));
if (sec->is_collapsed()) {
collapsed_sections.push_back(sec->get_category_name());
}
}
cf->set_value("task_palette", "collapsed_sections", collapsed_sections);
cf->set_value("task_palette", "type_filter", filter_settings.type_filter);
cf->set_value("task_palette", "category_filter", filter_settings.category_filter);
Array excluded_categories;
for (const String &cat : filter_settings.excluded_categories) {
excluded_categories.append(cat);
}
cf->set_value("task_palette", "excluded_categories", excluded_categories);
cf->save(conf_path);
} break;
case NOTIFICATION_THEME_CHANGED: {
tool_filters->set_icon(get_theme_icon(SNAME("AnimationFilter"), SNAME("EditorIcons")));
tool_refresh->set_icon(get_theme_icon(SNAME("Reload"), SNAME("EditorIcons")));
select_all->set_icon(get_theme_icon(SNAME("LimboSelectAll"), SNAME("EditorIcons")));
deselect_all->set_icon(get_theme_icon(SNAME("LimboDeselectAll"), SNAME("EditorIcons")));
category_choice_background = get_theme_stylebox(SNAME("normal"), SNAME("LineEdit"));
category_choice->queue_redraw();
if (is_visible_in_tree()) {
refresh();
}
} break;
}
}
void TaskPalette::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &TaskPalette::refresh);
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("favorite_tasks_changed"));
}
TaskPalette::TaskPalette() {
VBoxContainer *vb = memnew(VBoxContainer);
add_child(vb);
HBoxContainer *hb = memnew(HBoxContainer);
vb->add_child(hb);
tool_filters = memnew(Button);
tool_filters->set_tooltip_text(TTR("Show filters"));
tool_filters->set_flat(true);
tool_filters->set_toggle_mode(true);
tool_filters->set_focus_mode(FocusMode::FOCUS_NONE);
tool_filters->connect("pressed", callable_mp(this, &TaskPalette::_show_filter_popup));
hb->add_child(tool_filters);
filter_edit = memnew(LineEdit);
filter_edit->set_clear_button_enabled(true);
filter_edit->set_placeholder(TTR("Filter tasks"));
filter_edit->connect("text_changed", callable_mp(this, &TaskPalette::_apply_filter));
filter_edit->set_h_size_flags(SIZE_EXPAND_FILL);
hb->add_child(filter_edit);
tool_refresh = memnew(Button);
tool_refresh->set_tooltip_text(TTR("Refresh tasks"));
tool_refresh->set_flat(true);
tool_refresh->set_focus_mode(FocusMode::FOCUS_NONE);
tool_refresh->connect("pressed", callable_mp(this, &TaskPalette::refresh));
hb->add_child(tool_refresh);
ScrollContainer *sc = memnew(ScrollContainer);
sc->set_h_size_flags(SIZE_EXPAND_FILL);
sc->set_v_size_flags(SIZE_EXPAND_FILL);
vb->add_child(sc);
sections = memnew(VBoxContainer);
sections->set_h_size_flags(SIZE_EXPAND_FILL);
sections->set_v_size_flags(SIZE_EXPAND_FILL);
sc->add_child(sections);
menu = memnew(PopupMenu);
add_child(menu);
menu->connect("id_pressed", callable_mp(this, &TaskPalette::_menu_action_selected));
filter_popup = memnew(PopupPanel);
{
VBoxContainer *vbox = memnew(VBoxContainer);
filter_popup->add_child(vbox);
Label *type_header = memnew(Label);
type_header->set_text(TTR("Type"));
type_header->set_theme_type_variation("HeaderSmall");
vbox->add_child(type_header);
HBoxContainer *type_filter = memnew(HBoxContainer);
vbox->add_child(type_filter);
Ref<ButtonGroup> type_filter_group;
type_filter_group.instantiate();
type_all = memnew(Button);
type_all->set_text(TTR("All"));
type_all->set_tooltip_text(TTR("Show tasks of all types"));
type_all->set_toggle_mode(true);
type_all->set_focus_mode(FocusMode::FOCUS_NONE);
type_all->set_button_group(type_filter_group);
type_all->set_pressed(true);
type_all->connect("pressed", callable_mp(this, &TaskPalette::_type_filter_changed));
type_filter->add_child(type_all);
type_core = memnew(Button);
type_core->set_text(TTR("Core"));
type_core->set_tooltip_text(TTR("Show only core tasks"));
type_core->set_toggle_mode(true);
type_core->set_focus_mode(FocusMode::FOCUS_NONE);
type_core->set_button_group(type_filter_group);
type_core->connect("pressed", callable_mp(this, &TaskPalette::_type_filter_changed));
type_filter->add_child(type_core);
type_user = memnew(Button);
type_user->set_text(TTR("User"));
type_user->set_tooltip_text(TTR("Show only user-implemented tasks (aka scripts)"));
type_user->set_toggle_mode(true);
type_user->set_focus_mode(FocusMode::FOCUS_NONE);
type_user->set_button_group(type_filter_group);
type_user->connect("pressed", callable_mp(this, &TaskPalette::_type_filter_changed));
type_filter->add_child(type_user);
Control *space1 = memnew(Control);
space1->set_custom_minimum_size(Size2(0, 4));
vbox->add_child(space1);
Label *category_header = memnew(Label);
category_header->set_text(TTR("Categories"));
category_header->set_theme_type_variation("HeaderSmall");
vbox->add_child(category_header);
HBoxContainer *category_filter = memnew(HBoxContainer);
vbox->add_child(category_filter);
Ref<ButtonGroup> category_filter_group;
category_filter_group.instantiate();
category_all = memnew(Button);
category_all->set_text(TTR("All"));
category_all->set_tooltip_text(TTR("Show tasks of all categories"));
category_all->set_toggle_mode(true);
category_all->set_focus_mode(FocusMode::FOCUS_NONE);
category_all->set_pressed(true);
category_all->set_button_group(category_filter_group);
category_all->connect("pressed", callable_mp(this, &TaskPalette::_category_filter_changed));
category_filter->add_child(category_all);
category_include = memnew(Button);
category_include->set_text(TTR("Include"));
category_include->set_tooltip_text(TTR("Show tasks from selected categories"));
category_include->set_toggle_mode(true);
category_include->set_focus_mode(FocusMode::FOCUS_NONE);
category_include->set_button_group(category_filter_group);
category_include->connect("pressed", callable_mp(this, &TaskPalette::_category_filter_changed));
category_filter->add_child(category_include);
category_exclude = memnew(Button);
category_exclude->set_text(TTR("Exclude"));
category_exclude->set_tooltip_text(TTR("Don't show tasks from selected categories"));
category_exclude->set_toggle_mode(true);
category_exclude->set_focus_mode(FocusMode::FOCUS_NONE);
category_exclude->set_button_group(category_filter_group);
category_exclude->connect("pressed", callable_mp(this, &TaskPalette::_category_filter_changed));
category_filter->add_child(category_exclude);
category_choice = memnew(VBoxContainer);
category_choice->connect("draw", callable_mp(this, &TaskPalette::_draw_filter_popup_background));
vbox->add_child(category_choice);
HBoxContainer *selection_controls = memnew(HBoxContainer);
selection_controls->set_alignment(BoxContainer::ALIGNMENT_CENTER);
category_choice->add_child(selection_controls);
select_all = memnew(Button);
select_all->set_tooltip_text(TTR("Select all categories"));
select_all->set_focus_mode(FocusMode::FOCUS_NONE);
select_all->connect("pressed", callable_mp(this, &TaskPalette::_set_all_filter_categories).bind(true));
selection_controls->add_child(select_all);
deselect_all = memnew(Button);
deselect_all->set_tooltip_text(TTR("Deselect all categories"));
deselect_all->set_focus_mode(FocusMode::FOCUS_NONE);
deselect_all->connect("pressed", callable_mp(this, &TaskPalette::_set_all_filter_categories).bind(false));
selection_controls->add_child(deselect_all);
category_scroll = memnew(ScrollContainer);
category_choice->add_child(category_scroll);
category_list = memnew(VBoxContainer);
category_scroll->add_child(category_list);
}
add_child(filter_popup);
filter_popup->connect("popup_hide", callable_mp(this, &TaskPalette::_update_filter_button));
}
TaskPalette::~TaskPalette() {
}

142
editor/task_palette.h Normal file
View File

@ -0,0 +1,142 @@
/**
* task_palette.h
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#ifndef TASK_PALETTE_H
#define TASK_PALETTE_H
#include "scene/gui/panel_container.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/check_box.h"
#include "scene/gui/flow_container.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/popup.h"
class TaskButton : public Button {
GDCLASS(TaskButton, Button);
public:
virtual Control *make_custom_tooltip(const String &p_text) const override;
};
class TaskPaletteSection : public VBoxContainer {
GDCLASS(TaskPaletteSection, VBoxContainer);
private:
FlowContainer *tasks_container;
Button *section_header;
void _on_task_button_pressed(const String &p_task);
void _on_task_button_gui_input(const Ref<InputEvent> &p_event, const String &p_task);
void _on_header_pressed();
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_filter(String p_filter);
void add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta);
void set_collapsed(bool p_collapsed);
bool is_collapsed() const;
String get_category_name() const { return section_header->get_text(); }
TaskPaletteSection(String p_category_name);
~TaskPaletteSection();
};
class TaskPalette : public PanelContainer {
GDCLASS(TaskPalette, PanelContainer)
private:
enum MenuAction {
MENU_EDIT_SCRIPT,
MENU_OPEN_DOC,
MENU_FAVORITE,
};
struct FilterSettings {
enum TypeFilter {
TYPE_ALL,
TYPE_CORE,
TYPE_USER,
} type_filter;
enum CategoryFilter {
CATEGORY_ALL,
CATEGORY_INCLUDE,
CATEGORY_EXCLUDE,
} category_filter;
HashSet<String> excluded_categories;
} filter_settings;
LineEdit *filter_edit;
VBoxContainer *sections;
PopupMenu *menu;
Button *tool_filters;
Button *tool_refresh;
// Filter popup
PopupPanel *filter_popup;
Button *type_all;
Button *type_core;
Button *type_user;
Button *category_all;
Button *category_include;
Button *category_exclude;
VBoxContainer *category_choice;
Button *select_all;
Button *deselect_all;
ScrollContainer *category_scroll;
VBoxContainer *category_list;
Ref<StyleBox> category_choice_background;
String context_task;
void _menu_action_selected(int p_id);
void _on_task_button_pressed(const String &p_task);
void _on_task_button_rmb(const String &p_task);
void _apply_filter(const String &p_text);
void _update_filter_popup();
void _show_filter_popup();
void _type_filter_changed();
void _category_filter_changed();
void _set_all_filter_categories(bool p_selected);
void _category_item_toggled(bool p_pressed, const String &p_category);
void _filter_data_changed();
void _draw_filter_popup_background();
void _update_filter_button();
_FORCE_INLINE_ void _set_category_excluded(const String &p_category, bool p_excluded) {
if (p_excluded) {
filter_settings.excluded_categories.insert(p_category);
} else {
filter_settings.excluded_categories.erase(p_category);
}
}
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void refresh();
TaskPalette();
~TaskPalette();
};
#endif // TASK_PALETTE

293
editor/task_tree.cpp Normal file
View File

@ -0,0 +1,293 @@
/**
* task_tree.cpp
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#include "task_tree.h"
#include "modules/limboai/bt/tasks/bt_comment.h"
#include "modules/limboai/util/limbo_utility.h"
#include "editor/editor_scale.h"
//**** TaskTree
TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx) {
ERR_FAIL_COND_V(p_task.is_null(), nullptr);
TreeItem *item = tree->create_item(p_parent, p_idx);
item->set_metadata(0, p_task);
// p_task->connect("changed"...)
for (int i = 0; i < p_task->get_child_count(); i++) {
_create_tree(p_task->get_child(i), item);
}
_update_item(item);
return item;
}
void TaskTree::_update_item(TreeItem *p_item) {
if (p_item == nullptr) {
return;
}
Ref<BTTask> task = p_item->get_metadata(0);
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
p_item->set_text(0, task->get_task_name());
if (task->is_class_ptr(BTComment::get_class_ptr_static())) {
p_item->set_custom_font(0, (get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts"))));
p_item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor")));
} else if (task->get_custom_name().is_empty()) {
p_item->set_custom_font(0, nullptr);
p_item->clear_custom_color(0);
} else {
p_item->set_custom_font(0, (get_theme_font(SNAME("bold"), SNAME("EditorFonts"))));
// p_item->set_custom_color(0, get_theme_color(SNAME("warning_color"), SNAME("Editor")));
}
String type_arg;
if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) {
type_arg = task->get_script_instance()->get_script()->get_path();
} else {
type_arg = task->get_class();
}
p_item->set_icon(0, LimboUtility::get_singleton()->get_task_icon(type_arg));
p_item->set_icon_max_width(0, 16 * EDSCALE);
p_item->set_editable(0, false);
for (int i = 0; i < p_item->get_button_count(0); i++) {
p_item->erase_button(0, i);
}
PackedStringArray warnings = task->get_configuration_warnings();
String warning_text;
for (int j = 0; j < warnings.size(); j++) {
if (j > 0) {
warning_text += "\n";
}
warning_text += warnings[j];
}
if (!warning_text.is_empty()) {
p_item->add_button(0, get_theme_icon(SNAME("NodeWarning"), SNAME("EditorIcons")), 0, false, warning_text);
}
// TODO: Update probabilities.
}
void TaskTree::_update_tree() {
Ref<BTTask> sel;
if (tree->get_selected()) {
sel = tree->get_selected()->get_metadata(0);
}
tree->clear();
if (bt.is_null()) {
return;
}
if (bt->get_root_task().is_valid()) {
_create_tree(bt->get_root_task(), nullptr);
}
TreeItem *item = _find_item(sel);
if (item) {
item->select(0);
}
}
TreeItem *TaskTree::_find_item(const Ref<BTTask> &p_task) const {
if (p_task.is_null()) {
return nullptr;
}
TreeItem *item = tree->get_root();
List<TreeItem *> stack;
while (item && item->get_metadata(0) != p_task) {
if (item->get_child_count() > 0) {
stack.push_back(item->get_first_child());
}
item = item->get_next();
if (item == nullptr && !stack.is_empty()) {
item = stack.front()->get();
stack.pop_front();
}
}
return item;
}
void TaskTree::_on_item_mouse_selected(const Vector2 &p_pos, int p_button_index) {
if (p_button_index == 2) {
emit_signal(SNAME("rmb_pressed"), get_screen_position() + p_pos);
}
}
void TaskTree::_on_item_selected() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid()) {
update_task(last_selected);
if (last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
}
last_selected = get_selected();
last_selected->connect("changed", on_task_changed);
emit_signal(SNAME("task_selected"), last_selected);
}
void TaskTree::_on_item_double_clicked() {
emit_signal(SNAME("task_double_clicked"));
}
void TaskTree::_on_task_changed() {
_update_item(tree->get_selected());
}
void TaskTree::load_bt(const Ref<BehaviorTree> &p_behavior_tree) {
ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "Tried to load a null tree.");
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
bt = p_behavior_tree;
tree->clear();
if (bt->get_root_task().is_valid()) {
_create_tree(bt->get_root_task(), nullptr);
}
}
void TaskTree::unload() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
bt->unreference();
tree->clear();
}
void TaskTree::update_task(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
_update_item(item);
}
}
Ref<BTTask> TaskTree::get_selected() const {
if (tree->get_selected()) {
return tree->get_selected()->get_metadata(0);
}
return nullptr;
}
void TaskTree::deselect() {
TreeItem *sel = tree->get_selected();
if (sel) {
sel->deselect(0);
}
}
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
if (editable && tree->get_item_at_position(p_point)) {
Dictionary drag_data;
drag_data["type"] = "task";
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;
}
return Variant();
}
bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) const {
if (!editable) {
return false;
}
Dictionary d = p_data;
if (!d.has("type") || !d.has("task")) {
return false;
}
int section = tree->get_drop_section_at_position(p_point);
TreeItem *item = tree->get_item_at_position(p_point);
if (!item || section < -1 || (section == -1 && !item->get_parent())) {
return false;
}
if (String(d["type"]) == "task") {
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 false;
}
void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) {
Dictionary d = p_data;
TreeItem *item = tree->get_item_at_position(p_point);
if (item && d.has("task")) {
Ref<BTTask> task = d["task"];
emit_signal(SNAME("task_dragged"), task, item->get_metadata(0), tree->get_drop_section_at_position(p_point));
}
}
void TaskTree::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
_update_tree();
} break;
}
}
void TaskTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_bt", "p_behavior_tree"), &TaskTree::load_bt);
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", "p_task"), &TaskTree::update_task);
ClassDB::bind_method(D_METHOD("get_selected"), &TaskTree::get_selected);
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);
ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TaskTree::_drop_data_fw);
ADD_SIGNAL(MethodInfo("rmb_pressed"));
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("task_double_clicked"));
ADD_SIGNAL(MethodInfo("task_dragged",
PropertyInfo(Variant::OBJECT, "p_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::OBJECT, "p_to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::INT, "p_type")));
}
TaskTree::TaskTree() {
editable = true;
tree = memnew(Tree);
add_child(tree);
tree->set_columns(2);
tree->set_column_expand(0, true);
tree->set_column_expand(1, false);
tree->set_column_custom_minimum_width(1, 64);
tree->set_anchor(SIDE_RIGHT, ANCHOR_END);
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
tree->set_allow_rmb_select(true);
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected));
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_double_clicked));
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));
}
TaskTree::~TaskTree() {
Callable on_task_changed = callable_mp(this, &TaskTree::_on_task_changed);
if (last_selected.is_valid() && last_selected->is_connected("changed", on_task_changed)) {
last_selected->disconnect("changed", on_task_changed);
}
}

58
editor/task_tree.h Normal file
View File

@ -0,0 +1,58 @@
/**
* task_tree.h
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#include "modules/limboai/bt/behavior_tree.h"
#include "scene/gui/control.h"
#include "scene/gui/tree.h"
class TaskTree : public Control {
GDCLASS(TaskTree, Control);
private:
Tree *tree;
Ref<BehaviorTree> bt;
Ref<BTTask> last_selected;
bool editable;
TreeItem *_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent, int p_idx = -1);
void _update_item(TreeItem *p_item);
void _update_tree();
TreeItem *_find_item(const Ref<BTTask> &p_task) const;
void _on_item_selected();
void _on_item_double_clicked();
void _on_item_mouse_selected(const Vector2 &p_pos, int p_button_index);
void _on_task_changed();
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);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void load_bt(const Ref<BehaviorTree> &p_behavior_tree);
void unload();
Ref<BehaviorTree> get_bt() const { return bt; }
void update_tree() { _update_tree(); }
void update_task(const Ref<BTTask> &p_task);
Ref<BTTask> get_selected() const;
void deselect();
virtual bool editor_can_reload_from_file() { return false; }
TaskTree();
~TaskTree();
};

View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0"><path d="m14.67 0h-4.35c-.61 0-1.11.43-1.25 1h1.26 3.79.55c.18 0 .33.15.33.33v1.21 3.14 1.26c.57-.15 1-.64 1-1.26v-4.35c0-.74-.59-1.33-1.33-1.33z"/><path d="m1.48 15h-.15c-.18 0-.33-.15-.33-.33v-2.05-2.3-1.25c-.57.15-1 .64-1 1.26v4.35c0 .73.59 1.32 1.33 1.32h4.35c.62 0 1.11-.43 1.26-1h-1.27z"/><path d="m11.62 2.25h-1.62-1-4.62c-1.17 0-2.13.95-2.13 2.13v4.62 1 1.62c0 1.17.95 2.13 2.13 2.13h1.62 1 1.29 3.33c1.17 0 2.13-.95 2.13-2.13v-4.62-1-1.62c0-1.18-.95-2.13-2.13-2.13zm.63 9.37c0 .35-.28.63-.63.63h-7.24c-.35 0-.63-.28-.63-.63v-7.24c0-.35.28-.63.63-.63h7.25c.34 0 .63.28.63.63v7.24z"/></g></svg>

After

Width:  |  Height:  |  Size: 713 B

1
icons/LimboSelectAll.svg Normal file
View File

@ -0,0 +1 @@
<svg enable-background="new 0 0 16 16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><g fill="#e0e0e0"><path d="m10.23 4.83-3.03 4.7-1.44-2.12-.93.65 1.92 2.82c.11.15.28.25.47.25h.01c.19 0 .36-.1.47-.26l3.42-5.32.07-.1z"/><path d="m14.67 0h-4.35c-.61 0-1.11.43-1.25 1h1.26 3.79.55c.18 0 .33.15.33.33v1.21 3.14 1.26c.57-.15 1-.64 1-1.26v-4.35c0-.74-.59-1.33-1.33-1.33z"/><path d="m1.48 15h-.15c-.18 0-.33-.15-.33-.33v-2.05-2.3-1.25c-.57.15-1 .64-1 1.26v4.35c0 .73.59 1.32 1.33 1.32h4.35c.62 0 1.11-.43 1.26-1h-1.27z"/><path d="m11.62 2.25h-1.62-1-4.62c-1.17 0-2.13.95-2.13 2.13v4.62 1 1.62c0 1.17.95 2.13 2.13 2.13h1.62 1 1.29 3.33c1.17 0 2.13-.95 2.13-2.13v-4.62-1-1.62c0-1.18-.95-2.13-2.13-2.13zm.63 9.37c0 .35-.28.63-.63.63h-7.24c-.35 0-.63-.28-.63-.63v-7.24c0-.35.28-.63.63-.63h7.25c.34 0 .63.28.63.63v7.24z"/></g></svg>

After

Width:  |  Height:  |  Size: 832 B

View File

@ -47,18 +47,9 @@
#include "bt/behavior_tree.h"
#include "bt/bt_player.h"
#include "bt/bt_state.h"
#include "bt/tasks/actions/bt_await_animation.h"
#include "bt/tasks/actions/bt_call_method.h"
#include "bt/tasks/actions/bt_console_print.h"
#include "bt/tasks/actions/bt_fail.h"
#include "bt/tasks/actions/bt_pause_animation.h"
#include "bt/tasks/actions/bt_play_animation.h"
#include "bt/tasks/actions/bt_random_wait.h"
#include "bt/tasks/actions/bt_set_agent_property.h"
#include "bt/tasks/actions/bt_set_var.h"
#include "bt/tasks/actions/bt_stop_animation.h"
#include "bt/tasks/actions/bt_wait.h"
#include "bt/tasks/actions/bt_wait_ticks.h"
#include "bt/tasks/blackboard/bt_check_trigger.h"
#include "bt/tasks/blackboard/bt_check_var.h"
#include "bt/tasks/blackboard/bt_set_var.h"
#include "bt/tasks/bt_action.h"
#include "bt/tasks/bt_comment.h"
#include "bt/tasks/bt_composite.h"
@ -72,9 +63,6 @@
#include "bt/tasks/composites/bt_random_sequence.h"
#include "bt/tasks/composites/bt_selector.h"
#include "bt/tasks/composites/bt_sequence.h"
#include "bt/tasks/conditions/bt_check_agent_property.h"
#include "bt/tasks/conditions/bt_check_trigger.h"
#include "bt/tasks/conditions/bt_check_var.h"
#include "bt/tasks/decorators/bt_always_fail.h"
#include "bt/tasks/decorators/bt_always_succeed.h"
#include "bt/tasks/decorators/bt_cooldown.h"
@ -89,10 +77,23 @@
#include "bt/tasks/decorators/bt_run_limit.h"
#include "bt/tasks/decorators/bt_subtree.h"
#include "bt/tasks/decorators/bt_time_limit.h"
#include "bt/tasks/scene/bt_await_animation.h"
#include "bt/tasks/scene/bt_call_method.h"
#include "bt/tasks/scene/bt_check_agent_property.h"
#include "bt/tasks/scene/bt_pause_animation.h"
#include "bt/tasks/scene/bt_play_animation.h"
#include "bt/tasks/scene/bt_set_agent_property.h"
#include "bt/tasks/scene/bt_stop_animation.h"
#include "bt/tasks/utility/bt_console_print.h"
#include "bt/tasks/utility/bt_fail.h"
#include "bt/tasks/utility/bt_random_wait.h"
#include "bt/tasks/utility/bt_wait.h"
#include "bt/tasks/utility/bt_wait_ticks.h"
#include "editor/debugger/limbo_debugger.h"
#include "hsm/limbo_hsm.h"
#include "hsm/limbo_state.h"
#include "util/limbo_string_names.h"
#include "util/limbo_task_db.h"
#include "util/limbo_utility.h"
#ifdef TOOLS_ENABLED
@ -121,51 +122,51 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(BTPlayer);
GDREGISTER_CLASS(BTState);
GDREGISTER_CLASS(BTComment);
LIMBO_REGISTER_TASK(BTComment);
GDREGISTER_CLASS(BTComposite);
GDREGISTER_CLASS(BTSequence);
GDREGISTER_CLASS(BTSelector);
GDREGISTER_CLASS(BTParallel);
GDREGISTER_CLASS(BTDynamicSequence);
GDREGISTER_CLASS(BTDynamicSelector);
GDREGISTER_CLASS(BTRandomSequence);
GDREGISTER_CLASS(BTRandomSelector);
LIMBO_REGISTER_TASK(BTSequence);
LIMBO_REGISTER_TASK(BTSelector);
LIMBO_REGISTER_TASK(BTParallel);
LIMBO_REGISTER_TASK(BTDynamicSequence);
LIMBO_REGISTER_TASK(BTDynamicSelector);
LIMBO_REGISTER_TASK(BTRandomSequence);
LIMBO_REGISTER_TASK(BTRandomSelector);
GDREGISTER_CLASS(BTDecorator);
GDREGISTER_CLASS(BTInvert);
GDREGISTER_CLASS(BTAlwaysFail);
GDREGISTER_CLASS(BTAlwaysSucceed);
GDREGISTER_CLASS(BTDelay);
GDREGISTER_CLASS(BTRepeat);
GDREGISTER_CLASS(BTRepeatUntilFailure);
GDREGISTER_CLASS(BTRepeatUntilSuccess);
GDREGISTER_CLASS(BTRunLimit);
GDREGISTER_CLASS(BTTimeLimit);
GDREGISTER_CLASS(BTCooldown);
GDREGISTER_CLASS(BTProbability);
GDREGISTER_CLASS(BTForEach);
LIMBO_REGISTER_TASK(BTInvert);
LIMBO_REGISTER_TASK(BTAlwaysFail);
LIMBO_REGISTER_TASK(BTAlwaysSucceed);
LIMBO_REGISTER_TASK(BTDelay);
LIMBO_REGISTER_TASK(BTRepeat);
LIMBO_REGISTER_TASK(BTRepeatUntilFailure);
LIMBO_REGISTER_TASK(BTRepeatUntilSuccess);
LIMBO_REGISTER_TASK(BTRunLimit);
LIMBO_REGISTER_TASK(BTTimeLimit);
LIMBO_REGISTER_TASK(BTCooldown);
LIMBO_REGISTER_TASK(BTProbability);
LIMBO_REGISTER_TASK(BTForEach);
GDREGISTER_CLASS(BTAction);
GDREGISTER_CLASS(BTAwaitAnimation);
GDREGISTER_CLASS(BTCallMethod);
GDREGISTER_CLASS(BTConsolePrint);
GDREGISTER_CLASS(BTFail);
GDREGISTER_CLASS(BTNewScope);
GDREGISTER_CLASS(BTPauseAnimation);
GDREGISTER_CLASS(BTPlayAnimation);
GDREGISTER_CLASS(BTRandomWait);
GDREGISTER_CLASS(BTSetAgentProperty);
GDREGISTER_CLASS(BTSetVar);
GDREGISTER_CLASS(BTStopAnimation);
GDREGISTER_CLASS(BTSubtree);
GDREGISTER_CLASS(BTWait);
GDREGISTER_CLASS(BTWaitTicks);
LIMBO_REGISTER_TASK(BTAwaitAnimation);
LIMBO_REGISTER_TASK(BTCallMethod);
LIMBO_REGISTER_TASK(BTConsolePrint);
LIMBO_REGISTER_TASK(BTFail);
LIMBO_REGISTER_TASK(BTNewScope);
LIMBO_REGISTER_TASK(BTPauseAnimation);
LIMBO_REGISTER_TASK(BTPlayAnimation);
LIMBO_REGISTER_TASK(BTRandomWait);
LIMBO_REGISTER_TASK(BTSetAgentProperty);
LIMBO_REGISTER_TASK(BTSetVar);
LIMBO_REGISTER_TASK(BTStopAnimation);
LIMBO_REGISTER_TASK(BTSubtree);
LIMBO_REGISTER_TASK(BTWait);
LIMBO_REGISTER_TASK(BTWaitTicks);
GDREGISTER_CLASS(BTCondition);
GDREGISTER_CLASS(BTCheckAgentProperty);
GDREGISTER_CLASS(BTCheckTrigger);
GDREGISTER_CLASS(BTCheckVar);
LIMBO_REGISTER_TASK(BTCheckAgentProperty);
LIMBO_REGISTER_TASK(BTCheckTrigger);
LIMBO_REGISTER_TASK(BTCheckVar);
GDREGISTER_ABSTRACT_CLASS(BBParam);
GDREGISTER_CLASS(BBInt);

103
util/limbo_task_db.cpp Normal file
View File

@ -0,0 +1,103 @@
/**
* limbo_task_db.cpp
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#include "limbo_task_db.h"
#include "core/config/project_settings.h"
#include "core/io/dir_access.h"
HashMap<String, List<String>> LimboTaskDB::core_tasks;
HashMap<String, List<String>> LimboTaskDB::tasks_cache;
_FORCE_INLINE_ void _populate_scripted_tasks_from_dir(String p_path, List<String> *p_task_classes) {
if (p_path.is_empty()) {
return;
}
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
if (dir->change_dir(p_path) == OK) {
dir->list_dir_begin();
String fn = dir->get_next();
while (!fn.is_empty()) {
if (fn.ends_with(".gd")) {
String full_path = p_path.path_join(fn);
p_task_classes->push_back(full_path);
}
fn = dir->get_next();
}
dir->list_dir_end();
} else {
ERR_FAIL_MSG(vformat("Failed to list \"%s\" directory.", p_path));
}
}
_FORCE_INLINE_ void _populate_from_user_dir(String p_path, HashMap<String, List<String>> *p_categories) {
if (p_path.is_empty()) {
return;
}
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
if (dir->change_dir(p_path) == OK) {
dir->list_dir_begin();
String fn = dir->get_next();
while (!fn.is_empty()) {
if (dir->current_is_dir() && fn != "..") {
String full_path;
String category;
if (fn == ".") {
full_path = p_path;
category = LimboTaskDB::get_misc_category();
} else {
full_path = p_path.path_join(fn);
category = fn.capitalize();
}
if (!p_categories->has(category)) {
p_categories->insert(category, List<String>());
}
_populate_scripted_tasks_from_dir(full_path, &p_categories->get(category));
}
fn = dir->get_next();
}
dir->list_dir_end();
} else {
ERR_FAIL_MSG(vformat("Failed to list \"%s\" directory.", p_path));
}
}
void LimboTaskDB::scan_user_tasks() {
tasks_cache = HashMap<String, List<String>>(core_tasks);
if (!tasks_cache.has(LimboTaskDB::get_misc_category())) {
tasks_cache[LimboTaskDB::get_misc_category()] = List<String>();
}
for (int i = 1; i < 4; i++) {
String dir1 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_" + itos(i));
_populate_from_user_dir(dir1, &tasks_cache);
}
for (KeyValue<String, List<String>> &E : tasks_cache) {
E.value.sort_custom<ComparatorByTaskName>();
}
}
List<String> LimboTaskDB::get_categories() {
List<String> r_cat;
for (const KeyValue<String, List<String>> &E : tasks_cache) {
r_cat.push_back(E.key);
}
r_cat.sort();
return r_cat;
}
List<String> LimboTaskDB::get_tasks_in_category(const String &p_category) {
return List<String>(tasks_cache[p_category]);
}

70
util/limbo_task_db.h Normal file
View File

@ -0,0 +1,70 @@
/**
* limbo_task_db.h
* =============================================================================
* Copyright 2021-2023 Serhii Snitsaruk
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
* =============================================================================
*/
#ifndef LIMBO_TASK_DB_H
#define LIMBO_TASK_DB_H
#include "core/object/class_db.h"
#include "core/templates/hash_map.h"
#include "core/templates/list.h"
class LimboTaskDB {
private:
static HashMap<String, List<String>> core_tasks;
static HashMap<String, List<String>> tasks_cache;
struct ComparatorByTaskName {
bool operator()(const String &p_left, const String &p_right) const {
return get_task_name(p_left) < get_task_name(p_right);
}
};
public:
template <class T>
static void register_task() {
GDREGISTER_CLASS(T);
HashMap<String, List<String>>::Iterator E = core_tasks.find(T::get_task_category());
if (E) {
E->value.push_back(T::get_class_static());
} else {
List<String> tasks;
tasks.push_back(T::get_class_static());
core_tasks.insert(T::get_task_category(), tasks);
}
}
static void scan_user_tasks();
static _FORCE_INLINE_ String get_misc_category() { return "Misc"; }
static List<String> get_categories();
static List<String> get_tasks_in_category(const String &p_category);
static _FORCE_INLINE_ String get_task_name(String p_class_or_script_path) {
if (p_class_or_script_path.begins_with("res:")) {
return p_class_or_script_path.get_file().get_basename().trim_prefix("BT").to_pascal_case();
} else {
return p_class_or_script_path.trim_prefix("BT");
}
}
};
#define LIMBO_REGISTER_TASK(m_class) \
if (m_class::_class_is_enabled) { \
::LimboTaskDB::register_task<m_class>(); \
}
#define TASK_CATEGORY(m_cat) \
public: \
static _FORCE_INLINE_ String get_task_category() { \
return String(#m_cat); \
} \
\
private:
#endif // LIMBO_TASK_DB_H

View File

@ -18,6 +18,8 @@
#include "core/variant/variant.h"
#include "scene/resources/texture.h"
#define LOGICAL_XOR(a, b) (a) ? !(b) : (b)
class LimboUtility : public Object {
GDCLASS(LimboUtility, Object);