Merge branch 'categories+filtering'
This commit is contained in:
commit
f89b7897f6
6
SCsub
6
SCsub
|
@ -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")
|
||||
|
|
|
@ -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;
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
class BTCheckVar : public BTCondition {
|
||||
GDCLASS(BTCheckVar, BTCondition);
|
||||
TASK_CATEGORY(Blackboard);
|
||||
|
||||
private:
|
||||
String variable;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTDynamicSelector : public BTComposite {
|
||||
GDCLASS(BTDynamicSelector, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTDynamicSequence : public BTComposite {
|
||||
GDCLASS(BTDynamicSequence, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTParallel : public BTComposite {
|
||||
GDCLASS(BTParallel, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int num_successes_required = 1;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTRandomSelector : public BTComposite {
|
||||
GDCLASS(BTRandomSelector, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTRandomSequence : public BTComposite {
|
||||
GDCLASS(BTRandomSequence, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTSelector : public BTComposite {
|
||||
GDCLASS(BTSelector, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTSequence : public BTComposite {
|
||||
GDCLASS(BTSequence, BTComposite);
|
||||
TASK_CATEGORY(Composites);
|
||||
|
||||
private:
|
||||
int last_running_idx = 0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTAlwaysFail : public BTDecorator {
|
||||
GDCLASS(BTAlwaysFail, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTAlwaysSucceed : public BTDecorator {
|
||||
GDCLASS(BTAlwaysSucceed, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTCooldown : public BTDecorator {
|
||||
GDCLASS(BTCooldown, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
double duration = 10.0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTDelay : public BTDecorator {
|
||||
GDCLASS(BTDelay, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
double seconds = 1.0;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTForEach : public BTDecorator {
|
||||
GDCLASS(BTForEach, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
String array_var;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTInvert : public BTDecorator {
|
||||
GDCLASS(BTInvert, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTNewScope : public BTDecorator {
|
||||
GDCLASS(BTNewScope, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
Dictionary blackboard_data;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTProbability : public BTDecorator {
|
||||
GDCLASS(BTProbability, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
float run_chance = 0.5;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTRepeat : public BTDecorator {
|
||||
GDCLASS(BTRepeat, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
bool forever = false;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTRepeatUntilFailure : public BTDecorator {
|
||||
GDCLASS(BTRepeatUntilFailure, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTRepeatUntilSuccess : public BTDecorator {
|
||||
GDCLASS(BTRepeatUntilSuccess, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTRunLimit : public BTDecorator {
|
||||
GDCLASS(BTRunLimit, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
int run_limit = 1;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTSubtree : public BTNewScope {
|
||||
GDCLASS(BTSubtree, BTNewScope);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
Ref<BehaviorTree> subtree;
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTTimeLimit : public BTDecorator {
|
||||
GDCLASS(BTTimeLimit, BTDecorator);
|
||||
TASK_CATEGORY(Decorators);
|
||||
|
||||
private:
|
||||
double time_limit = 5.0;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
class BTAwaitAnimation : public BTAction {
|
||||
GDCLASS(BTAwaitAnimation, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
Ref<BBNode> animation_player_param;
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTCallMethod : public BTAction {
|
||||
GDCLASS(BTCallMethod, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
StringName method_name;
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
class BTCheckAgentProperty : public BTCondition {
|
||||
GDCLASS(BTCheckAgentProperty, BTCondition);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
StringName property;
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
class BTPauseAnimation : public BTAction {
|
||||
GDCLASS(BTPauseAnimation, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
Ref<BBNode> animation_player_param;
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
class BTPlayAnimation : public BTAction {
|
||||
GDCLASS(BTPlayAnimation, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
Ref<BBNode> animation_player_param;
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTSetAgentProperty : public BTAction {
|
||||
GDCLASS(BTSetAgentProperty, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
StringName property;
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
class BTStopAnimation : public BTAction {
|
||||
GDCLASS(BTStopAnimation, BTAction);
|
||||
TASK_CATEGORY(Scene);
|
||||
|
||||
private:
|
||||
Ref<BBNode> animation_player_param;
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
class BTConsolePrint : public BTAction {
|
||||
GDCLASS(BTConsolePrint, BTAction);
|
||||
TASK_CATEGORY(Utility);
|
||||
|
||||
private:
|
||||
String text;
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTFail : public BTAction {
|
||||
GDCLASS(BTFail, BTAction);
|
||||
TASK_CATEGORY(Utility);
|
||||
|
||||
protected:
|
||||
virtual int _tick(double p_delta) override;
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTRandomWait : public BTAction {
|
||||
GDCLASS(BTRandomWait, BTAction);
|
||||
TASK_CATEGORY(Utility);
|
||||
|
||||
private:
|
||||
double min_duration = 1.0;
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTWait : public BTAction {
|
||||
GDCLASS(BTWait, BTAction);
|
||||
TASK_CATEGORY(Utility);
|
||||
|
||||
private:
|
||||
double duration = 1.0;
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
class BTWaitTicks : public BTAction {
|
||||
GDCLASS(BTWaitTicks, BTAction);
|
||||
TASK_CATEGORY(Utility);
|
||||
|
||||
private:
|
||||
int num_ticks = 1;
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
};
|
|
@ -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 |
|
@ -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 |
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue