/** * limbo_ai_editor_plugin.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. * ============================================================================= */ #ifdef TOOLS_ENABLED #include "limbo_ai_editor_plugin.h" #include "modules/limboai/bt/behavior_tree.h" #include "modules/limboai/bt/tasks/bt_action.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/editor_file_system.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 "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 &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 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("BTComment")) { 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 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 &p_task) const { if (p_task.is_null()) { return nullptr; } TreeItem *item = tree->get_root(); List 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 &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 &p_task) { ERR_FAIL_COND(p_task.is_null()); TreeItem *item = _find_item(p_task); if (item) { _update_item(item); } } Ref 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 task = d["task"]; const Ref 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 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 ^ //**** 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 &p_event, const String &p_task) { if (!p_event->is_pressed()) { return; } Ref 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