diff --git a/bt/tasks/bt_task.cpp b/bt/tasks/bt_task.cpp index 5748129..16d5c5d 100644 --- a/bt/tasks/bt_task.cpp +++ b/bt/tasks/bt_task.cpp @@ -76,15 +76,35 @@ Array BTTask::_get_children() const { } void BTTask::_set_children(Array p_children) { - data.children.clear(); const int num_children = p_children.size(); + int num_null = 0; + + data.children.clear(); data.children.resize(num_children); + for (int i = 0; i < num_children; i++) { - Variant task_var = p_children[i]; - Ref task_ref = task_var; - task_ref->data.parent = this; - task_ref->data.index = i; - data.children.set(i, task_var); + Ref task = p_children[i]; + if (task.is_null()) { + ERR_PRINT("Invalid BTTask reference."); + num_null += 1; + continue; + } + if (task->data.parent != nullptr && task->data.parent != this) { + task = task->clone(); + if (task.is_null()) { + // * BTComment::clone() returns nullptr at runtime - we omit those. + num_null += 1; + continue; + } + } + int idx = i - num_null; + task->data.parent = this; + task->data.index = idx; + data.children.set(idx, task); + } + + if (num_null > 0) { + data.children.resize(num_children - num_null); } } @@ -145,24 +165,8 @@ void BTTask::initialize(Node *p_agent, const Ref &p_blackboard) { Ref BTTask::clone() const { Ref inst = duplicate(false); - inst->data.parent = nullptr; - inst->data.agent = nullptr; - inst->data.blackboard.unref(); - int num_null = 0; - for (int i = 0; i < data.children.size(); i++) { - Ref c = get_child(i)->clone(); - if (c.is_valid()) { - c->data.parent = inst.ptr(); - c->data.index = i; - inst->data.children.set(i - num_null, c); - } else { - num_null += 1; - } - } - if (num_null > 0) { - // * BTComment tasks return nullptr at runtime - we remove those. - inst->data.children.resize(data.children.size() - num_null); - } + + // * Children are duplicated via children property. See _set_children(). #ifdef LIMBOAI_MODULE // Make BBParam properties unique. @@ -279,9 +283,9 @@ void BTTask::add_child_at_index(Ref p_child, int p_idx) { if (p_idx < 0 || p_idx > data.children.size()) { p_idx = data.children.size(); } - data.children.insert(p_idx, p_child); p_child->data.parent = this; p_child->data.index = p_idx; + data.children.insert(p_idx, p_child); for (int i = p_idx + 1; i < data.children.size(); i++) { get_child(i)->data.index = i; } diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 439a03e..9f7c88a 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -76,7 +76,7 @@ _FORCE_INLINE_ String _get_script_template_path() { return templates_search_path.path_join("BTTask").path_join("custom_task.gd"); } -void LimboAIEditor::_add_task(const Ref &p_task) { +void LimboAIEditor::_add_task(const Ref &p_task, bool p_as_sibling) { if (task_tree->get_bt().is_null()) { return; } @@ -98,8 +98,8 @@ void LimboAIEditor::_add_task(const Ref &p_task) { undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), p_task); undo_redo->add_undo_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), task_tree->get_bt()->get_root_task()); } else { - if (Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT)) && selected->get_parent().is_valid()) { - // When shift is pressed, insert task after the currently selected and on the same level. + if (p_as_sibling && selected.is_valid() && selected->get_parent().is_valid()) { + // Insert task after the currently selected and on the same level (usually when shift is pressed). parent = selected->get_parent(); insert_idx = selected->get_index() + 1; } @@ -113,6 +113,12 @@ void LimboAIEditor::_add_task(const Ref &p_task) { _mark_as_dirty(true); } +void LimboAIEditor::_add_task_with_prototype(const Ref &p_prototype) { + Ref selected = task_tree->get_selected(); + bool as_sibling = Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT)); + _add_task(p_prototype->clone(), as_sibling); +} + Ref LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const { Ref ret; @@ -139,7 +145,9 @@ Ref LimboAIEditor::_create_task_by_class_or_path(const String &p_class_o } void LimboAIEditor::_add_task_by_class_or_path(const String &p_class_or_path) { - _add_task(_create_task_by_class_or_path(p_class_or_path)); + Ref selected = task_tree->get_selected(); + bool as_sibling = Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT)); + _add_task(_create_task_by_class_or_path(p_class_or_path), as_sibling); } void LimboAIEditor::_remove_task(const Ref &p_task) { @@ -368,6 +376,14 @@ void LimboAIEditor::_process_shortcut_input(const Ref &p_event) { if (LW_IS_SHORTCUT("limbo_ai/rename_task", p_event)) { _action_selected(ACTION_RENAME); + } else if (LW_IS_SHORTCUT("limbo_ai/cut_task", p_event)) { + _action_selected(ACTION_CUT); + } else if (LW_IS_SHORTCUT("limbo_ai/copy_task", p_event)) { + _action_selected(ACTION_COPY); + } else if (LW_IS_SHORTCUT("limbo_ai/paste_task", p_event)) { + _action_selected(ACTION_PASTE); + } else if (LW_IS_SHORTCUT("limbo_ai/paste_task_after", p_event)) { + _action_selected(ACTION_PASTE_AFTER); } else if (LW_IS_SHORTCUT("limbo_ai/move_task_up", p_event)) { _action_selected(ACTION_MOVE_UP); } else if (LW_IS_SHORTCUT("limbo_ai/move_task_down", p_event)) { @@ -404,6 +420,14 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) { menu->add_icon_item(theme_cache.doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC); menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script() == Variant()); + menu->add_separator(); + menu->add_icon_shortcut(theme_cache.cut_icon, LW_GET_SHORTCUT("limbo_ai/cut_task"), ACTION_CUT); + menu->add_icon_shortcut(theme_cache.copy_icon, LW_GET_SHORTCUT("limbo_ai/copy_task"), ACTION_COPY); + menu->add_icon_shortcut(theme_cache.paste_icon, LW_GET_SHORTCUT("limbo_ai/paste_task"), ACTION_PASTE); + menu->add_icon_shortcut(theme_cache.paste_icon, LW_GET_SHORTCUT("limbo_ai/paste_task_after"), ACTION_PASTE_AFTER); + menu->set_item_disabled(ACTION_PASTE, clipboard_task.is_null()); + menu->set_item_disabled(ACTION_PASTE_AFTER, clipboard_task.is_null()); + menu->add_separator(); menu->add_icon_shortcut(theme_cache.move_task_up_icon, LW_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP); menu->add_icon_shortcut(theme_cache.move_task_down_icon, LW_GET_SHORTCUT("limbo_ai/move_task_down"), ACTION_MOVE_DOWN); @@ -476,6 +500,22 @@ void LimboAIEditor::_action_selected(int p_id) { LimboUtility::get_singleton()->open_doc_class(help_class); } break; + case ACTION_COPY: { + Ref sel = task_tree->get_selected(); + if (sel.is_valid()) { + clipboard_task = sel->clone(); + } + } break; + case ACTION_PASTE: { + if (clipboard_task.is_valid()) { + _add_task(clipboard_task->clone(), false); + } + } break; + case ACTION_PASTE_AFTER: { + if (clipboard_task.is_valid()) { + _add_task(clipboard_task->clone(), true); + } + } break; case ACTION_MOVE_UP: { Ref sel = task_tree->get_selected(); if (sel.is_valid() && sel->get_parent().is_valid()) { @@ -554,9 +594,14 @@ void LimboAIEditor::_action_selected(int p_id) { extract_dialog->popup_centered_ratio(); } } break; + case ACTION_CUT: case ACTION_REMOVE: { Ref sel = task_tree->get_selected(); if (sel.is_valid()) { + if (p_id == ACTION_CUT) { + clipboard_task = sel->clone(); + } + undo_redo->create_action(TTR("Remove BT Task")); if (sel->is_root()) { undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), Variant()); @@ -1050,6 +1095,9 @@ void LimboAIEditor::_do_update_theme_item_cache() { theme_cache.remove_task_icon = get_theme_icon(LW_NAME(Remove), LW_NAME(EditorIcons)); theme_cache.rename_task_icon = get_theme_icon(LW_NAME(Rename), LW_NAME(EditorIcons)); theme_cache.change_type_icon = get_theme_icon(LW_NAME(Reload), LW_NAME(EditorIcons)); + theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), LW_NAME(EditorIcons)); + theme_cache.copy_icon = get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons)); + theme_cache.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons)); theme_cache.behavior_tree_icon = LimboUtility::get_singleton()->get_task_icon("BehaviorTree"); theme_cache.percent_icon = LimboUtility::get_singleton()->get_task_icon("LimboPercent"); @@ -1161,6 +1209,10 @@ LimboAIEditor::LimboAIEditor() { LW_SHORTCUT("limbo_ai/move_task_down", TTR("Move Down"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(DOWN))); LW_SHORTCUT("limbo_ai/duplicate_task", TTR("Duplicate"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(D))); LW_SHORTCUT("limbo_ai/remove_task", TTR("Remove"), Key::KEY_DELETE); + LW_SHORTCUT("limbo_ai/cut_task", TTR("Cut"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(X))); + LW_SHORTCUT("limbo_ai/copy_task", TTR("Copy"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(C))); + LW_SHORTCUT("limbo_ai/paste_task", TTR("Paste"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(V))); + LW_SHORTCUT("limbo_ai/paste_task_after", TTR("Paste After Selected"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(SHIFT) | LW_KEY(V))); LW_SHORTCUT("limbo_ai/new_behavior_tree", TTR("New Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(N))); LW_SHORTCUT("limbo_ai/save_behavior_tree", TTR("Save Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(S))); diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h index a080e01..3b22d09 100644 --- a/editor/limbo_ai_editor_plugin.h +++ b/editor/limbo_ai_editor_plugin.h @@ -77,6 +77,10 @@ private: ACTION_CHANGE_TYPE, ACTION_EDIT_SCRIPT, ACTION_OPEN_DOC, + ACTION_CUT, + ACTION_COPY, + ACTION_PASTE, + ACTION_PASTE_AFTER, ACTION_MOVE_UP, ACTION_MOVE_DOWN, ACTION_DUPLICATE, @@ -109,12 +113,16 @@ private: Ref change_type_icon; Ref extract_subtree_icon; Ref behavior_tree_icon; + Ref cut_icon; + Ref copy_icon; + Ref paste_icon; } theme_cache; EditorPlugin *plugin; Vector> history; int idx_history; HashSet> dirty; + Ref clipboard_task; VBoxContainer *vbox; Button *header; @@ -155,11 +163,11 @@ private: AcceptDialog *info_dialog; - void _add_task(const Ref &p_task); + void _add_task(const Ref &p_task, bool p_as_sibling); + void _add_task_with_prototype(const Ref &p_prototype); Ref _create_task_by_class_or_path(const String &p_class_or_path) const; void _add_task_by_class_or_path(const String &p_class_or_path); void _remove_task(const Ref &p_task); - _FORCE_INLINE_ void _add_task_with_prototype(const Ref &p_prototype) { _add_task(p_prototype->clone()); } void _update_header() const; void _update_history_buttons(); void _update_favorite_tasks(); diff --git a/util/limbo_string_names.cpp b/util/limbo_string_names.cpp index 80b8596..097e008 100644 --- a/util/limbo_string_names.cpp +++ b/util/limbo_string_names.cpp @@ -42,6 +42,9 @@ LimboStringNames::LimboStringNames() { _update_banners = SN("_update_banners"); _weight_ = SN("_weight_"); accent_color = SN("accent_color"); + ActionCopy = SN("ActionCopy"); + ActionCut = SN("ActionCut"); + ActionPaste = SN("ActionPaste"); Add = SN("Add"); add_child = SN("add_child"); add_child_at_index = SN("add_child_at_index"); diff --git a/util/limbo_string_names.h b/util/limbo_string_names.h index f3188f9..35aeace 100644 --- a/util/limbo_string_names.h +++ b/util/limbo_string_names.h @@ -56,6 +56,9 @@ public: StringName _update; StringName _weight_; StringName accent_color; + StringName ActionCopy; + StringName ActionCut; + StringName ActionPaste; StringName add_child_at_index; StringName add_child; StringName Add;