Merge branch 'qol-improvements'

This commit is contained in:
Serhii Snitsaruk 2023-08-25 16:15:00 +02:00
commit 26b9327090
20 changed files with 865 additions and 156 deletions

View File

@ -13,7 +13,7 @@
PackedStringArray BTAction::get_configuration_warnings() const { PackedStringArray BTAction::get_configuration_warnings() const {
PackedStringArray warnings = BTTask::get_configuration_warnings(); PackedStringArray warnings = BTTask::get_configuration_warnings();
if (get_child_count() != 0) { if (get_child_count_excluding_comments() != 0) {
warnings.append("Action can't have child tasks."); warnings.append("Action can't have child tasks.");
} }
return warnings; return warnings;

32
bt/tasks/bt_comment.cpp Normal file
View File

@ -0,0 +1,32 @@
/**
* bt_comment.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 "bt_comment.h"
#include "bt_task.h"
Ref<BTTask> BTComment::clone() const {
if (Engine::get_singleton()->is_editor_hint()) {
return BTTask::clone();
}
return nullptr;
}
PackedStringArray BTComment::get_configuration_warnings() const {
PackedStringArray warnings = BTTask::get_configuration_warnings();
if (get_child_count_excluding_comments() > 0) {
warnings.append("Can only have other comment tasks as children.");
}
if (get_parent() == nullptr) {
warnings.append("Can't be the root task.");
}
return warnings;
}

27
bt/tasks/bt_comment.h Normal file
View File

@ -0,0 +1,27 @@
/**
* bt_comment.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.
* =============================================================================
*/
/* bt_comment.h */
#ifndef BT_COMMENT_H
#define BT_COMMENT_H
#include "bt_task.h"
class BTComment : public BTTask {
GDCLASS(BTComment, BTTask);
private:
public:
virtual Ref<BTTask> clone() const override;
virtual PackedStringArray get_configuration_warnings() const override;
};
#endif // BT_COMMENT

View File

@ -13,7 +13,7 @@
PackedStringArray BTComposite::get_configuration_warnings() const { PackedStringArray BTComposite::get_configuration_warnings() const {
PackedStringArray warnings = BTTask::get_configuration_warnings(); PackedStringArray warnings = BTTask::get_configuration_warnings();
if (get_child_count() < 1) { if (get_child_count_excluding_comments() < 1) {
warnings.append("Composite should have at least one child task."); warnings.append("Composite should have at least one child task.");
} }
return warnings; return warnings;

View File

@ -13,7 +13,7 @@
PackedStringArray BTCondition::get_configuration_warnings() const { PackedStringArray BTCondition::get_configuration_warnings() const {
PackedStringArray warnings = BTTask::get_configuration_warnings(); PackedStringArray warnings = BTTask::get_configuration_warnings();
if (get_child_count() != 0) { if (get_child_count_excluding_comments() != 0) {
warnings.append("Condition task can't have child tasks."); warnings.append("Condition task can't have child tasks.");
} }
return warnings; return warnings;

View File

@ -13,7 +13,7 @@
PackedStringArray BTDecorator::get_configuration_warnings() const { PackedStringArray BTDecorator::get_configuration_warnings() const {
PackedStringArray warnings = BTTask::get_configuration_warnings(); PackedStringArray warnings = BTTask::get_configuration_warnings();
if (get_child_count() != 1) { if (get_child_count_excluding_comments() != 1) {
warnings.append("Decorator should have a single child task."); warnings.append("Decorator should have a single child task.");
} }
return warnings; return warnings;

View File

@ -11,6 +11,7 @@
#include "bt_task.h" #include "bt_task.h"
#include "bt_comment.h"
#include "modules/limboai/blackboard/blackboard.h" #include "modules/limboai/blackboard/blackboard.h"
#include "modules/limboai/util/limbo_string_names.h" #include "modules/limboai/util/limbo_string_names.h"
#include "modules/limboai/util/limbo_utility.h" #include "modules/limboai/util/limbo_utility.h"
@ -104,10 +105,19 @@ Ref<BTTask> BTTask::clone() const {
inst->data.parent = nullptr; inst->data.parent = nullptr;
inst->data.agent = nullptr; inst->data.agent = nullptr;
inst->data.blackboard.unref(); inst->data.blackboard.unref();
int num_null = 0;
for (int i = 0; i < data.children.size(); i++) { for (int i = 0; i < data.children.size(); i++) {
Ref<BTTask> c = get_child(i)->clone(); Ref<BTTask> c = get_child(i)->clone();
if (c.is_valid()) {
c->data.parent = inst.ptr(); c->data.parent = inst.ptr();
inst->data.children.set(i, c); inst->data.children.set(i - num_null, c);
} else {
num_null += 1;
}
}
if (num_null > 0) {
// * BTComment tasks return nullptr at runtime - we remove those.
inst->data.children.resize(data.children.size() - num_null);
} }
// Make BBParam properties unique. // Make BBParam properties unique.
@ -189,6 +199,16 @@ int BTTask::get_child_count() const {
return data.children.size(); return data.children.size();
} }
int BTTask::get_child_count_excluding_comments() const {
int count = 0;
for (int i = 0; i < data.children.size(); i++) {
if (!data.children[i]->is_class_ptr(BTComment::get_class_ptr_static())) {
count += 1;
}
}
return count;
}
void BTTask::add_child(Ref<BTTask> p_child) { void BTTask::add_child(Ref<BTTask> p_child) {
ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!"); ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!");
p_child->data.parent = this; p_child->data.parent = this;
@ -282,6 +302,7 @@ void BTTask::_bind_methods() {
ClassDB::bind_method(D_METHOD("execute", "p_delta"), &BTTask::execute); ClassDB::bind_method(D_METHOD("execute", "p_delta"), &BTTask::execute);
ClassDB::bind_method(D_METHOD("get_child", "p_idx"), &BTTask::get_child); ClassDB::bind_method(D_METHOD("get_child", "p_idx"), &BTTask::get_child);
ClassDB::bind_method(D_METHOD("get_child_count"), &BTTask::get_child_count); ClassDB::bind_method(D_METHOD("get_child_count"), &BTTask::get_child_count);
ClassDB::bind_method(D_METHOD("get_child_count_excluding_comments"), &BTTask::get_child_count_excluding_comments);
ClassDB::bind_method(D_METHOD("add_child", "p_child"), &BTTask::add_child); ClassDB::bind_method(D_METHOD("add_child", "p_child"), &BTTask::add_child);
ClassDB::bind_method(D_METHOD("add_child_at_index", "p_child", "p_idx"), &BTTask::add_child_at_index); ClassDB::bind_method(D_METHOD("add_child_at_index", "p_child", "p_idx"), &BTTask::add_child_at_index);
ClassDB::bind_method(D_METHOD("remove_child", "p_child"), &BTTask::remove_child); ClassDB::bind_method(D_METHOD("remove_child", "p_child"), &BTTask::remove_child);

View File

@ -93,6 +93,7 @@ public:
Ref<BTTask> get_child(int p_idx) const; Ref<BTTask> get_child(int p_idx) const;
int get_child_count() const; int get_child_count() const;
int get_child_count_excluding_comments() const;
void add_child(Ref<BTTask> p_child); void add_child(Ref<BTTask> p_child);
void add_child_at_index(Ref<BTTask> p_child, int p_idx); void add_child_at_index(Ref<BTTask> p_child, int p_idx);
void remove_child(Ref<BTTask> p_child); void remove_child(Ref<BTTask> p_child);

View File

@ -65,6 +65,7 @@ def get_doc_classes():
"BTCheckAgentProperty", "BTCheckAgentProperty",
"BTCheckTrigger", "BTCheckTrigger",
"BTCheckVar", "BTCheckVar",
"BTComment",
"BTComposite", "BTComposite",
"BTCondition", "BTCondition",
"BTConsolePrint", "BTConsolePrint",

View File

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<class name="BTParallel" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <class name="BTParallel" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description> <brief_description>
BT composite that executes tasks simultaneously. BT composite that executes child tasks until one of the criteria is met.
</brief_description> </brief_description>
<description> <description>
BT composite that executes tasks simultaneously until one of the criterea is met. BTParallel will execute each task from first to last at least once before returning a result. BTParallel executes its child tasks until one of the criterea is met. It will execute each task at least once, from the first to the last, before returning a result.
If set to [member repeat], the tasks will be executed again, even if they returned [code]SUCCESS[/code] or [code]FAILURE[/code] on the previous tick. If set to [member repeat], the child tasks will be re-executed, regardless of whether they previously resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code].
Returns [code]FAILURE[/code] when a required number of tasks return [code]FAILURE[/code]. When [member repeat] is set to [code]false[/code], if none of the criteria were met, and all child tasks returned [code]SUCCESS[/code] or [code]FAILURE[/code], [BTParallel] will return [code]FAILURE[/code]. Returns [code]FAILURE[/code] when the required number of tasks return [code]FAILURE[/code]. When [member repeat] is set to [code]false[/code], if none of the criteria were met, and all child tasks resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code], BTParallel will return [code]FAILURE[/code].
Returns [code]SUCCESS[/code] when a required number of tasks return [code]SUCCESS[/code]. Returns [code]SUCCESS[/code] when the required number of tasks return [code]SUCCESS[/code].
Returns [code]RUNNING[/code] after executing all tasks from first to last, and for as long as the above criterea are not met. Returns [code]RUNNING[/code] if none of the criterea were fulfilled, and either [member repeat] is set to [code]true[/code] or a child task resulted in [code]RUNNING[/code].
</description> </description>
<tutorials> <tutorials>
</tutorials> </tutorials>
<members> <members>
<member name="num_failures_required" type="int" setter="set_num_failures_required" getter="get_num_failures_required" default="1"> <member name="num_failures_required" type="int" setter="set_num_failures_required" getter="get_num_failures_required" default="1">
When the specified number of child tasks return [code]SUCCESS[/code], [BTParallel] will also return [code]SUCCESS[/code]. When the specified number of child tasks return [code]SUCCESS[/code], BTParallel will also return [code]SUCCESS[/code].
</member> </member>
<member name="num_successes_required" type="int" setter="set_num_successes_required" getter="get_num_successes_required" default="1"> <member name="num_successes_required" type="int" setter="set_num_successes_required" getter="get_num_successes_required" default="1">
When the specified number of child tasks return [code]FAILURE[/code], [BTParallel] will also return [code]FAILURE[/code]. When the specified number of child tasks return [code]FAILURE[/code], BTParallel will also return [code]FAILURE[/code].
</member> </member>
<member name="repeat" type="bool" setter="set_repeat" getter="get_repeat" default="false"> <member name="repeat" type="bool" setter="set_repeat" getter="get_repeat" default="false">
When [code]true[/code], the tasks will be executed again, even if they returned [code]SUCCESS[/code] or [code]FAILURE[/code] on the previous tick. When [code]true[/code], the child tasks will be executed again, regardless of whether they previously resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code].
When [code]false[/code], if none of the criteria were met, and all child tasks returned [code]SUCCESS[/code] or [code]FAILURE[/code], [BTParallel] will return [code]FAILURE[/code]. When [code]false[/code], if none of the criteria were met, and all child tasks resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code], BTParallel will return [code]FAILURE[/code].
</member> </member>
</members> </members>
</class> </class>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<class name="BTSelector" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <class name="BTSelector" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description> <brief_description>
BT composite that executes tasks in turn until first [code]SUCCESS[/code]. BT composite that sequentially executes tasks until first [code]SUCCESS[/code].
</brief_description> </brief_description>
<description> <description>
BT composite that executes child tasks from first to last until any child returns [code]SUCCESS[/code]. BTSelector executes its child tasks sequentially, from first to last, until any child returns [code]SUCCESS[/code].
Returns [code]RUNNING[/code] if a task returns [code]RUNNING[/code], and remembers last [code]RUNNING[/code] child. BTSelector will continue where it left off on the next execution tick. Returns [code]RUNNING[/code] if any child task returns [code]RUNNING[/code]. The composite will also remember the last child task that returned [code]RUNNING[/code], ensuring it resumes from that point in the next execution tick.
Returns [code]FAILURE[/code] if all tasks return [code]FAILURE[/code]. Returns [code]FAILURE[/code] if all child tasks return [code]FAILURE[/code].
Returns [code]SUCCESS[/code] if any task returns [code]SUCCESS[/code]. Returns [code]SUCCESS[/code] if a child task returns [code]SUCCESS[/code].
</description> </description>
<tutorials> <tutorials>
</tutorials> </tutorials>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<class name="BTSequence" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd"> <class name="BTSequence" inherits="BTComposite" version="4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description> <brief_description>
BT composite that executes tasks in turn for as long as they return [code]SUCCESS[/code]. BT composite that sequentially executes tasks as long as they return [code]SUCCESS[/code].
</brief_description> </brief_description>
<description> <description>
BT composite that executes child tasks from first to last for as long as they return [code]SUCCESS[/code]. BTSequence executes its child tasks sequentially, from first to last, as long as they return [code]SUCCESS[/code].
Returns [code]RUNNING[/code] if a task returns [code]RUNNING[/code], and remembers last [code]RUNNING[/code] child. BTSequence will continue where it left off on the next execution tick. Returns [code]RUNNING[/code] if any child task returns [code]RUNNING[/code]. The composite will also remember the last child task that returned [code]RUNNING[/code], ensuring it resumes from that point in the next execution tick.
Returns [code]FAILURE[/code] if any task returns [code]FAILURE[/code]. Returns [code]FAILURE[/code] if a child task returns [code]FAILURE[/code].
Returns [code]SUCCESS[/code] if all tasks return [code]SUCCESS[/code]. Returns [code]SUCCESS[/code] if all child tasks return [code]SUCCESS[/code].
</description> </description>
<tutorials> <tutorials>
</tutorials> </tutorials>

79
editor/action_banner.cpp Normal file
View File

@ -0,0 +1,79 @@
/**
* action_banner.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 "action_banner.h"
#include "scene/gui/button.h"
void ActionBanner::set_text(const String &p_text) {
message->set_text(p_text);
}
String ActionBanner::get_text() const {
return message->get_text();
}
void ActionBanner::close() {
queue_free();
}
void ActionBanner::add_action(const String &p_name, const Callable &p_action, bool p_auto_close) {
Button *action_btn = memnew(Button);
action_btn->set_text(p_name);
action_btn->connect(SNAME("pressed"), callable_mp(this, &ActionBanner::_execute_action).bind(p_action, p_auto_close));
hbox->add_child(action_btn);
}
void ActionBanner::_execute_action(const Callable &p_action, bool p_auto_close) {
Callable::CallError ce;
Variant ret;
p_action.callp(nullptr, 0, ret, ce);
if (p_auto_close) {
queue_free();
}
}
void ActionBanner::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_THEME_CHANGED: {
icon->set_texture(get_theme_icon(SNAME("StatusWarning"), SNAME("EditorIcons")));
} break;
}
}
void ActionBanner::_bind_methods() {
}
ActionBanner::ActionBanner() {
add_theme_constant_override("margin_bottom", 4);
add_theme_constant_override("margin_top", 4);
add_theme_constant_override("margin_left", 10);
add_theme_constant_override("margin_right", 10);
hbox = memnew(HBoxContainer);
hbox->add_theme_constant_override("hseparation", 8);
add_child(hbox);
icon = memnew(TextureRect);
icon->set_expand_mode(TextureRect::ExpandMode::EXPAND_KEEP_SIZE);
icon->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED);
hbox->add_child(icon);
message = memnew(Label);
message->set_text(vformat(TTR("User task folder doesn't exist")));
message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER);
hbox->add_child(message);
Control *spacer = memnew(Control);
spacer->set_custom_minimum_size(Size2(0, 16));
hbox->add_child(spacer);
}

48
editor/action_banner.h Normal file
View File

@ -0,0 +1,48 @@
/**
* action_banner.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.
* =============================================================================
*/
/* action_banner.h */
#ifndef ACTION_BANNER_H
#define ACTION_BANNER_H
#include "scene/gui/margin_container.h"
#include "scene/gui/box_container.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
class ActionBanner : public MarginContainer {
GDCLASS(ActionBanner, MarginContainer);
private:
TextureRect *icon;
Label *message;
HBoxContainer *hbox;
void _execute_action(const Callable &p_action, bool p_auto_close);
protected:
static void _bind_methods();
void _notification(int p_what);
public:
void set_text(const String &p_text);
String get_text() const;
void add_action(const String &p_name, const Callable &p_action, bool p_auto_close = false);
void close();
ActionBanner();
};
#endif // ACTION_BANNER

View File

@ -32,9 +32,10 @@
#include "scene/gui/label.h" #include "scene/gui/label.h"
#include "scene/gui/line_edit.h" #include "scene/gui/line_edit.h"
#include "scene/gui/split_container.h" #include "scene/gui/split_container.h"
#include "scene/gui/tab_container.h"
#include "scene/gui/texture_rect.h" #include "scene/gui/texture_rect.h"
/////////////////////// LimboDebuggerTab //**** LimboDebuggerTab
void LimboDebuggerTab::start_session() { void LimboDebuggerTab::start_session() {
bt_player_list->clear(); bt_player_list->clear();
@ -209,7 +210,9 @@ LimboDebuggerTab::LimboDebuggerTab(Ref<EditorDebuggerSession> p_session, WindowW
stop_session(); stop_session();
} }
//////////////////////// LimboDebuggerPlugin //**** LimboDebuggerPlugin
LimboDebuggerPlugin *LimboDebuggerPlugin::singleton = nullptr;
void LimboDebuggerPlugin::_window_visibility_changed(bool p_visible) { void LimboDebuggerPlugin::_window_visibility_changed(bool p_visible) {
} }
@ -259,8 +262,23 @@ bool LimboDebuggerPlugin::has_capture(const String &p_capture) const {
return p_capture == "limboai"; return p_capture == "limboai";
} }
WindowWrapper *LimboDebuggerPlugin::get_session_tab() const {
return window_wrapper;
}
int LimboDebuggerPlugin::get_session_tab_index() const {
TabContainer *c = Object::cast_to<TabContainer>(window_wrapper->get_parent());
ERR_FAIL_COND_V(c == nullptr, -1);
return c->get_tab_idx_from_control(window_wrapper);
}
LimboDebuggerPlugin::LimboDebuggerPlugin() { LimboDebuggerPlugin::LimboDebuggerPlugin() {
tab = nullptr; tab = nullptr;
singleton = this;
}
LimboDebuggerPlugin::~LimboDebuggerPlugin() {
singleton = nullptr;
} }
#endif // TOOLS_ENABLED #endif // TOOLS_ENABLED

View File

@ -70,17 +70,25 @@ class LimboDebuggerPlugin : public EditorDebuggerPlugin {
GDCLASS(LimboDebuggerPlugin, EditorDebuggerPlugin); GDCLASS(LimboDebuggerPlugin, EditorDebuggerPlugin);
private: private:
static LimboDebuggerPlugin *singleton;
LimboDebuggerTab *tab = nullptr; LimboDebuggerTab *tab = nullptr;
WindowWrapper *window_wrapper = nullptr; WindowWrapper *window_wrapper = nullptr;
void _window_visibility_changed(bool p_visible); void _window_visibility_changed(bool p_visible);
public: public:
static _FORCE_INLINE_ LimboDebuggerPlugin *get_singleton() { return singleton; }
void setup_session(int p_idx) override; void setup_session(int p_idx) override;
bool has_capture(const String &p_capture) const override; bool has_capture(const String &p_capture) const override;
bool capture(const String &p_message, const Array &p_data, int p_session) override; bool capture(const String &p_message, const Array &p_data, int p_session) override;
WindowWrapper *get_session_tab() const;
int get_session_tab_index() const;
LimboDebuggerPlugin(); LimboDebuggerPlugin();
~LimboDebuggerPlugin();
}; };
#endif // LIMBO_DEBUGGER_PLUGIN #endif // LIMBO_DEBUGGER_PLUGIN

View File

@ -13,8 +13,10 @@
#include "limbo_ai_editor_plugin.h" #include "limbo_ai_editor_plugin.h"
#include "action_banner.h"
#include "modules/limboai/bt/behavior_tree.h" #include "modules/limboai/bt/behavior_tree.h"
#include "modules/limboai/bt/tasks/bt_action.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/bt_task.h"
#include "modules/limboai/bt/tasks/composites/bt_parallel.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_selector.h"
@ -49,7 +51,10 @@
#include "core/variant/callable.h" #include "core/variant/callable.h"
#include "core/variant/dictionary.h" #include "core/variant/dictionary.h"
#include "core/variant/variant.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_file_system.h"
#include "editor/editor_help.h"
#include "editor/editor_inspector.h" #include "editor/editor_inspector.h"
#include "editor/editor_node.h" #include "editor/editor_node.h"
#include "editor/editor_paths.h" #include "editor/editor_paths.h"
@ -59,6 +64,7 @@
#include "editor/editor_undo_redo_manager.h" #include "editor/editor_undo_redo_manager.h"
#include "editor/inspector_dock.h" #include "editor/inspector_dock.h"
#include "editor/plugins/script_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h"
#include "editor/project_settings_editor.h"
#include "scene/gui/box_container.h" #include "scene/gui/box_container.h"
#include "scene/gui/button.h" #include "scene/gui/button.h"
#include "scene/gui/control.h" #include "scene/gui/control.h"
@ -94,6 +100,16 @@ void TaskTree::_update_item(TreeItem *p_item) {
Ref<BTTask> task = p_item->get_metadata(0); Ref<BTTask> task = p_item->get_metadata(0);
ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata."); ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata.");
p_item->set_text(0, task->get_task_name()); 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; String type_arg;
if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) { 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(); type_arg = task->get_script_instance()->get_script()->get_path();
@ -341,12 +357,43 @@ TaskTree::~TaskTree() {
//**** TaskTree ^ //**** 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 //**** TaskSection
void TaskSection::_on_task_button_pressed(const StringName &p_task) { void TaskSection::_on_task_button_pressed(const String &p_task) {
emit_signal(SNAME("task_button_pressed"), 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() { void TaskSection::_on_header_pressed() {
set_collapsed(!is_collapsed()); set_collapsed(!is_collapsed());
} }
@ -368,12 +415,14 @@ void TaskSection::set_filter(String p_filter_text) {
} }
} }
void TaskSection::add_task_button(String p_name, const Ref<Texture> &icon, Variant p_meta) { void TaskSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta) {
Button *btn = memnew(Button); TaskButton *btn = memnew(TaskButton);
btn->set_text(p_name); btn->set_text(p_name);
btn->set_icon(icon); 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->add_theme_constant_override(SNAME("icon_max_width"), 16 * EDSCALE); // Force user icons to be of the proper size.
btn->connect("pressed", callable_mp(this, &TaskSection::_on_task_button_pressed).bind(p_meta)); 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); tasks_container->add_child(btn);
} }
@ -395,6 +444,7 @@ void TaskSection::_notification(int p_what) {
void TaskSection::_bind_methods() { void TaskSection::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_button_pressed")); ADD_SIGNAL(MethodInfo("task_button_pressed"));
ADD_SIGNAL(MethodInfo("task_button_rmb"));
} }
TaskSection::TaskSection(String p_category_name) { TaskSection::TaskSection(String p_category_name) {
@ -415,11 +465,68 @@ TaskSection::~TaskSection() {
//**** TaskPanel //**** TaskPanel
void TaskPanel::_on_task_button_pressed(const StringName &p_task) { 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); emit_signal(SNAME("task_selected"), p_task);
} }
void TaskPanel::_on_filter_text_changed(String p_text) { 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++) { for (int i = 0; i < sections->get_child_count(); i++) {
TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i)); TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i));
ERR_FAIL_COND(sec == nullptr); ERR_FAIL_COND(sec == nullptr);
@ -470,7 +577,7 @@ void TaskPanel::refresh() {
categorized_tasks["Conditions"] = List<String>(); categorized_tasks["Conditions"] = List<String>();
_populate_core_tasks_from_class("BTCondition", &categorized_tasks["Conditions"]); _populate_core_tasks_from_class("BTCondition", &categorized_tasks["Conditions"]);
categorized_tasks["User"] = List<String>(); categorized_tasks["Uncategorized"] = List<String>();
String dir1 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1"); String dir1 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1");
_populate_from_user_dir(dir1, &categorized_tasks); _populate_from_user_dir(dir1, &categorized_tasks);
@ -497,19 +604,42 @@ void TaskPanel::refresh() {
TaskSection *sec = memnew(TaskSection(cat)); TaskSection *sec = memnew(TaskSection(cat));
for (String task_meta : tasks) { for (String task_meta : tasks) {
Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta); Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta);
String tname; String tname;
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E;
if (task_meta.begins_with("res:")) { if (task_meta.begins_with("res:")) {
tname = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case(); 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 { } else {
tname = task_meta.trim_prefix("BT"); tname = task_meta.trim_prefix("BT");
E = dd->class_list.find(task_meta);
} }
sec->add_task_button(tname, icon, 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->set_filter("");
sec->connect("task_button_pressed", callable_mp(this, &TaskPanel::_on_task_button_pressed)); 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); sections->add_child(sec);
sec->set_collapsed(collapsed_sections.has(cat)); 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) { void TaskPanel::_populate_core_tasks_from_class(const StringName &p_base_class, List<String> *p_task_classes) {
@ -535,7 +665,7 @@ void TaskPanel::_populate_from_user_dir(String p_path, HashMap<String, List<Stri
String category; String category;
if (fn == ".") { if (fn == ".") {
full_path = p_path; full_path = p_path;
category = "User"; category = "Uncategorized";
} else { } else {
full_path = p_path.path_join(fn); full_path = p_path.path_join(fn);
category = fn.capitalize(); category = fn.capitalize();
@ -596,6 +726,7 @@ void TaskPanel::_notification(int p_what) {
conf.save(conf_path); conf.save(conf_path);
} break; } break;
case NOTIFICATION_THEME_CHANGED: { case NOTIFICATION_THEME_CHANGED: {
refresh_btn->set_icon(get_theme_icon(SNAME("Reload"), SNAME("EditorIcons")));
if (is_visible_in_tree()) { if (is_visible_in_tree()) {
refresh(); refresh();
} }
@ -607,17 +738,29 @@ void TaskPanel::_bind_methods() {
ClassDB::bind_method(D_METHOD("refresh"), &TaskPanel::refresh); ClassDB::bind_method(D_METHOD("refresh"), &TaskPanel::refresh);
ADD_SIGNAL(MethodInfo("task_selected")); ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("favorite_tasks_changed"));
} }
TaskPanel::TaskPanel() { TaskPanel::TaskPanel() {
VBoxContainer *vb = memnew(VBoxContainer); VBoxContainer *vb = memnew(VBoxContainer);
add_child(vb); add_child(vb);
HBoxContainer *hb = memnew(HBoxContainer);
vb->add_child(hb);
filter_edit = memnew(LineEdit); filter_edit = memnew(LineEdit);
filter_edit->set_clear_button_enabled(true); filter_edit->set_clear_button_enabled(true);
filter_edit->set_placeholder(TTR("Filter tasks")); filter_edit->set_placeholder(TTR("Filter tasks"));
filter_edit->connect("text_changed", callable_mp(this, &TaskPanel::_on_filter_text_changed)); filter_edit->connect("text_changed", callable_mp(this, &TaskPanel::_apply_filter));
vb->add_child(filter_edit); 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); ScrollContainer *sc = memnew(ScrollContainer);
sc->set_h_size_flags(SIZE_EXPAND_FILL); sc->set_h_size_flags(SIZE_EXPAND_FILL);
@ -628,6 +771,10 @@ TaskPanel::TaskPanel() {
sections->set_h_size_flags(SIZE_EXPAND_FILL); sections->set_h_size_flags(SIZE_EXPAND_FILL);
sections->set_v_size_flags(SIZE_EXPAND_FILL); sections->set_v_size_flags(SIZE_EXPAND_FILL);
sc->add_child(sections); sc->add_child(sections);
menu = memnew(PopupMenu);
add_child(menu);
menu->connect("id_pressed", callable_mp(this, &TaskPanel::_menu_action_selected));
} }
TaskPanel::~TaskPanel() { TaskPanel::~TaskPanel() {
@ -637,6 +784,11 @@ TaskPanel::~TaskPanel() {
//**** LimboAIEditor //**** LimboAIEditor
_FORCE_INLINE_ String _get_script_template_path() {
String templates_search_path = GLOBAL_GET("editor/script/templates_search_path");
return templates_search_path.path_join("BTTask").path_join("custom_task.gd");
}
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) { void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null()); ERR_FAIL_COND(p_task.is_null());
ERR_FAIL_COND(task_tree->get_bt().is_null()); ERR_FAIL_COND(task_tree->get_bt().is_null());
@ -659,6 +811,33 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
_mark_as_dirty(true); _mark_as_dirty(true);
} }
void LimboAIEditor::_add_task_by_class_or_path(String p_class_or_path) {
Ref<BTTask> task;
if (p_class_or_path.begins_with("res:")) {
Ref<Script> s = ResourceLoader::load(p_class_or_path, "Script");
ERR_FAIL_COND_MSG(s.is_null() || !s->is_valid(), vformat("LimboAI: Failed to instantiate task. Bad script: %s", p_class_or_path));
Variant inst = ClassDB::instantiate(s->get_instance_base_type());
ERR_FAIL_COND_MSG(inst.is_zero(), vformat("LimboAI: Failed to instantiate base type \"%s\".", s->get_instance_base_type()));
if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
if (!inst.is_ref_counted()) {
memdelete((Object *)inst);
}
ERR_PRINT(vformat("LimboAI: Failed to instantiate task. Script is not a BTTask: %s", p_class_or_path));
return;
}
if (inst && s.is_valid()) {
((Object *)inst)->set_script(s);
task = inst;
}
} else {
task = ClassDB::instantiate(p_class_or_path);
}
_add_task(task);
}
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) { void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null()); ERR_FAIL_COND(p_task.is_null());
ERR_FAIL_COND(task_tree->get_bt().is_null()); ERR_FAIL_COND(task_tree->get_bt().is_null());
@ -764,40 +943,137 @@ void LimboAIEditor::_mark_as_dirty(bool p_dirty) {
} }
} }
void LimboAIEditor::_create_user_task_dir() {
String task_dir = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1");
ERR_FAIL_COND_MSG(DirAccess::exists(task_dir), "LimboAIEditor: Directory already exists: " + task_dir);
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
Error err;
err = dir->make_dir_recursive(task_dir);
ERR_FAIL_COND_MSG(err != OK, "LimboAIEditor: Failed to create directory: " + task_dir);
EditorFileSystem::get_singleton()->scan_changes();
_update_banners();
}
void LimboAIEditor::_edit_project_settings() {
ProjectSettingsEditor::get_singleton()->set_general_page("limbo_ai/behavior_tree");
ProjectSettingsEditor::get_singleton()->popup_project_settings();
ProjectSettingsEditor::get_singleton()->connect(SNAME("visibility_changed"), callable_mp(this, &LimboAIEditor::_update_banners), CONNECT_ONE_SHOT);
}
void LimboAIEditor::_remove_task_from_favorite(const String &p_task) {
PackedStringArray favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
favorite_tasks.erase(p_task);
ProjectSettings::get_singleton()->set_setting("limbo_ai/behavior_tree/favorite_tasks", favorite_tasks);
ProjectSettings::get_singleton()->save();
}
void LimboAIEditor::shortcut_input(const Ref<InputEvent> &p_event) {
if (!p_event->is_pressed()) {
return;
}
// * Global shortcuts.
if (ED_IS_SHORTCUT("limbo_ai/open_debugger", p_event)) {
_misc_option_selected(MISC_OPEN_DEBUGGER);
accept_event();
}
// * Local shortcuts.
if (!(has_focus() || get_viewport()->gui_get_focus_owner() == nullptr || is_ancestor_of(get_viewport()->gui_get_focus_owner()))) {
return;
}
if (ED_IS_SHORTCUT("limbo_ai/rename_task", p_event)) {
_action_selected(ACTION_RENAME);
} else if (ED_IS_SHORTCUT("limbo_ai/move_task_up", p_event)) {
_action_selected(ACTION_MOVE_UP);
} else if (ED_IS_SHORTCUT("limbo_ai/move_task_down", p_event)) {
_action_selected(ACTION_MOVE_DOWN);
} else if (ED_IS_SHORTCUT("limbo_ai/duplicate_task", p_event)) {
_action_selected(ACTION_DUPLICATE);
} else if (ED_IS_SHORTCUT("limbo_ai/remove_task", p_event)) {
_action_selected(ACTION_REMOVE);
} else if (ED_IS_SHORTCUT("limbo_ai/new_behavior_tree", p_event)) {
_new_bt();
} else if (ED_IS_SHORTCUT("limbo_ai/save_behavior_tree", p_event)) {
_on_save_pressed();
} else if (ED_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) {
load_dialog->popup_file_dialog();
} else {
return;
}
accept_event();
}
void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) { void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
menu->clear(); menu->clear();
menu->add_icon_item(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")), TTR("Remove"), ACTION_REMOVE);
Ref<BTTask> task = task_tree->get_selected();
ERR_FAIL_COND_MSG(task.is_null(), "LimboAIEditor: get_selected() returned null");
menu->add_icon_shortcut(get_theme_icon(SNAME("Rename"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/rename_task"), ACTION_RENAME);
menu->add_icon_item(get_theme_icon(SNAME("Script"), SNAME("EditorIcons")), TTR("Edit Script"), ACTION_EDIT_SCRIPT);
menu->add_icon_item(get_theme_icon(SNAME("Help"), SNAME("EditorIcons")), TTR("Open Documentation"), ACTION_OPEN_DOC);
menu->set_item_disabled(ACTION_EDIT_SCRIPT, task->get_script().is_null());
menu->add_separator(); menu->add_separator();
menu->add_icon_item(get_theme_icon(SNAME("MoveUp"), SNAME("EditorIcons")), TTR("Move Up"), ACTION_MOVE_UP); menu->add_icon_shortcut(get_theme_icon(SNAME("MoveUp"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP);
menu->add_icon_item(get_theme_icon(SNAME("MoveDown"), SNAME("EditorIcons")), TTR("Move Down"), ACTION_MOVE_DOWN); menu->add_icon_shortcut(get_theme_icon(SNAME("MoveDown"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/move_task_down"), ACTION_MOVE_DOWN);
menu->add_icon_item(get_theme_icon(SNAME("Duplicate"), SNAME("EditorIcons")), TTR("Duplicate"), ACTION_DUPLICATE); menu->add_icon_shortcut(get_theme_icon(SNAME("Duplicate"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/duplicate_task"), ACTION_DUPLICATE);
menu->add_icon_item(get_theme_icon(SNAME("NewRoot"), SNAME("EditorIcons")), TTR("Make Root"), ACTION_MAKE_ROOT); menu->add_icon_item(get_theme_icon(SNAME("NewRoot"), SNAME("EditorIcons")), TTR("Make Root"), ACTION_MAKE_ROOT);
menu->add_separator();
menu->add_icon_shortcut(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/remove_task"), ACTION_REMOVE);
menu->reset_size(); menu->reset_size();
menu->set_position(p_menu_pos); menu->set_position(p_menu_pos);
menu->popup(); menu->popup();
} }
void LimboAIEditor::_on_action_selected(int p_id) { void LimboAIEditor::_action_selected(int p_id) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
switch (p_id) { switch (p_id) {
case ACTION_REMOVE: { case ACTION_RENAME: {
Ref<BTTask> sel = task_tree->get_selected(); if (!task_tree->get_selected().is_valid()) {
if (sel.is_valid()) { return;
undo_redo->create_action(TTR("Remove BT Task")); }
if (sel->get_parent().is_null()) { Ref<BTTask> task = task_tree->get_selected();
undo_redo->add_do_method(task_tree->get_bt().ptr(), SNAME("set_root_task"), Variant()); if (task->is_class_ptr(BTComment::get_class_ptr_static())) {
undo_redo->add_undo_method(task_tree->get_bt().ptr(), SNAME("set_root_task"), task_tree->get_bt()->get_root_task()); rename_dialog->set_title(TTR("Edit Comment"));
rename_dialog->get_ok_button()->set_text(TTR("OK"));
rename_edit->set_placeholder(TTR("Comment"));
} else { } else {
undo_redo->add_do_method(sel->get_parent().ptr(), SNAME("remove_child"), sel); rename_dialog->set_title(TTR("Rename Task"));
undo_redo->add_undo_method(sel->get_parent().ptr(), SNAME("add_child_at_index"), sel, sel->get_parent()->get_child_index(sel)); rename_dialog->get_ok_button()->set_text(TTR("Rename"));
rename_edit->set_placeholder(TTR("Custom Name"));
} }
undo_redo->add_do_method(task_tree, SNAME("update_tree")); rename_edit->set_text(task->get_custom_name());
undo_redo->add_undo_method(task_tree, SNAME("update_tree")); rename_dialog->popup_centered();
undo_redo->commit_action(); rename_edit->select_all();
EditorNode::get_singleton()->edit_resource(task_tree->get_selected()); rename_edit->grab_focus();
_mark_as_dirty(true); } break;
case ACTION_EDIT_SCRIPT: {
ERR_FAIL_COND(task_tree->get_selected().is_null());
EditorNode::get_singleton()->edit_resource(task_tree->get_selected()->get_script());
} break;
case ACTION_OPEN_DOC: {
Ref<BTTask> task = task_tree->get_selected();
ERR_FAIL_COND(task.is_null());
String help_class;
if (!task->get_script().is_null()) {
Ref<Script> s = task->get_script();
help_class = s->get_language()->get_global_class_name(s->get_path());
} }
if (help_class.is_empty()) {
help_class = task->get_class();
}
ScriptEditor::get_singleton()->goto_help("class_name:" + help_class);
EditorNode::get_singleton()->set_visible_editor(EditorNode::EDITOR_SCRIPT);
} break; } break;
case ACTION_MOVE_UP: { case ACTION_MOVE_UP: {
Ref<BTTask> sel = task_tree->get_selected(); Ref<BTTask> sel = task_tree->get_selected();
@ -871,6 +1147,92 @@ void LimboAIEditor::_on_action_selected(int p_id) {
_mark_as_dirty(true); _mark_as_dirty(true);
} }
} break; } break;
case ACTION_REMOVE: {
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) {
undo_redo->create_action(TTR("Remove BT Task"));
if (sel->get_parent().is_null()) {
undo_redo->add_do_method(task_tree->get_bt().ptr(), SNAME("set_root_task"), Variant());
undo_redo->add_undo_method(task_tree->get_bt().ptr(), SNAME("set_root_task"), task_tree->get_bt()->get_root_task());
} else {
undo_redo->add_do_method(sel->get_parent().ptr(), SNAME("remove_child"), sel);
undo_redo->add_undo_method(sel->get_parent().ptr(), SNAME("add_child_at_index"), sel, sel->get_parent()->get_child_index(sel));
}
undo_redo->add_do_method(task_tree, SNAME("update_tree"));
undo_redo->add_undo_method(task_tree, SNAME("update_tree"));
undo_redo->commit_action();
EditorNode::get_singleton()->edit_resource(task_tree->get_selected());
_mark_as_dirty(true);
}
} break;
}
}
void LimboAIEditor::_misc_option_selected(int p_id) {
switch (p_id) {
case MISC_OPEN_DEBUGGER: {
ERR_FAIL_COND(LimboDebuggerPlugin::get_singleton() == nullptr);
if (LimboDebuggerPlugin::get_singleton()->get_session_tab()->get_window_enabled()) {
LimboDebuggerPlugin::get_singleton()->get_session_tab()->set_window_enabled(true);
} else {
EditorNode::get_singleton()->make_bottom_panel_item_visible(EditorDebuggerNode::get_singleton());
EditorDebuggerNode::get_singleton()->get_default_debugger()->switch_to_debugger(
LimboDebuggerPlugin::get_singleton()->get_session_tab_index());
}
} break;
case MISC_PROJECT_SETTINGS: {
_edit_project_settings();
} break;
case MISC_CREATE_SCRIPT_TEMPLATE: {
String template_path = _get_script_template_path();
String template_dir = template_path.get_base_dir();
if (!FileAccess::exists(template_path)) {
Ref<DirAccess> dir = DirAccess::create(DirAccess::ACCESS_RESOURCES);
Error err;
if (!dir->exists(template_dir)) {
err = dir->make_dir_recursive(template_dir);
ERR_FAIL_COND(err != OK);
}
Ref<FileAccess> f = FileAccess::open(template_path, FileAccess::WRITE, &err);
ERR_FAIL_COND(err != OK);
String script_template =
"# meta-name: Custom Task\n"
"# meta-description: Custom task to be used in a BehaviorTree\n"
"# meta-default: true\n"
"@tool\n"
"extends _BASE_\n"
"## _CLASS_\n"
"\n\n"
"# Display a customized name (requires @tool).\n"
"func _generate_name() -> String:\n"
"_TS_return \"_CLASS_\"\n"
"\n\n"
"# Called once during initialization.\n"
"func _setup() -> void:\n"
"_TS_pass\n"
"\n\n"
"# Called each time this task is entered.\n"
"func _enter() -> void:\n"
"_TS_pass\n"
"\n\n"
"# Called each time this task is exited.\n"
"func _exit() -> void:\n"
"_TS_pass\n"
"\n\n"
"# Called each time this task is ticked (aka executed).\n"
"func _tick(delta: float) -> int:\n"
"_TS_return SUCCESS\n";
f->store_string(script_template);
f->close();
}
ScriptEditor::get_singleton()->open_file(template_path);
} break;
} }
} }
@ -879,41 +1241,7 @@ void LimboAIEditor::_on_tree_task_selected(const Ref<BTTask> &p_task) {
} }
void LimboAIEditor::_on_tree_task_double_clicked() { void LimboAIEditor::_on_tree_task_double_clicked() {
if (!task_tree->get_selected().is_valid()) { _action_selected(ACTION_RENAME);
return;
}
rename_dialog->popup_centered();
rename_edit->set_text(task_tree->get_selected()->get_custom_name());
rename_edit->select_all();
rename_edit->grab_focus();
}
void LimboAIEditor::_on_panel_task_selected(String p_task) {
Ref<BTTask> task;
if (p_task.begins_with("res:")) {
Ref<Script> s = ResourceLoader::load(p_task, "Script");
ERR_FAIL_COND_MSG(s.is_null() || !s->is_valid(), vformat("LimboAI: Failed to instance task. Bad script: %s", p_task));
Variant inst = ClassDB::instantiate(s->get_instance_base_type());
ERR_FAIL_COND_MSG(inst.is_zero(), vformat("LimboAI: Failed to instance base type \"%s\".", s->get_instance_base_type()));
if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
if (!inst.is_ref_counted()) {
memdelete((Object *)inst);
}
ERR_PRINT(vformat("LimboAI: Failed to instance task. Script is not a BTTask: %s", p_task));
return;
}
if (inst && s.is_valid()) {
((Object *)inst)->set_script(s);
task = inst;
}
} else {
task = ClassDB::instantiate(p_task);
}
_add_task(task);
} }
void LimboAIEditor::_on_visibility_changed() { void LimboAIEditor::_on_visibility_changed() {
@ -927,6 +1255,7 @@ void LimboAIEditor::_on_visibility_changed() {
task_panel->refresh(); task_panel->refresh();
} }
_update_favorite_tasks();
} }
void LimboAIEditor::_on_header_pressed() { void LimboAIEditor::_on_header_pressed() {
@ -1073,6 +1402,86 @@ void LimboAIEditor::apply_changes() {
} }
} }
void LimboAIEditor::_update_favorite_tasks() {
for (int i = 0; i < fav_tasks_hbox->get_child_count(); i++) {
fav_tasks_hbox->get_child(i)->queue_free();
}
Array favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
for (int i = 0; i < favorite_tasks.size(); i++) {
String task_meta = favorite_tasks[i];
if (task_meta.is_empty() || (!FileAccess::exists(task_meta) && !ClassDB::class_exists(task_meta))) {
call_deferred(SNAME("_update_banners"));
continue;
}
Button *btn = memnew(Button);
String task_name;
if (task_meta.begins_with("res:")) {
task_name = task_meta.get_file().get_basename().trim_prefix("BT").to_pascal_case();
} else {
task_name = task_meta.trim_prefix("BT");
}
btn->set_text(task_name);
btn->set_meta(SNAME("task_meta"), task_meta);
btn->set_icon(LimboUtility::get_singleton()->get_task_icon(task_meta));
btn->set_tooltip_text(vformat(TTR("Add %s task."), task_name));
btn->set_flat(true);
btn->add_theme_constant_override(SNAME("icon_max_width"), 16 * EDSCALE); // Force user icons to be of the proper size.
btn->set_focus_mode(Control::FOCUS_NONE);
btn->connect(SNAME("pressed"), callable_mp(this, &LimboAIEditor::_add_task_by_class_or_path).bind(task_meta));
fav_tasks_hbox->add_child(btn);
}
}
void LimboAIEditor::_update_misc_menu() {
PopupMenu *misc_menu = misc_btn->get_popup();
misc_menu->clear();
misc_menu->add_icon_shortcut(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Debug"), SNAME("EditorIcons")), ED_GET_SHORTCUT("limbo_ai/open_debugger"), MISC_OPEN_DEBUGGER);
misc_menu->add_item(TTR("Project Settings..."), MISC_PROJECT_SETTINGS);
misc_menu->add_separator();
misc_menu->add_item(
FileAccess::exists(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("Create Script Template"),
MISC_CREATE_SCRIPT_TEMPLATE);
}
void LimboAIEditor::_update_banners() {
for (int i = 0; i < banners->get_child_count(); i++) {
if (banners->get_child(i)->has_meta(SNAME("managed"))) {
banners->get_child(i)->queue_free();
}
}
for (String dir_setting : { "limbo_ai/behavior_tree/user_task_dir_1", "limbo_ai/behavior_tree/user_task_dir_2", "limbo_ai/behavior_tree/user_task_dir_3" }) {
String task_dir = GLOBAL_GET(dir_setting);
if (!task_dir.is_empty() && !DirAccess::exists(task_dir)) {
ActionBanner *banner = memnew(ActionBanner);
banner->set_text(vformat(TTR("Task folder not found: %s"), task_dir));
banner->add_action(TTR("Create"), callable_mp(this, &LimboAIEditor::_create_user_task_dir), true);
banner->add_action(TTR("Edit Path..."), callable_mp(this, &LimboAIEditor::_edit_project_settings));
banner->set_meta(SNAME("managed"), Variant(true));
banners->call_deferred(SNAME("add_child"), banner);
}
}
Array favorite_tasks = GLOBAL_GET("limbo_ai/behavior_tree/favorite_tasks");
for (int i = 0; i < favorite_tasks.size(); i++) {
String task_meta = favorite_tasks[i];
if (task_meta.is_empty() || (!FileAccess::exists(task_meta) && !ClassDB::class_exists(task_meta))) {
ActionBanner *banner = memnew(ActionBanner);
banner->set_text(vformat(TTR("Favorite task not found: %s"), task_meta));
banner->add_action(TTR("Remove"), callable_mp(this, &LimboAIEditor::_remove_task_from_favorite).bind(task_meta), true);
banner->add_action(TTR("Edit Favorite Tasks..."), callable_mp(this, &LimboAIEditor::_edit_project_settings));
banner->set_meta(SNAME("managed"), Variant(true));
banners->call_deferred(SNAME("add_child"), banner);
}
}
}
void LimboAIEditor::_notification(int p_what) { void LimboAIEditor::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case NOTIFICATION_ENTER_TREE: { case NOTIFICATION_ENTER_TREE: {
@ -1090,9 +1499,6 @@ void LimboAIEditor::_notification(int p_what) {
conf.save(conf_path); conf.save(conf_path);
} break; } break;
case NOTIFICATION_THEME_CHANGED: { case NOTIFICATION_THEME_CHANGED: {
selector_btn->set_icon(EditorNode::get_singleton()->get_class_icon("BTSelector"));
sequence_btn->set_icon(EditorNode::get_singleton()->get_class_icon("BTSequence"));
parallel_btn->set_icon(EditorNode::get_singleton()->get_class_icon("BTParallel"));
new_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("New"), SNAME("EditorIcons"))); new_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("New"), SNAME("EditorIcons")));
load_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Load"), SNAME("EditorIcons"))); load_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Load"), SNAME("EditorIcons")));
save_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Save"), SNAME("EditorIcons"))); save_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Save"), SNAME("EditorIcons")));
@ -1100,6 +1506,9 @@ void LimboAIEditor::_notification(int p_what) {
history_back->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Back"), SNAME("EditorIcons"))); history_back->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Back"), SNAME("EditorIcons")));
history_forward->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Forward"), SNAME("EditorIcons"))); history_forward->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Forward"), SNAME("EditorIcons")));
misc_btn->set_icon(EditorNode::get_singleton()->get_gui_base()->get_theme_icon(SNAME("Tools"), SNAME("EditorIcons")));
_update_favorite_tasks();
_update_header(); _update_header();
} }
} }
@ -1120,6 +1529,20 @@ void LimboAIEditor::_bind_methods() {
LimboAIEditor::LimboAIEditor() { LimboAIEditor::LimboAIEditor() {
idx_history = 0; idx_history = 0;
ED_SHORTCUT("limbo_ai/rename_task", TTR("Rename"), Key::F2);
ED_SHORTCUT_OVERRIDE("limbo_ai/rename_task", "macos", Key::ENTER);
ED_SHORTCUT("limbo_ai/move_task_up", TTR("Move Up"), KeyModifierMask::CMD_OR_CTRL | Key::UP);
ED_SHORTCUT("limbo_ai/move_task_down", TTR("Move Down"), KeyModifierMask::CMD_OR_CTRL | Key::DOWN);
ED_SHORTCUT("limbo_ai/duplicate_task", TTR("Duplicate"), KeyModifierMask::CMD_OR_CTRL | Key::D);
ED_SHORTCUT("limbo_ai/remove_task", TTR("Remove"), Key::KEY_DELETE);
ED_SHORTCUT("limbo_ai/new_behavior_tree", TTR("New Behavior Tree"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::N);
ED_SHORTCUT("limbo_ai/save_behavior_tree", TTR("Save Behavior Tree"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::S);
ED_SHORTCUT("limbo_ai/load_behavior_tree", TTR("Load Behavior Tree"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::L);
ED_SHORTCUT("limbo_ai/open_debugger", TTR("Open Debugger"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::ALT | Key::D);
set_process_shortcut_input(true);
save_dialog = memnew(FileDialog); save_dialog = memnew(FileDialog);
save_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE); save_dialog->set_file_mode(FileDialog::FILE_MODE_SAVE_FILE);
save_dialog->set_title("Save Behavior Tree"); save_dialog->set_title("Save Behavior Tree");
@ -1136,76 +1559,80 @@ LimboAIEditor::LimboAIEditor() {
load_dialog->hide(); load_dialog->hide();
add_child(load_dialog); add_child(load_dialog);
VBoxContainer *vb = memnew(VBoxContainer); vbox = memnew(VBoxContainer);
vb->set_anchor(SIDE_RIGHT, ANCHOR_END); vbox->set_anchor(SIDE_RIGHT, ANCHOR_END);
vb->set_anchor(SIDE_BOTTOM, ANCHOR_END); vbox->set_anchor(SIDE_BOTTOM, ANCHOR_END);
add_child(vb); add_child(vbox);
HBoxContainer *panel = memnew(HBoxContainer); HBoxContainer *toolbar = memnew(HBoxContainer);
vb->add_child(panel); vbox->add_child(toolbar);
selector_btn = memnew(Button); PackedStringArray favorite_tasks_default;
selector_btn->set_text(TTR("Selector")); favorite_tasks_default.append("BTSelector");
selector_btn->set_tooltip_text(TTR("Add Selector task.")); favorite_tasks_default.append("BTSequence");
selector_btn->set_flat(true); favorite_tasks_default.append("BTParallel");
selector_btn->set_focus_mode(Control::FOCUS_NONE); GLOBAL_DEF(PropertyInfo(Variant::PACKED_STRING_ARRAY, "limbo_ai/behavior_tree/favorite_tasks", PROPERTY_HINT_ARRAY_TYPE, "String"), favorite_tasks_default);
selector_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_add_task_with_prototype).bind(Ref<BTTask>(memnew(BTSelector))));
panel->add_child(selector_btn);
sequence_btn = memnew(Button); fav_tasks_hbox = memnew(HBoxContainer);
sequence_btn->set_text(TTR("Sequence")); toolbar->add_child(fav_tasks_hbox);
sequence_btn->set_tooltip_text(TTR("Add Sequence task."));
sequence_btn->set_flat(true);
sequence_btn->set_focus_mode(Control::FOCUS_NONE);
sequence_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_add_task_with_prototype).bind(Ref<BTTask>(memnew(BTSequence))));
panel->add_child(sequence_btn);
parallel_btn = memnew(Button); comment_btn = memnew(Button);
parallel_btn->set_text(TTR("Parallel")); comment_btn->set_text(TTR("Comment"));
parallel_btn->set_tooltip_text(TTR("Add Parallel task.")); comment_btn->set_icon(LimboUtility::get_singleton()->get_task_icon("BTComment"));
parallel_btn->set_flat(true); comment_btn->set_tooltip_text(TTR("Add a BTComment task."));
parallel_btn->set_focus_mode(Control::FOCUS_NONE); comment_btn->set_flat(true);
parallel_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_add_task_with_prototype).bind(Ref<BTTask>(memnew(BTParallel)))); comment_btn->set_focus_mode(Control::FOCUS_NONE);
panel->add_child(parallel_btn); comment_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_add_task_by_class_or_path).bind("BTComment"));
toolbar->add_child(comment_btn);
panel->add_child(memnew(VSeparator)); toolbar->add_child(memnew(VSeparator));
new_btn = memnew(Button); new_btn = memnew(Button);
new_btn->set_text(TTR("New")); new_btn->set_text(TTR("New"));
new_btn->set_tooltip_text(TTR("Create new behavior tree.")); new_btn->set_tooltip_text(TTR("Create a new behavior tree."));
new_btn->set_shortcut(ED_GET_SHORTCUT("limbo_ai/new_behavior_tree"));
new_btn->set_flat(true); new_btn->set_flat(true);
new_btn->set_focus_mode(Control::FOCUS_NONE); new_btn->set_focus_mode(Control::FOCUS_NONE);
new_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_new_bt)); new_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_new_bt));
panel->add_child(new_btn); toolbar->add_child(new_btn);
load_btn = memnew(Button); load_btn = memnew(Button);
load_btn->set_text(TTR("Load")); load_btn->set_text(TTR("Load"));
load_btn->set_tooltip_text(TTR("Load behavior tree.")); load_btn->set_tooltip_text(TTR("Load behavior tree from a resource file."));
load_btn->set_shortcut(ED_GET_SHORTCUT("limbo_ai/load_behavior_tree"));
load_btn->set_flat(true); load_btn->set_flat(true);
load_btn->set_focus_mode(Control::FOCUS_NONE); load_btn->set_focus_mode(Control::FOCUS_NONE);
load_btn->connect("pressed", callable_mp(load_dialog, &FileDialog::popup_file_dialog)); load_btn->connect("pressed", callable_mp(load_dialog, &FileDialog::popup_file_dialog));
panel->add_child(load_btn); toolbar->add_child(load_btn);
save_btn = memnew(Button); save_btn = memnew(Button);
save_btn->set_text(TTR("Save")); save_btn->set_text(TTR("Save"));
save_btn->set_tooltip_text(TTR("Save current behavior tree.")); save_btn->set_tooltip_text(TTR("Save edited behavior tree to a resource file."));
save_btn->set_shortcut(ED_GET_SHORTCUT("limbo_ai/save_behavior_tree"));
save_btn->set_flat(true); save_btn->set_flat(true);
save_btn->set_focus_mode(Control::FOCUS_NONE); save_btn->set_focus_mode(Control::FOCUS_NONE);
save_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_on_save_pressed)); save_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_on_save_pressed));
panel->add_child(save_btn); toolbar->add_child(save_btn);
panel->add_child(memnew(VSeparator)); toolbar->add_child(memnew(VSeparator));
new_script_btn = memnew(Button); new_script_btn = memnew(Button);
new_script_btn->set_text(TTR("New Task")); new_script_btn->set_text(TTR("New Task"));
new_script_btn->set_tooltip_text(TTR("Create new task script and edit it.")); new_script_btn->set_tooltip_text(TTR("Create new task script and edit it."));
new_script_btn->set_flat(true); new_script_btn->set_flat(true);
new_script_btn->set_focus_mode(Control::FOCUS_NONE); new_script_btn->set_focus_mode(Control::FOCUS_NONE);
panel->add_child(new_script_btn); toolbar->add_child(new_script_btn);
misc_btn = memnew(MenuButton);
misc_btn->set_text(TTR("Misc"));
misc_btn->set_flat(true);
misc_btn->connect("pressed", callable_mp(this, &LimboAIEditor::_update_misc_menu));
misc_btn->get_popup()->connect("id_pressed", callable_mp(this, &LimboAIEditor::_misc_option_selected));
toolbar->add_child(misc_btn);
HBoxContainer *nav = memnew(HBoxContainer); HBoxContainer *nav = memnew(HBoxContainer);
nav->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END); nav->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END);
panel->add_child(nav); toolbar->add_child(nav);
history_back = memnew(Button); history_back = memnew(Button);
history_back->set_flat(true); history_back->set_flat(true);
@ -1223,22 +1650,24 @@ LimboAIEditor::LimboAIEditor() {
header->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); header->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT);
header->add_theme_constant_override("hseparation", 8); header->add_theme_constant_override("hseparation", 8);
header->connect("pressed", callable_mp(this, &LimboAIEditor::_on_header_pressed)); header->connect("pressed", callable_mp(this, &LimboAIEditor::_on_header_pressed));
vb->add_child(header); vbox->add_child(header);
hsc = memnew(HSplitContainer); hsc = memnew(HSplitContainer);
hsc->set_h_size_flags(SIZE_EXPAND_FILL); hsc->set_h_size_flags(SIZE_EXPAND_FILL);
hsc->set_v_size_flags(SIZE_EXPAND_FILL); hsc->set_v_size_flags(SIZE_EXPAND_FILL);
vb->add_child(hsc); hsc->set_focus_mode(FOCUS_NONE);
vbox->add_child(hsc);
task_tree = memnew(TaskTree); task_tree = memnew(TaskTree);
task_tree->set_v_size_flags(SIZE_EXPAND_FILL); task_tree->set_v_size_flags(SIZE_EXPAND_FILL);
task_tree->set_h_size_flags(SIZE_EXPAND_FILL); task_tree->set_h_size_flags(SIZE_EXPAND_FILL);
task_tree->hide();
task_tree->connect("rmb_pressed", callable_mp(this, &LimboAIEditor::_on_tree_rmb)); 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("task_selected", callable_mp(this, &LimboAIEditor::_on_tree_task_selected));
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed));
task_tree->connect("task_dragged", callable_mp(this, &LimboAIEditor::_on_task_dragged)); task_tree->connect("task_dragged", callable_mp(this, &LimboAIEditor::_on_task_dragged));
task_tree->connect("task_double_clicked", callable_mp(this, &LimboAIEditor::_on_tree_task_double_clicked)); task_tree->connect("task_double_clicked", callable_mp(this, &LimboAIEditor::_on_tree_task_double_clicked));
task_tree->hide(); task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed));
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_update_banners));
hsc->add_child(task_tree); hsc->add_child(task_tree);
usage_hint = memnew(Panel); usage_hint = memnew(Panel);
@ -1256,29 +1685,29 @@ LimboAIEditor::LimboAIEditor() {
task_panel = memnew(TaskPanel()); task_panel = memnew(TaskPanel());
hsc->set_split_offset(-300); hsc->set_split_offset(-300);
task_panel->connect("task_selected", callable_mp(this, &LimboAIEditor::_on_panel_task_selected)); 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(); task_panel->hide();
hsc->add_child(task_panel); hsc->add_child(task_panel);
banners = memnew(VBoxContainer);
vbox->add_child(banners);
menu = memnew(PopupMenu); menu = memnew(PopupMenu);
add_child(menu); add_child(menu);
menu->connect("id_pressed", callable_mp(this, &LimboAIEditor::_on_action_selected)); menu->connect("id_pressed", callable_mp(this, &LimboAIEditor::_action_selected));
rename_dialog = memnew(ConfirmationDialog); rename_dialog = memnew(ConfirmationDialog);
{ {
rename_dialog->set_title("Rename Task");
VBoxContainer *vbc = memnew(VBoxContainer); VBoxContainer *vbc = memnew(VBoxContainer);
rename_dialog->add_child(vbc); rename_dialog->add_child(vbc);
rename_edit = memnew(LineEdit); rename_edit = memnew(LineEdit);
vbc->add_child(rename_edit); vbc->add_child(rename_edit);
rename_edit->set_placeholder("Custom Name");
rename_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL); rename_edit->set_h_size_flags(Control::SIZE_EXPAND_FILL);
rename_edit->set_custom_minimum_size(Size2(350.0, 0.0)); rename_edit->set_custom_minimum_size(Size2(350.0, 0.0));
rename_dialog->register_text_enter(rename_edit); rename_dialog->register_text_enter(rename_edit);
rename_dialog->get_ok_button()->set_text(TTR("Rename"));
rename_dialog->connect("confirmed", callable_mp(this, &LimboAIEditor::_rename_task_confirmed)); rename_dialog->connect("confirmed", callable_mp(this, &LimboAIEditor::_rename_task_confirmed));
} }
add_child(rename_dialog); add_child(rename_dialog);

