2022-09-01 22:20:37 +00:00
|
|
|
/* limbo_ai_editor_plugin.cpp */
|
|
|
|
|
|
|
|
#ifdef TOOLS_ENABLED
|
|
|
|
|
|
|
|
#include "limbo_ai_editor_plugin.h"
|
|
|
|
|
|
|
|
#include "core/class_db.h"
|
2022-09-03 15:00:20 +00:00
|
|
|
#include "core/dictionary.h"
|
2022-09-02 22:10:22 +00:00
|
|
|
#include "core/error_list.h"
|
|
|
|
#include "core/error_macros.h"
|
2022-09-06 11:28:25 +00:00
|
|
|
#include "core/io/config_file.h"
|
2022-09-05 14:39:40 +00:00
|
|
|
#include "core/io/image_loader.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "core/io/resource_loader.h"
|
|
|
|
#include "core/io/resource_saver.h"
|
2022-09-02 17:30:32 +00:00
|
|
|
#include "core/list.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "core/math/math_defs.h"
|
|
|
|
#include "core/object.h"
|
2022-09-02 22:10:22 +00:00
|
|
|
#include "core/os/dir_access.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "core/os/memory.h"
|
|
|
|
#include "core/print_string.h"
|
2022-09-03 10:59:11 +00:00
|
|
|
#include "core/project_settings.h"
|
2022-09-02 22:10:22 +00:00
|
|
|
#include "core/script_language.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "core/string_name.h"
|
2022-09-02 13:43:54 +00:00
|
|
|
#include "core/typedefs.h"
|
2022-09-02 17:30:32 +00:00
|
|
|
#include "core/ustring.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "core/variant.h"
|
|
|
|
#include "core/vector.h"
|
2022-09-05 19:57:24 +00:00
|
|
|
#include "editor/editor_inspector.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "editor/editor_node.h"
|
|
|
|
#include "editor/editor_plugin.h"
|
2022-09-05 14:39:40 +00:00
|
|
|
#include "editor/editor_scale.h"
|
2022-09-06 11:28:25 +00:00
|
|
|
#include "editor/editor_settings.h"
|
2022-09-02 22:10:22 +00:00
|
|
|
#include "modules/limboai/bt/actions/bt_action.h"
|
2022-09-02 13:43:54 +00:00
|
|
|
#include "modules/limboai/bt/behavior_tree.h"
|
2022-09-02 22:10:22 +00:00
|
|
|
#include "modules/limboai/bt/bt_task.h"
|
|
|
|
#include "modules/limboai/bt/composites/bt_parallel.h"
|
|
|
|
#include "modules/limboai/bt/composites/bt_selector.h"
|
|
|
|
#include "modules/limboai/bt/composites/bt_sequence.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/box_container.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "scene/gui/button.h"
|
2022-09-05 11:31:38 +00:00
|
|
|
#include "scene/gui/control.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/file_dialog.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "scene/gui/flow_container.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/label.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "scene/gui/line_edit.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/popup_menu.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "scene/gui/scroll_container.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/separator.h"
|
2022-09-02 12:25:03 +00:00
|
|
|
#include "scene/gui/split_container.h"
|
2022-09-01 22:20:37 +00:00
|
|
|
#include "scene/gui/tree.h"
|
|
|
|
#include <cstddef>
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
////////////////////////////// TaskTree //////////////////////////////////////
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
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) {
|
|
|
|
ERR_FAIL_COND_MSG(p_item == nullptr, "Argument \"p_item\" is null.");
|
|
|
|
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());
|
2022-09-05 14:39:40 +00:00
|
|
|
if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().empty()) {
|
|
|
|
p_item->set_icon(0, LimboAIEditor::get_task_icon(task->get_script_instance()->get_script()->get_path()));
|
|
|
|
} else {
|
|
|
|
p_item->set_icon(0, LimboAIEditor::get_task_icon(task->get_class()));
|
|
|
|
}
|
2022-09-01 22:20:37 +00:00
|
|
|
p_item->set_editable(0, false);
|
|
|
|
|
2022-09-03 11:11:50 +00:00
|
|
|
for (int i = 0; i < p_item->get_button_count(0); i++) {
|
|
|
|
p_item->erase_button(0, i);
|
|
|
|
}
|
|
|
|
String warning = task->get_configuration_warning();
|
|
|
|
if (!warning.empty()) {
|
|
|
|
p_item->add_button(0, get_icon("NodeWarning", "EditorIcons"), 0, false, warning);
|
|
|
|
}
|
2022-09-01 22:20:37 +00:00
|
|
|
|
|
|
|
// 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->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_children()) {
|
|
|
|
stack.push_back(item->get_children());
|
|
|
|
}
|
|
|
|
item = item->get_next();
|
|
|
|
if (item == nullptr && !stack.empty()) {
|
|
|
|
item = stack.front()->get();
|
|
|
|
stack.pop_front();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskTree::_on_item_rmb_selected(const Vector2 &p_pos) {
|
|
|
|
emit_signal("rmb_pressed", tree->get_global_transform().xform(p_pos));
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskTree::_on_item_selected() {
|
|
|
|
if (last_selected.is_valid()) {
|
|
|
|
update_task(last_selected);
|
2022-09-03 15:13:15 +00:00
|
|
|
if (last_selected->is_connected("changed", this, "_on_task_changed")) {
|
|
|
|
last_selected->disconnect("changed", this, "_on_task_changed");
|
|
|
|
}
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
last_selected = get_selected();
|
2022-09-03 15:13:15 +00:00
|
|
|
last_selected->connect("changed", this, "_on_task_changed");
|
2022-09-01 22:20:37 +00:00
|
|
|
emit_signal("task_selected", last_selected);
|
|
|
|
}
|
|
|
|
|
2022-09-03 15:13:15 +00:00
|
|
|
void TaskTree::_on_task_changed() {
|
|
|
|
_update_item(tree->get_selected());
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
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.");
|
2022-09-03 15:13:15 +00:00
|
|
|
|
|
|
|
if (last_selected.is_valid() and last_selected->is_connected("changed", this, "_on_task_changed")) {
|
|
|
|
last_selected->disconnect("changed", this, "_on_task_changed");
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
bt = p_behavior_tree;
|
|
|
|
tree->clear();
|
|
|
|
if (bt->get_root_task().is_valid()) {
|
|
|
|
_create_tree(bt->get_root_task(), nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
Variant TaskTree::get_drag_data_fw(const Point2 &p_point, Control *p_from) {
|
|
|
|
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, Control *p_from) 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, Control *p_from) {
|
|
|
|
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("task_dragged", task, item->get_metadata(0), tree->get_drop_section_at_position(p_point));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void TaskTree::_bind_methods() {
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_item_rmb_selected"), &TaskTree::_on_item_rmb_selected);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_item_selected"), &TaskTree::_on_item_selected);
|
2022-09-03 15:13:15 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_task_changed"), &TaskTree::_on_task_changed);
|
2022-09-01 22:20:37 +00:00
|
|
|
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);
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
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);
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
ADD_SIGNAL(MethodInfo("rmb_pressed"));
|
|
|
|
ADD_SIGNAL(MethodInfo("task_selected"));
|
2022-09-03 15:00:20 +00:00
|
|
|
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")));
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TaskTree::TaskTree() {
|
2022-09-03 15:00:20 +00:00
|
|
|
editable = true;
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
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_min_width(1, 64);
|
|
|
|
tree->set_anchor(MARGIN_RIGHT, ANCHOR_END);
|
|
|
|
tree->set_anchor(MARGIN_BOTTOM, ANCHOR_END);
|
|
|
|
tree->set_allow_rmb_select(true);
|
|
|
|
tree->connect("item_rmb_selected", this, "_on_item_rmb_selected");
|
|
|
|
tree->connect("item_selected", this, "_on_item_selected");
|
2022-09-03 15:00:20 +00:00
|
|
|
tree->set_drag_forwarding(this);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TaskTree::~TaskTree() {
|
2022-09-03 15:13:15 +00:00
|
|
|
if (last_selected.is_valid() and last_selected->is_connected("changed", this, "_on_task_changed")) {
|
|
|
|
last_selected->disconnect("changed", this, "_on_task_changed");
|
|
|
|
}
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
////////////////////////////// TaskTree //////////////////////////////////////
|
|
|
|
|
|
|
|
////////////////////////////// TaskSection ////////////////////////////////////
|
2022-09-01 22:20:37 +00:00
|
|
|
|
2022-09-02 12:25:03 +00:00
|
|
|
void TaskSection::_on_task_button_pressed(const StringName &p_task) {
|
|
|
|
emit_signal("task_button_pressed", p_task);
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskSection::_on_header_pressed() {
|
|
|
|
tasks_container->set_visible(!tasks_container->is_visible());
|
|
|
|
section_header->set_icon(tasks_container->is_visible() ? get_icon("GuiTreeArrowDown", "EditorIcons") : get_icon("GuiTreeArrowRight", "EditorIcons"));
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskSection::set_filter(String p_filter_text) {
|
|
|
|
int num_hidden = 0;
|
|
|
|
if (p_filter_text.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());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-02 17:30:32 +00:00
|
|
|
void TaskSection::add_task_button(String p_name, const Ref<Texture> &icon, Variant p_meta) {
|
|
|
|
Button *btn = memnew(Button);
|
|
|
|
btn->set_text(p_name);
|
|
|
|
btn->set_icon(icon);
|
|
|
|
btn->connect("pressed", this, "_on_task_button_pressed", varray(p_meta));
|
|
|
|
tasks_container->add_child(btn);
|
|
|
|
}
|
|
|
|
|
2022-09-02 12:25:03 +00:00
|
|
|
void TaskSection::_bind_methods() {
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_task_button_pressed", "p_class"), &TaskSection::_on_task_button_pressed);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_header_pressed"), &TaskSection::_on_header_pressed);
|
|
|
|
|
|
|
|
ADD_SIGNAL(MethodInfo("task_button_pressed"));
|
|
|
|
}
|
|
|
|
|
2022-09-02 17:30:32 +00:00
|
|
|
TaskSection::TaskSection(String p_category_name, EditorNode *p_editor) {
|
2022-09-02 12:25:03 +00:00
|
|
|
section_header = memnew(Button);
|
|
|
|
add_child(section_header);
|
2022-09-02 17:30:32 +00:00
|
|
|
section_header->set_text(p_category_name);
|
2022-09-02 12:25:03 +00:00
|
|
|
section_header->set_icon(p_editor->get_gui_base()->get_icon("GuiTreeArrowDown", "EditorIcons"));
|
|
|
|
section_header->set_focus_mode(FOCUS_NONE);
|
|
|
|
section_header->connect("pressed", this, "_on_header_pressed");
|
|
|
|
|
|
|
|
tasks_container = memnew(HFlowContainer);
|
|
|
|
add_child(tasks_container);
|
|
|
|
}
|
|
|
|
|
|
|
|
TaskSection::~TaskSection() {
|
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
////////////////////////////// TaskSection ////////////////////////////////////
|
|
|
|
|
|
|
|
////////////////////////////// TaskPanel /////////////////////////////////////
|
2022-09-02 12:25:03 +00:00
|
|
|
|
|
|
|
void TaskPanel::_on_task_button_pressed(const StringName &p_task) {
|
|
|
|
emit_signal("task_selected", p_task);
|
|
|
|
}
|
|
|
|
|
|
|
|
void TaskPanel::_on_filter_text_changed(String p_text) {
|
|
|
|
for (int i = 0; i < sections->get_child_count(); i++) {
|
|
|
|
TaskSection *sec = Object::cast_to<TaskSection>(sections->get_child(i));
|
|
|
|
sec->set_filter(p_text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-05 15:56:38 +00:00
|
|
|
void TaskPanel::refresh() {
|
2022-09-02 12:25:03 +00:00
|
|
|
filter_edit->set_right_icon(get_icon("Search", "EditorIcons"));
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-09-05 15:56:38 +00:00
|
|
|
for (int i = 0; i < sections->get_child_count(); i++) {
|
|
|
|
sections->get_child(i)->queue_delete();
|
|
|
|
}
|
|
|
|
|
2022-09-02 17:30:32 +00:00
|
|
|
HashMap<String, List<String>> categories;
|
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
categories["Composites"] = List<String>();
|
|
|
|
_populate_core_tasks_from_class("BTComposite", &categories["Composites"]);
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
categories["Actions"] = List<String>();
|
|
|
|
_populate_core_tasks_from_class("BTAction", &categories["Actions"]);
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
categories["Decorators"] = List<String>();
|
|
|
|
_populate_core_tasks_from_class("BTDecorator", &categories["Decorators"]);
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
categories["Conditions"] = List<String>();
|
|
|
|
_populate_core_tasks_from_class("BTCondition", &categories["Conditions"]);
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-09-02 22:10:22 +00:00
|
|
|
categories["User"] = List<String>();
|
2022-09-03 10:59:11 +00:00
|
|
|
|
2022-09-05 11:11:47 +00:00
|
|
|
String dir1 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1");
|
2022-10-28 23:50:47 +00:00
|
|
|
_populate_from_user_dir(dir1, &categories);
|
|
|
|
|
2022-09-05 11:11:47 +00:00
|
|
|
String dir2 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_2");
|
2022-10-28 23:50:47 +00:00
|
|
|
_populate_from_user_dir(dir2, &categories);
|
|
|
|
|
2022-09-05 11:11:47 +00:00
|
|
|
String dir3 = GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_3");
|
2022-10-28 23:50:47 +00:00
|
|
|
_populate_from_user_dir(dir3, &categories);
|
2022-09-02 17:30:32 +00:00
|
|
|
|
2022-09-02 22:10:22 +00:00
|
|
|
List<String> keys;
|
2022-09-02 17:30:32 +00:00
|
|
|
categories.get_key_list(&keys);
|
|
|
|
keys.sort();
|
|
|
|
for (List<String>::Element *E = keys.front(); E; E = E->next()) {
|
|
|
|
String cat = E->get();
|
|
|
|
List<String> task_list = categories.get(cat);
|
2022-10-28 23:50:47 +00:00
|
|
|
|
|
|
|
if (task_list.size() == 0) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-09-02 17:30:32 +00:00
|
|
|
TaskSection *sec = memnew(TaskSection(cat, editor));
|
|
|
|
for (List<String>::Element *E = task_list.front(); E; E = E->next()) {
|
2022-09-02 22:10:22 +00:00
|
|
|
String meta = E->get();
|
|
|
|
String tname;
|
|
|
|
Ref<Texture> icon;
|
2022-09-05 14:39:40 +00:00
|
|
|
icon = LimboAIEditor::get_task_icon(meta);
|
|
|
|
tname = meta.begins_with("res:") ? meta.get_file().get_basename().trim_prefix("BT") : meta.trim_prefix("BT");
|
2022-09-02 22:10:22 +00:00
|
|
|
sec->add_task_button(tname, icon, meta);
|
2022-09-02 17:30:32 +00:00
|
|
|
}
|
|
|
|
sec->set_filter("");
|
|
|
|
sec->connect("task_button_pressed", this, "_on_task_button_pressed");
|
|
|
|
sections->add_child(sec);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 (List<StringName>::Element *E = inheriters.front(); E; E = E->next()) {
|
|
|
|
p_task_classes->push_back(E->get());
|
|
|
|
}
|
2022-09-02 12:25:03 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
void TaskPanel::_populate_from_user_dir(String p_path, HashMap<String, List<String>> *p_categories) {
|
|
|
|
if (p_path.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
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.empty()) {
|
|
|
|
if (dir->current_is_dir()) {
|
2022-10-30 11:33:38 +00:00
|
|
|
String full_path;
|
|
|
|
String category;
|
|
|
|
if (fn == ".") {
|
|
|
|
full_path = p_path;
|
|
|
|
category = "User";
|
|
|
|
} else {
|
|
|
|
full_path = p_path.plus_file(fn);
|
|
|
|
category = fn.capitalize();
|
|
|
|
}
|
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
if (!p_categories->has(category)) {
|
|
|
|
p_categories->set(category, List<String>());
|
|
|
|
}
|
2022-10-30 11:33:38 +00:00
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
_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));
|
|
|
|
}
|
|
|
|
memdelete(dir);
|
|
|
|
}
|
|
|
|
|
2022-09-02 22:10:22 +00:00
|
|
|
void TaskPanel::_populate_scripted_tasks_from_dir(String p_path, List<String> *p_task_classes) {
|
2022-09-03 10:59:11 +00:00
|
|
|
if (p_path.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-02 22:10:22 +00:00
|
|
|
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.empty()) {
|
|
|
|
if (fn.ends_with(".gd")) {
|
|
|
|
String full_path = p_path.plus_file(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));
|
|
|
|
}
|
|
|
|
memdelete(dir);
|
|
|
|
}
|
|
|
|
|
2022-09-02 12:25:03 +00:00
|
|
|
void TaskPanel::_bind_methods() {
|
2022-09-05 15:56:38 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("refresh"), &TaskPanel::refresh);
|
2022-09-02 12:25:03 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_task_button_pressed"), &TaskPanel::_on_task_button_pressed);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_filter_text_changed"), &TaskPanel::_on_filter_text_changed);
|
|
|
|
|
|
|
|
ADD_SIGNAL(MethodInfo("task_selected"));
|
|
|
|
}
|
|
|
|
|
|
|
|
TaskPanel::TaskPanel(EditorNode *p_editor) {
|
|
|
|
editor = p_editor;
|
|
|
|
|
|
|
|
VBoxContainer *vb = memnew(VBoxContainer);
|
|
|
|
add_child(vb);
|
|
|
|
|
|
|
|
filter_edit = memnew(LineEdit);
|
|
|
|
vb->add_child(filter_edit);
|
|
|
|
filter_edit->set_clear_button_enabled(true);
|
|
|
|
filter_edit->connect("text_changed", this, "_on_filter_text_changed");
|
|
|
|
|
|
|
|
ScrollContainer *sc = memnew(ScrollContainer);
|
|
|
|
vb->add_child(sc);
|
|
|
|
sc->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
sc->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
|
|
|
|
sections = memnew(VBoxContainer);
|
|
|
|
sc->add_child(sections);
|
|
|
|
sections->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
sections->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
}
|
|
|
|
|
|
|
|
TaskPanel::~TaskPanel() {
|
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
////////////////////////////// TaskPanel /////////////////////////////////////
|
|
|
|
|
|
|
|
//////////////////////////// LimboAIEditor ///////////////////////////////////
|
2022-09-02 12:25:03 +00:00
|
|
|
|
2022-09-03 12:11:47 +00:00
|
|
|
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
|
|
|
|
ERR_FAIL_COND(p_task.is_null());
|
2022-09-05 19:57:24 +00:00
|
|
|
ERR_FAIL_COND(task_tree->get_bt().is_null());
|
2022-09-01 22:20:37 +00:00
|
|
|
Ref<BTTask> parent = task_tree->get_selected();
|
|
|
|
if (parent.is_null()) {
|
|
|
|
parent = task_tree->get_bt()->get_root_task();
|
|
|
|
}
|
|
|
|
if (parent.is_null()) {
|
2022-09-03 12:11:47 +00:00
|
|
|
task_tree->get_bt()->set_root_task(p_task);
|
2022-09-01 22:20:37 +00:00
|
|
|
} else {
|
2022-09-03 12:11:47 +00:00
|
|
|
parent->add_child(p_task);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
task_tree->update_tree();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_update_header() {
|
|
|
|
String text = task_tree->get_bt()->get_path();
|
|
|
|
if (text.empty()) {
|
|
|
|
text = TTR("New Behavior Tree");
|
2022-09-03 12:01:13 +00:00
|
|
|
} else if (dirty.has(task_tree->get_bt())) {
|
|
|
|
text += "(*)";
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
2022-09-03 12:01:13 +00:00
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
header->set_text(text);
|
|
|
|
header->set_icon(editor->get_object_icon(task_tree->get_bt().ptr(), "BehaviorTree"));
|
|
|
|
}
|
|
|
|
|
2022-09-02 13:43:54 +00:00
|
|
|
void LimboAIEditor::_update_history_buttons() {
|
|
|
|
history_back->set_disabled(idx_history == 0);
|
|
|
|
history_forward->set_disabled(idx_history == (history.size() - 1));
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void LimboAIEditor::_new_bt() {
|
|
|
|
BehaviorTree *bt = memnew(BehaviorTree);
|
|
|
|
bt->set_root_task(memnew(BTSelector));
|
2022-09-05 19:57:24 +00:00
|
|
|
editor->edit_resource(bt);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_save_bt(String p_path) {
|
|
|
|
ERR_FAIL_COND_MSG(p_path.empty(), "Empty p_path");
|
|
|
|
ERR_FAIL_COND_MSG(task_tree->get_bt().is_null(), "Behavior Tree is null.");
|
|
|
|
task_tree->get_bt()->set_path(p_path, true);
|
|
|
|
ResourceSaver::save(p_path, task_tree->get_bt(), ResourceSaver::FLAG_CHANGE_PATH);
|
|
|
|
_update_header();
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(false);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_load_bt(String p_path) {
|
|
|
|
ERR_FAIL_COND_MSG(p_path.empty(), "Empty p_path");
|
2022-09-02 13:43:54 +00:00
|
|
|
Ref<BehaviorTree> bt = ResourceLoader::load(p_path, "BehaviorTree");
|
|
|
|
|
|
|
|
if (history.find(bt) != -1) {
|
|
|
|
history.erase(bt);
|
|
|
|
history.push_back(bt);
|
|
|
|
}
|
|
|
|
|
2022-09-05 19:57:24 +00:00
|
|
|
editor->edit_resource(bt);
|
2022-09-02 13:43:54 +00:00
|
|
|
}
|
|
|
|
|
2022-09-05 19:57:24 +00:00
|
|
|
void LimboAIEditor::edit_bt(Ref<BehaviorTree> p_behavior_tree) {
|
2022-09-02 13:43:54 +00:00
|
|
|
ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "p_behavior_tree is null");
|
2022-09-05 19:57:24 +00:00
|
|
|
|
|
|
|
if (task_tree->get_bt() == p_behavior_tree) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-09-02 13:43:54 +00:00
|
|
|
task_tree->load_bt(p_behavior_tree);
|
|
|
|
|
|
|
|
int idx = history.find(p_behavior_tree);
|
|
|
|
if (idx != -1) {
|
|
|
|
idx_history = idx;
|
|
|
|
} else {
|
|
|
|
history.push_back(p_behavior_tree);
|
|
|
|
idx_history = history.size() - 1;
|
|
|
|
}
|
|
|
|
|
2022-09-05 19:57:24 +00:00
|
|
|
usage_hint->hide();
|
|
|
|
task_tree->show();
|
|
|
|
task_panel->show();
|
|
|
|
|
2022-09-02 13:43:54 +00:00
|
|
|
_update_history_buttons();
|
2022-09-01 22:20:37 +00:00
|
|
|
_update_header();
|
|
|
|
}
|
|
|
|
|
2022-09-03 12:01:13 +00:00
|
|
|
void LimboAIEditor::_mark_as_dirty(bool p_dirty) {
|
|
|
|
Ref<BehaviorTree> bt = task_tree->get_bt();
|
|
|
|
if (p_dirty && !dirty.has(bt)) {
|
|
|
|
dirty.insert(bt);
|
|
|
|
_update_header();
|
|
|
|
} else if (p_dirty == false && dirty.has(bt)) {
|
|
|
|
dirty.erase(bt);
|
|
|
|
_update_header();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
|
|
|
|
menu->set_size(Size2(1, 1));
|
|
|
|
menu->set_position(p_menu_pos);
|
|
|
|
|
|
|
|
menu->clear();
|
|
|
|
menu->add_icon_item(get_icon("Remove", "EditorIcons"), TTR("Remove"), ACTION_REMOVE);
|
|
|
|
menu->add_separator();
|
|
|
|
menu->add_icon_item(get_icon("MoveUp", "EditorIcons"), TTR("Move Up"), ACTION_MOVE_UP);
|
|
|
|
menu->add_icon_item(get_icon("MoveDown", "EditorIcons"), TTR("Move Down"), ACTION_MOVE_DOWN);
|
|
|
|
menu->add_icon_item(get_icon("Duplicate", "EditorIcons"), TTR("Duplicate"), ACTION_DUPLICATE);
|
|
|
|
menu->add_icon_item(get_icon("NewRoot", "EditorIcons"), TTR("Make Root"), ACTION_MAKE_ROOT);
|
|
|
|
|
|
|
|
menu->popup();
|
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_on_action_selected(int p_id) {
|
|
|
|
switch (p_id) {
|
|
|
|
case ACTION_REMOVE: {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid()) {
|
|
|
|
if (sel->get_parent().is_null()) {
|
|
|
|
task_tree->get_bt()->set_root_task(nullptr);
|
|
|
|
} else {
|
|
|
|
sel->get_parent()->remove_child(sel);
|
|
|
|
}
|
|
|
|
task_tree->update_tree();
|
|
|
|
editor->edit_node(nullptr);
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
} break;
|
|
|
|
case ACTION_MOVE_UP: {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid() && sel->get_parent().is_valid()) {
|
|
|
|
Ref<BTTask> parent = sel->get_parent();
|
|
|
|
int idx = parent->get_child_index(sel);
|
|
|
|
if (idx > 0 && idx < parent->get_child_count()) {
|
|
|
|
parent->remove_child(sel);
|
|
|
|
parent->add_child_at_index(sel, idx - 1);
|
|
|
|
task_tree->update_tree();
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
case ACTION_MOVE_DOWN: {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid() && sel->get_parent().is_valid()) {
|
|
|
|
Ref<BTTask> parent = sel->get_parent();
|
|
|
|
int idx = parent->get_child_index(sel);
|
|
|
|
if (idx >= 0 && idx < (parent->get_child_count() - 1)) {
|
|
|
|
parent->remove_child(sel);
|
|
|
|
parent->add_child_at_index(sel, idx + 1);
|
|
|
|
task_tree->update_tree();
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
case ACTION_DUPLICATE: {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid()) {
|
|
|
|
Ref<BTTask> parent = sel->get_parent();
|
|
|
|
if (parent.is_null()) {
|
|
|
|
parent = sel;
|
|
|
|
}
|
2022-09-03 12:11:03 +00:00
|
|
|
parent->add_child_at_index(sel->clone(), parent->get_child_index(sel) + 1);
|
2022-09-01 22:20:37 +00:00
|
|
|
task_tree->update_tree();
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
} break;
|
|
|
|
case ACTION_MAKE_ROOT: {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid() && task_tree->get_bt()->get_root_task() != sel) {
|
|
|
|
Ref<BTTask> parent = sel->get_parent();
|
|
|
|
ERR_FAIL_COND(parent.is_null());
|
|
|
|
parent->remove_child(sel);
|
|
|
|
Ref<BTTask> old_root = task_tree->get_bt()->get_root_task();
|
|
|
|
task_tree->get_bt()->set_root_task(sel);
|
|
|
|
sel->add_child(old_root);
|
|
|
|
task_tree->update_tree();
|
2022-09-03 12:01:13 +00:00
|
|
|
_mark_as_dirty(true);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-02 12:25:03 +00:00
|
|
|
void LimboAIEditor::_on_tree_task_selected(const Ref<BTTask> &p_task) const {
|
2022-09-01 22:20:37 +00:00
|
|
|
editor->edit_resource(p_task);
|
|
|
|
}
|
|
|
|
|
2022-09-02 22:10:22 +00:00
|
|
|
void LimboAIEditor::_on_panel_task_selected(String p_task) {
|
|
|
|
if (p_task.begins_with("res:")) {
|
|
|
|
Ref<Script> script = ResourceLoader::load(p_task, "Script");
|
2022-10-30 11:33:38 +00:00
|
|
|
ERR_FAIL_COND_MSG(script.is_null() || !script->is_valid(), vformat("LimboAI: Failed to instance task. Bad script: %s", p_task));
|
2022-09-02 22:10:22 +00:00
|
|
|
Variant inst = ClassDB::instance(script->get_instance_base_type());
|
2022-10-28 23:50:47 +00:00
|
|
|
ERR_FAIL_COND_MSG(inst.is_zero(), vformat("LimboAI: Failed to instance base type \"%s\".", script->get_instance_base_type()));
|
|
|
|
|
|
|
|
if (unlikely(!((Object *)inst)->is_class("BTTask"))) {
|
|
|
|
if (!inst.is_ref()) {
|
|
|
|
memdelete((Object *)inst);
|
|
|
|
}
|
|
|
|
ERR_PRINT(vformat("LimboAI: Failed to instance task. Script is not a BTTask: %s", p_task));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-09-02 22:10:22 +00:00
|
|
|
if (inst && script.is_valid()) {
|
|
|
|
((Object *)inst)->set_script(script.get_ref_ptr());
|
|
|
|
_add_task(Variant(inst));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_add_task(Ref<BTTask>(ClassDB::instance(p_task)));
|
|
|
|
}
|
2022-09-02 12:25:03 +00:00
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void LimboAIEditor::_on_visibility_changed() const {
|
|
|
|
if (is_visible()) {
|
|
|
|
Ref<BTTask> sel = task_tree->get_selected();
|
|
|
|
if (sel.is_valid()) {
|
|
|
|
editor->edit_resource(sel);
|
2022-09-05 19:57:24 +00:00
|
|
|
} else if (task_tree->get_bt().is_valid() && editor->get_inspector()->get_edited_object() != task_tree->get_bt().ptr()) {
|
2022-09-01 22:20:37 +00:00
|
|
|
editor->edit_resource(task_tree->get_bt());
|
|
|
|
}
|
2022-09-05 15:56:38 +00:00
|
|
|
|
|
|
|
task_panel->refresh();
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_on_header_pressed() const {
|
|
|
|
task_tree->deselect();
|
|
|
|
editor->edit_resource(task_tree->get_bt());
|
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_on_save_pressed() {
|
2022-09-06 11:08:27 +00:00
|
|
|
if (task_tree->get_bt().is_null()) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-01 22:20:37 +00:00
|
|
|
String path = task_tree->get_bt()->get_path();
|
|
|
|
if (path.empty()) {
|
|
|
|
save_dialog->popup_centered_ratio();
|
|
|
|
} else {
|
|
|
|
_save_bt(path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-02 13:43:54 +00:00
|
|
|
void LimboAIEditor::_on_history_back() {
|
|
|
|
idx_history = MAX(idx_history - 1, 0);
|
2022-09-05 19:57:24 +00:00
|
|
|
editor->edit_resource(history[idx_history]);
|
2022-09-02 13:43:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditor::_on_history_forward() {
|
|
|
|
idx_history = MIN(idx_history + 1, history.size() - 1);
|
2022-09-05 19:57:24 +00:00
|
|
|
editor->edit_resource(history[idx_history]);
|
2022-09-02 13:43:54 +00:00
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
void LimboAIEditor::_on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type) {
|
|
|
|
if (p_task == p_to_task) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (p_type == 0) {
|
|
|
|
p_task->get_parent()->remove_child(p_task);
|
|
|
|
p_to_task->add_child(p_task);
|
|
|
|
task_tree->update_tree();
|
|
|
|
_mark_as_dirty(true);
|
|
|
|
} else if (p_type == -1 && p_to_task->get_parent().is_valid()) {
|
|
|
|
p_task->get_parent()->remove_child(p_task);
|
|
|
|
p_to_task->get_parent()->add_child_at_index(p_task, p_to_task->get_parent()->get_child_index(p_to_task));
|
|
|
|
task_tree->update_tree();
|
|
|
|
_mark_as_dirty(true);
|
|
|
|
} else if (p_type == 1 && p_to_task->get_parent().is_valid()) {
|
|
|
|
p_task->get_parent()->remove_child(p_task);
|
|
|
|
p_to_task->get_parent()->add_child_at_index(p_task, p_to_task->get_parent()->get_child_index(p_to_task) + 1);
|
|
|
|
task_tree->update_tree();
|
|
|
|
_mark_as_dirty(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-03 12:01:13 +00:00
|
|
|
void LimboAIEditor::apply_changes() {
|
|
|
|
for (int i = 0; i < history.size(); i++) {
|
|
|
|
Ref<BehaviorTree> bt = history.get(i);
|
|
|
|
String path = bt->get_path();
|
|
|
|
if (ResourceLoader::exists(path)) {
|
|
|
|
ResourceSaver::save(path, bt);
|
|
|
|
}
|
|
|
|
dirty.clear();
|
|
|
|
_update_header();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-05 14:39:40 +00:00
|
|
|
Ref<Texture> LimboAIEditor::get_task_icon(String p_script_path_or_class) {
|
|
|
|
// TODO: Implement caching.
|
|
|
|
String base_type = p_script_path_or_class;
|
|
|
|
if (p_script_path_or_class.begins_with("res:")) {
|
|
|
|
Ref<Script> script = ResourceLoader::load(p_script_path_or_class, "Script");
|
|
|
|
Ref<Script> base_script = script;
|
|
|
|
while (base_script.is_valid()) {
|
|
|
|
StringName name = EditorNode::get_editor_data().script_class_get_name(base_script->get_path());
|
|
|
|
String icon_path = EditorNode::get_editor_data().script_class_get_icon_path(name);
|
|
|
|
if (!icon_path.empty()) {
|
|
|
|
Ref<Image> img = memnew(Image);
|
|
|
|
Error err = ImageLoader::load_image(icon_path, img);
|
|
|
|
if (err == OK) {
|
|
|
|
Ref<ImageTexture> icon = memnew(ImageTexture);
|
|
|
|
img->resize(16 * EDSCALE, 16 * EDSCALE, Image::INTERPOLATE_LANCZOS);
|
|
|
|
icon->create_from_image(img);
|
|
|
|
return icon;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
base_script = base_script->get_base_script();
|
|
|
|
}
|
|
|
|
base_type = script->get_instance_base_type();
|
|
|
|
}
|
|
|
|
|
2022-10-28 23:50:47 +00:00
|
|
|
// TODO: Walk inheritance tree until icon is found.
|
2022-09-05 14:39:40 +00:00
|
|
|
return EditorNode::get_singleton()->get_class_icon(base_type, "BTTask");
|
|
|
|
}
|
|
|
|
|
2022-09-06 11:28:25 +00:00
|
|
|
void LimboAIEditor::_notification(int p_what) {
|
|
|
|
switch (p_what) {
|
|
|
|
case NOTIFICATION_ENTER_TREE: {
|
|
|
|
ConfigFile conf;
|
|
|
|
String conf_path = EditorSettings::get_singleton()->get_project_settings_dir().plus_file("limbo_ai.cfg");
|
|
|
|
if (conf.load(conf_path) == OK) {
|
|
|
|
hsc->set_split_offset(conf.get_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset()));
|
|
|
|
}
|
|
|
|
} break;
|
|
|
|
case NOTIFICATION_EXIT_TREE: {
|
|
|
|
ConfigFile conf;
|
|
|
|
String conf_path = EditorSettings::get_singleton()->get_project_settings_dir().plus_file("limbo_ai.cfg");
|
|
|
|
conf.load(conf_path);
|
|
|
|
conf.set_value("bt_editor", "bteditor_hsplit", hsc->get_split_offset());
|
|
|
|
conf.save(conf_path);
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void LimboAIEditor::_bind_methods() {
|
|
|
|
ClassDB::bind_method(D_METHOD("_add_task", "p_task"), &LimboAIEditor::_add_task);
|
2022-09-03 12:11:47 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_add_task_with_prototype", "p_prototype"), &LimboAIEditor::_add_task_with_prototype);
|
2022-09-01 22:20:37 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_tree_rmb"), &LimboAIEditor::_on_tree_rmb);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_action_selected", "p_id"), &LimboAIEditor::_on_action_selected);
|
2022-09-02 12:25:03 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_tree_task_selected", "p_task"), &LimboAIEditor::_on_tree_task_selected);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_panel_task_selected", "p_task"), &LimboAIEditor::_on_panel_task_selected);
|
2022-09-01 22:20:37 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_visibility_changed"), &LimboAIEditor::_on_visibility_changed);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_header_pressed"), &LimboAIEditor::_on_header_pressed);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_save_pressed"), &LimboAIEditor::_on_save_pressed);
|
2022-09-02 13:43:54 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_history_back"), &LimboAIEditor::_on_history_back);
|
|
|
|
ClassDB::bind_method(D_METHOD("_on_history_forward"), &LimboAIEditor::_on_history_forward);
|
2022-09-03 15:00:20 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_on_task_dragged", "p_task", "p_to_task", "p_type"), &LimboAIEditor::_on_task_dragged);
|
2022-09-01 22:20:37 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("_new_bt"), &LimboAIEditor::_new_bt);
|
|
|
|
ClassDB::bind_method(D_METHOD("_save_bt", "p_path"), &LimboAIEditor::_save_bt);
|
|
|
|
ClassDB::bind_method(D_METHOD("_load_bt", "p_path"), &LimboAIEditor::_load_bt);
|
2022-09-05 19:57:24 +00:00
|
|
|
ClassDB::bind_method(D_METHOD("edit_bt", "p_behavior_tree"), &LimboAIEditor::edit_bt);
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
LimboAIEditor::LimboAIEditor(EditorNode *p_editor) {
|
|
|
|
editor = p_editor;
|
|
|
|
|
|
|
|
save_dialog = memnew(FileDialog);
|
|
|
|
add_child(save_dialog);
|
|
|
|
save_dialog->set_mode(FileDialog::MODE_SAVE_FILE);
|
|
|
|
save_dialog->set_title("Save Behavior Tree");
|
|
|
|
save_dialog->add_filter("*.tres");
|
|
|
|
save_dialog->connect("file_selected", this, "_save_bt");
|
|
|
|
save_dialog->hide();
|
|
|
|
|
|
|
|
load_dialog = memnew(FileDialog);
|
|
|
|
add_child(load_dialog);
|
|
|
|
load_dialog->set_mode(FileDialog::MODE_OPEN_FILE);
|
|
|
|
load_dialog->set_title("Load Behavior Tree");
|
|
|
|
load_dialog->add_filter("*.tres");
|
|
|
|
load_dialog->connect("file_selected", this, "_load_bt");
|
|
|
|
load_dialog->hide();
|
|
|
|
|
2022-09-02 12:25:03 +00:00
|
|
|
VBoxContainer *vb = memnew(VBoxContainer);
|
|
|
|
vb->set_anchor(MARGIN_RIGHT, ANCHOR_END);
|
|
|
|
vb->set_anchor(MARGIN_BOTTOM, ANCHOR_END);
|
|
|
|
add_child(vb);
|
2022-09-01 22:20:37 +00:00
|
|
|
|
|
|
|
HBoxContainer *panel = memnew(HBoxContainer);
|
2022-09-02 12:25:03 +00:00
|
|
|
vb->add_child(panel);
|
2022-09-01 22:20:37 +00:00
|
|
|
|
|
|
|
Button *selector_btn = memnew(Button);
|
|
|
|
selector_btn->set_text(TTR("Selector"));
|
|
|
|
selector_btn->set_tooltip(TTR("Add Selector task."));
|
|
|
|
selector_btn->set_icon(editor->get_class_icon("BTSelector"));
|
|
|
|
selector_btn->set_flat(true);
|
|
|
|
selector_btn->set_focus_mode(Control::FOCUS_NONE);
|
2022-09-03 12:11:47 +00:00
|
|
|
selector_btn->connect("pressed", this, "_add_task_with_prototype", varray(Ref<BTTask>(memnew(BTSelector))));
|
2022-09-01 22:20:37 +00:00
|
|
|
panel->add_child(selector_btn);
|
|
|
|
|
|
|
|
Button *sequence_btn = memnew(Button);
|
|
|
|
sequence_btn->set_text(TTR("Sequence"));
|
|
|
|
sequence_btn->set_tooltip(TTR("Add Sequence task."));
|
|
|
|
sequence_btn->set_icon(editor->get_class_icon("BTSequence"));
|
|
|
|
sequence_btn->set_flat(true);
|
|
|
|
sequence_btn->set_focus_mode(Control::FOCUS_NONE);
|
2022-09-03 12:11:47 +00:00
|
|
|
sequence_btn->connect("pressed", this, "_add_task_with_prototype", varray(Ref<BTTask>(memnew(BTSequence))));
|
2022-09-01 22:20:37 +00:00
|
|
|
panel->add_child(sequence_btn);
|
|
|
|
|
|
|
|
Button *parallel_btn = memnew(Button);
|
|
|
|
parallel_btn->set_text(TTR("Parallel"));
|
|
|
|
parallel_btn->set_tooltip(TTR("Add Parallel task."));
|
|
|
|
parallel_btn->set_icon(editor->get_class_icon("BTParallel"));
|
|
|
|
parallel_btn->set_flat(true);
|
|
|
|
parallel_btn->set_focus_mode(Control::FOCUS_NONE);
|
2022-09-03 12:11:47 +00:00
|
|
|
parallel_btn->connect("pressed", this, "_add_task_with_prototype", varray(Ref<BTTask>(memnew(BTParallel))));
|
2022-09-01 22:20:37 +00:00
|
|
|
panel->add_child(parallel_btn);
|
|
|
|
|
|
|
|
panel->add_child(memnew(VSeparator));
|
|
|
|
|
|
|
|
Button *new_btn = memnew(Button);
|
|
|
|
panel->add_child(new_btn);
|
|
|
|
new_btn->set_text(TTR("New"));
|
|
|
|
new_btn->set_tooltip(TTR("Create new behavior tree."));
|
|
|
|
new_btn->set_icon(editor->get_gui_base()->get_icon("New", "EditorIcons"));
|
|
|
|
new_btn->set_flat(true);
|
|
|
|
new_btn->set_focus_mode(Control::FOCUS_NONE);
|
|
|
|
new_btn->connect("pressed", this, "_new_bt");
|
|
|
|
|
|
|
|
Button *load_btn = memnew(Button);
|
|
|
|
panel->add_child(load_btn);
|
|
|
|
load_btn->set_text(TTR("Load"));
|
|
|
|
load_btn->set_tooltip(TTR("Load behavior tree."));
|
|
|
|
load_btn->set_icon(editor->get_gui_base()->get_icon("Load", "EditorIcons"));
|
|
|
|
load_btn->set_flat(true);
|
|
|
|
load_btn->set_focus_mode(Control::FOCUS_NONE);
|
|
|
|
load_btn->connect("pressed", load_dialog, "popup_centered_ratio");
|
|
|
|
|
|
|
|
Button *save_btn = memnew(Button);
|
|
|
|
panel->add_child(save_btn);
|
|
|
|
save_btn->set_text(TTR("Save"));
|
|
|
|
save_btn->set_tooltip(TTR("Save current behavior tree."));
|
|
|
|
save_btn->set_icon(editor->get_gui_base()->get_icon("Save", "EditorIcons"));
|
|
|
|
save_btn->set_flat(true);
|
|
|
|
save_btn->set_focus_mode(Control::FOCUS_NONE);
|
|
|
|
save_btn->connect("pressed", this, "_on_save_pressed");
|
|
|
|
|
|
|
|
panel->add_child(memnew(VSeparator));
|
|
|
|
|
2022-09-05 11:31:38 +00:00
|
|
|
Button *new_script_btn = memnew(Button);
|
|
|
|
panel->add_child(new_script_btn);
|
|
|
|
new_script_btn->set_text(TTR("New Task"));
|
|
|
|
new_script_btn->set_tooltip(TTR("Create new task script and edit it."));
|
|
|
|
new_script_btn->set_icon(editor->get_gui_base()->get_icon("ScriptCreate", "EditorIcons"));
|
|
|
|
new_script_btn->set_flat(true);
|
|
|
|
new_script_btn->set_focus_mode(Control::FOCUS_NONE);
|
|
|
|
|
2022-09-02 13:43:54 +00:00
|
|
|
HBoxContainer *nav = memnew(HBoxContainer);
|
|
|
|
panel->add_child(nav);
|
|
|
|
nav->set_h_size_flags(SIZE_EXPAND | SIZE_SHRINK_END);
|
|
|
|
|
|
|
|
history_back = memnew(Button);
|
|
|
|
history_back->set_icon(editor->get_gui_base()->get_icon("Back", "EditorIcons"));
|
|
|
|
history_back->set_flat(true);
|
|
|
|
history_back->set_focus_mode(FOCUS_NONE);
|
|
|
|
history_back->connect("pressed", this, "_on_history_back");
|
|
|
|
nav->add_child(history_back);
|
|
|
|
|
|
|
|
history_forward = memnew(Button);
|
|
|
|
history_forward->set_icon(editor->get_gui_base()->get_icon("Forward", "EditorIcons"));
|
|
|
|
history_forward->set_flat(true);
|
|
|
|
history_forward->set_focus_mode(FOCUS_NONE);
|
|
|
|
history_forward->connect("pressed", this, "_on_history_forward");
|
|
|
|
nav->add_child(history_forward);
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
header = memnew(Button);
|
2022-09-02 12:25:03 +00:00
|
|
|
vb->add_child(header);
|
2022-09-01 22:20:37 +00:00
|
|
|
header->set_text_align(Button::ALIGN_LEFT);
|
|
|
|
header->add_constant_override("hseparation", 8);
|
|
|
|
header->connect("pressed", this, "_on_header_pressed");
|
|
|
|
|
2022-09-06 11:28:25 +00:00
|
|
|
hsc = memnew(HSplitContainer);
|
2022-09-02 12:25:03 +00:00
|
|
|
vb->add_child(hsc);
|
|
|
|
hsc->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
hsc->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
task_tree = memnew(TaskTree);
|
2022-09-02 12:25:03 +00:00
|
|
|
hsc->add_child(task_tree);
|
2022-09-01 22:20:37 +00:00
|
|
|
task_tree->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
task_tree->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
task_tree->connect("rmb_pressed", this, "_on_tree_rmb");
|
2022-09-02 12:25:03 +00:00
|
|
|
task_tree->connect("task_selected", this, "_on_tree_task_selected");
|
2022-09-03 10:59:11 +00:00
|
|
|
task_tree->connect("visibility_changed", this, "_on_visibility_changed");
|
2022-09-03 15:00:20 +00:00
|
|
|
task_tree->connect("task_dragged", this, "_on_task_dragged");
|
2022-09-05 19:57:24 +00:00
|
|
|
task_tree->hide();
|
|
|
|
|
|
|
|
usage_hint = memnew(Panel);
|
|
|
|
usage_hint->set_v_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
usage_hint->set_h_size_flags(SIZE_EXPAND_FILL);
|
|
|
|
hsc->add_child(usage_hint);
|
|
|
|
Label *usage_label = memnew(Label);
|
|
|
|
usage_label->set_anchor(MARGIN_RIGHT, 1);
|
|
|
|
usage_label->set_anchor(MARGIN_BOTTOM, 1);
|
|
|
|
usage_label->set_align(Label::ALIGN_CENTER);
|
|
|
|
usage_label->set_valign(Label::VALIGN_CENTER);
|
|
|
|
usage_label->set_text(TTR("Create a new or load an existing behavior tree."));
|
|
|
|
usage_hint->add_child(usage_label);
|
2022-09-02 12:25:03 +00:00
|
|
|
|
2022-09-05 15:56:38 +00:00
|
|
|
task_panel = memnew(TaskPanel(p_editor));
|
2022-09-02 12:25:03 +00:00
|
|
|
hsc->add_child(task_panel);
|
2022-09-02 13:43:54 +00:00
|
|
|
hsc->set_split_offset(-300);
|
2022-09-02 12:25:03 +00:00
|
|
|
task_panel->connect("task_selected", this, "_on_panel_task_selected");
|
2022-09-05 19:57:24 +00:00
|
|
|
task_panel->hide();
|
2022-09-01 22:20:37 +00:00
|
|
|
|
|
|
|
menu = memnew(PopupMenu);
|
|
|
|
add_child(menu);
|
|
|
|
menu->connect("id_pressed", this, "_on_action_selected");
|
|
|
|
menu->set_hide_on_window_lose_focus(true);
|
|
|
|
|
2022-09-05 11:11:47 +00:00
|
|
|
GLOBAL_DEF("limbo_ai/behavior_tree/behavior_tree_default_dir", "res://ai/trees");
|
|
|
|
ProjectSettings::get_singleton()->set_custom_property_info("limbo_ai/behavior_tree/behavior_tree_default_dir",
|
|
|
|
PropertyInfo(Variant::STRING, "limbo_ai/behavior_tree/behavior_tree_default_dir", PROPERTY_HINT_DIR));
|
|
|
|
GLOBAL_DEF("limbo_ai/behavior_tree/user_task_dir_1", "res://ai/tasks");
|
|
|
|
ProjectSettings::get_singleton()->set_custom_property_info("limbo_ai/behavior_tree/user_task_dir_1",
|
|
|
|
PropertyInfo(Variant::STRING, "limbo_ai/behavior_tree/user_task_dir_1", PROPERTY_HINT_DIR));
|
|
|
|
GLOBAL_DEF("limbo_ai/behavior_tree/user_task_dir_2", "");
|
|
|
|
ProjectSettings::get_singleton()->set_custom_property_info("limbo_ai/behavior_tree/user_task_dir_2",
|
|
|
|
PropertyInfo(Variant::STRING, "limbo_ai/behavior_tree/user_task_dir_2", PROPERTY_HINT_DIR));
|
|
|
|
GLOBAL_DEF("limbo_ai/behavior_tree/user_task_dir_3", "");
|
|
|
|
ProjectSettings::get_singleton()->set_custom_property_info("limbo_ai/behavior_tree/user_task_dir_3",
|
|
|
|
PropertyInfo(Variant::STRING, "limbo_ai/behavior_tree/user_task_dir_3", PROPERTY_HINT_DIR));
|
|
|
|
|
|
|
|
save_dialog->set_current_dir(GLOBAL_GET("limbo_ai/behavior_tree/behavior_tree_default_dir"));
|
|
|
|
load_dialog->set_current_dir(GLOBAL_GET("limbo_ai/behavior_tree/behavior_tree_default_dir"));
|
2022-09-05 11:31:38 +00:00
|
|
|
new_script_btn->connect("pressed", ScriptEditor::get_singleton(), "open_script_create_dialog",
|
|
|
|
varray("BTAction", String(GLOBAL_GET("limbo_ai/behavior_tree/user_task_dir_1")).plus_file("new_task")));
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
LimboAIEditor::~LimboAIEditor() {
|
|
|
|
}
|
|
|
|
|
2022-09-03 15:00:20 +00:00
|
|
|
//////////////////////////// LimboAIEditor ///////////////////////////////////
|
|
|
|
|
|
|
|
///////////////////////// LimboAIEditorPlugin ////////////////////////////////
|
2022-09-01 22:20:37 +00:00
|
|
|
|
|
|
|
const Ref<Texture> LimboAIEditorPlugin::get_icon() const {
|
2022-10-19 19:02:46 +00:00
|
|
|
return editor->get_gui_base()->get_icon("LimboAIEditor", "EditorIcons");
|
2022-09-01 22:20:37 +00:00
|
|
|
}
|
|
|
|
|
2022-09-03 12:01:13 +00:00
|
|
|
void LimboAIEditorPlugin::apply_changes() {
|
|
|
|
limbo_ai_editor->apply_changes();
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
void LimboAIEditorPlugin::_notification(int p_notification) {
|
|
|
|
// print_line(vformat("NOTIFICATION: %d", p_notification));
|
|
|
|
}
|
|
|
|
|
|
|
|
void LimboAIEditorPlugin::make_visible(bool p_visible) {
|
|
|
|
limbo_ai_editor->set_visible(p_visible);
|
|
|
|
}
|
|
|
|
|
2022-09-05 19:57:24 +00:00
|
|
|
void LimboAIEditorPlugin::edit(Object *p_object) {
|
|
|
|
if (Object::cast_to<BehaviorTree>(p_object)) {
|
|
|
|
limbo_ai_editor->edit_bt(Object::cast_to<BehaviorTree>(p_object));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool LimboAIEditorPlugin::handles(Object *p_object) const {
|
|
|
|
return p_object->is_class("BehaviorTree");
|
|
|
|
}
|
|
|
|
|
2022-09-01 22:20:37 +00:00
|
|
|
LimboAIEditorPlugin::LimboAIEditorPlugin(EditorNode *p_editor) {
|
|
|
|
editor = p_editor;
|
|
|
|
limbo_ai_editor = memnew(LimboAIEditor(p_editor));
|
|
|
|
limbo_ai_editor->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
|
|
|
editor->get_viewport()->add_child(limbo_ai_editor);
|
|
|
|
limbo_ai_editor->hide();
|
|
|
|
}
|
|
|
|
|
|
|
|
LimboAIEditorPlugin::~LimboAIEditorPlugin() {
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // TOOLS_ENABLED
|