View File

@ -26,6 +26,7 @@
#include "scene/gui/file_dialog.h" #include "scene/gui/file_dialog.h"
#include "scene/gui/flow_container.h" #include "scene/gui/flow_container.h"
#include "scene/gui/line_edit.h" #include "scene/gui/line_edit.h"
#include "scene/gui/margin_container.h"
#include "scene/gui/panel_container.h" #include "scene/gui/panel_container.h"
#include "scene/gui/popup_menu.h" #include "scene/gui/popup_menu.h"
#include "scene/gui/split_container.h" #include "scene/gui/split_container.h"
@ -75,6 +76,13 @@ public:
~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 { class TaskSection : public VBoxContainer {
GDCLASS(TaskSection, VBoxContainer); GDCLASS(TaskSection, VBoxContainer);
@ -82,7 +90,8 @@ private:
FlowContainer *tasks_container; FlowContainer *tasks_container;
Button *section_header; Button *section_header;
void _on_task_button_pressed(const StringName &p_task); 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(); void _on_header_pressed();
protected: protected:
@ -92,7 +101,7 @@ protected:
public: public:
void set_filter(String p_filter); void set_filter(String p_filter);
void add_task_button(String p_name, const Ref<Texture> &icon, Variant p_meta); 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); void set_collapsed(bool p_collapsed);
bool is_collapsed() const; bool is_collapsed() const;
@ -107,14 +116,26 @@ class TaskPanel : public PanelContainer {
GDCLASS(TaskPanel, PanelContainer) GDCLASS(TaskPanel, PanelContainer)
private: private:
enum MenuAction {
MENU_EDIT_SCRIPT,
MENU_OPEN_DOC,
MENU_FAVORITE,
};
LineEdit *filter_edit; LineEdit *filter_edit;
VBoxContainer *sections; 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_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_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 _populate_scripted_tasks_from_dir(String p_path, List<String> *p_task_classes);
void _on_task_button_pressed(const StringName &p_task); void _menu_action_selected(int p_id);
void _on_filter_text_changed(String p_text); 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: protected:
static void _bind_methods(); static void _bind_methods();
@ -133,20 +154,31 @@ class LimboAIEditor : public Control {
private: private:
enum Action { enum Action {
ACTION_REMOVE, ACTION_RENAME,
ACTION_EDIT_SCRIPT,
ACTION_OPEN_DOC,
ACTION_MOVE_UP, ACTION_MOVE_UP,
ACTION_MOVE_DOWN, ACTION_MOVE_DOWN,
ACTION_DUPLICATE, ACTION_DUPLICATE,
ACTION_MAKE_ROOT, ACTION_MAKE_ROOT,
ACTION_REMOVE,
};
enum MiscMenu {
MISC_OPEN_DEBUGGER,
MISC_PROJECT_SETTINGS,
MISC_CREATE_SCRIPT_TEMPLATE,
}; };
Vector<Ref<BehaviorTree>> history; Vector<Ref<BehaviorTree>> history;
int idx_history; int idx_history;
HashSet<Ref<BehaviorTree>> dirty; HashSet<Ref<BehaviorTree>> dirty;
VBoxContainer *vbox;
Button *header; Button *header;
HSplitContainer *hsc; HSplitContainer *hsc;
TaskTree *task_tree; TaskTree *task_tree;
VBoxContainer *banners;
Panel *usage_hint; Panel *usage_hint;
PopupMenu *menu; PopupMenu *menu;
FileDialog *save_dialog; FileDialog *save_dialog;
@ -154,14 +186,14 @@ private:
Button *history_back; Button *history_back;
Button *history_forward; Button *history_forward;
TaskPanel *task_panel; TaskPanel *task_panel;
HBoxContainer *fav_tasks_hbox;
Button *selector_btn; Button *comment_btn;
Button *sequence_btn;
Button *parallel_btn;
Button *new_btn; Button *new_btn;
Button *load_btn; Button *load_btn;
Button *save_btn; Button *save_btn;
Button *new_script_btn; Button *new_script_btn;
MenuButton *misc_btn;
ConfirmationDialog *rename_dialog; ConfirmationDialog *rename_dialog;
LineEdit *rename_edit; LineEdit *rename_edit;
@ -171,14 +203,21 @@ private:
HashSet<String> disk_changed_files; HashSet<String> disk_changed_files;
void _add_task(const Ref<BTTask> &p_task); void _add_task(const Ref<BTTask> &p_task);
void _add_task_by_class_or_path(String p_class_or_path);
void _remove_task(const Ref<BTTask> &p_task); void _remove_task(const Ref<BTTask> &p_task);
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); } _FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
void _update_header() const; void _update_header() const;
void _update_history_buttons(); void _update_history_buttons();
void _update_favorite_tasks();
void _update_misc_menu();
void _update_banners();
void _new_bt(); void _new_bt();
void _save_bt(String p_path); void _save_bt(String p_path);
void _load_bt(String p_path); void _load_bt(String p_path);
void _mark_as_dirty(bool p_dirty); void _mark_as_dirty(bool p_dirty);
void _create_user_task_dir();
void _edit_project_settings();
void _remove_task_from_favorite(const String &p_task);
void _reload_modified(); void _reload_modified();
void _resave_modified(String _str = ""); void _resave_modified(String _str = "");
@ -186,18 +225,20 @@ private:
void _rename_task_confirmed(); void _rename_task_confirmed();
void _on_tree_rmb(const Vector2 &p_menu_pos); void _on_tree_rmb(const Vector2 &p_menu_pos);
void _on_action_selected(int p_id); void _action_selected(int p_id);
void _misc_option_selected(int p_id);
void _on_tree_task_selected(const Ref<BTTask> &p_task); void _on_tree_task_selected(const Ref<BTTask> &p_task);
void _on_tree_task_double_clicked(); void _on_tree_task_double_clicked();
void _on_visibility_changed(); void _on_visibility_changed();
void _on_header_pressed(); void _on_header_pressed();
void _on_save_pressed(); void _on_save_pressed();
void _on_panel_task_selected(String p_task);
void _on_history_back(); void _on_history_back();
void _on_history_forward(); void _on_history_forward();
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type); void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
void _on_resources_reload(const Vector<String> &p_resources); void _on_resources_reload(const Vector<String> &p_resources);
virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
protected: protected:
static void _bind_methods(); static void _bind_methods();

1
icons/BTComment.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"><path d="m11.77 6.91-.75 2.22h2.8l-.21 1.97h-3.22l-1.26 3.91h-2.38l1.27-3.91h-2.1l-1.25 3.91h-2.33l1.22-3.91h-2.57l.2-1.97h3l.76-2.22h-2.74l.2-2h3.13l1.29-3.9h2.35l-1.27 3.9h2.16l1.26-3.9h2.33l-1.26 3.9h2.59l-.2 2zm-5.23 2.22h2.13l.75-2.22h-2.12z" fill="#808080"/></svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@ -60,6 +60,7 @@
#include "bt/tasks/actions/bt_wait.h" #include "bt/tasks/actions/bt_wait.h"
#include "bt/tasks/actions/bt_wait_ticks.h" #include "bt/tasks/actions/bt_wait_ticks.h"
#include "bt/tasks/bt_action.h" #include "bt/tasks/bt_action.h"
#include "bt/tasks/bt_comment.h"
#include "bt/tasks/bt_composite.h" #include "bt/tasks/bt_composite.h"
#include "bt/tasks/bt_condition.h" #include "bt/tasks/bt_condition.h"
#include "bt/tasks/bt_decorator.h" #include "bt/tasks/bt_decorator.h"
@ -120,6 +121,8 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(BTPlayer); GDREGISTER_CLASS(BTPlayer);
GDREGISTER_CLASS(BTState); GDREGISTER_CLASS(BTState);
GDREGISTER_CLASS(BTComment);
GDREGISTER_CLASS(BTComposite); GDREGISTER_CLASS(BTComposite);
GDREGISTER_CLASS(BTSequence); GDREGISTER_CLASS(BTSequence);
GDREGISTER_CLASS(BTSelector); GDREGISTER_CLASS(BTSelector);