parent
fca8aea99c
commit
d38e6f97f1
|
@ -76,15 +76,35 @@ Array BTTask::_get_children() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void BTTask::_set_children(Array p_children) {
|
void BTTask::_set_children(Array p_children) {
|
||||||
data.children.clear();
|
|
||||||
const int num_children = p_children.size();
|
const int num_children = p_children.size();
|
||||||
|
int num_null = 0;
|
||||||
|
|
||||||
|
data.children.clear();
|
||||||
data.children.resize(num_children);
|
data.children.resize(num_children);
|
||||||
|
|
||||||
for (int i = 0; i < num_children; i++) {
|
for (int i = 0; i < num_children; i++) {
|
||||||
Variant task_var = p_children[i];
|
Ref<BTTask> task = p_children[i];
|
||||||
Ref<BTTask> task_ref = task_var;
|
if (task.is_null()) {
|
||||||
task_ref->data.parent = this;
|
ERR_PRINT("Invalid BTTask reference.");
|
||||||
task_ref->data.index = i;
|
num_null += 1;
|
||||||
data.children.set(i, task_var);
|
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<Blackboard> &p_blackboard) {
|
||||||
|
|
||||||
Ref<BTTask> BTTask::clone() const {
|
Ref<BTTask> BTTask::clone() const {
|
||||||
Ref<BTTask> inst = duplicate(false);
|
Ref<BTTask> inst = duplicate(false);
|
||||||
inst->data.parent = nullptr;
|
|
||||||
inst->data.agent = nullptr;
|
// * Children are duplicated via children property. See _set_children().
|
||||||
inst->data.blackboard.unref();
|
|
||||||
int num_null = 0;
|
|
||||||
for (int i = 0; i < data.children.size(); i++) {
|
|
||||||
Ref<BTTask> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef LIMBOAI_MODULE
|
#ifdef LIMBOAI_MODULE
|
||||||
// Make BBParam properties unique.
|
// Make BBParam properties unique.
|
||||||
|
@ -279,9 +283,9 @@ void BTTask::add_child_at_index(Ref<BTTask> p_child, int p_idx) {
|
||||||
if (p_idx < 0 || p_idx > data.children.size()) {
|
if (p_idx < 0 || p_idx > data.children.size()) {
|
||||||
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.parent = this;
|
||||||
p_child->data.index = p_idx;
|
p_child->data.index = p_idx;
|
||||||
|
data.children.insert(p_idx, p_child);
|
||||||
for (int i = p_idx + 1; i < data.children.size(); i++) {
|
for (int i = p_idx + 1; i < data.children.size(); i++) {
|
||||||
get_child(i)->data.index = i;
|
get_child(i)->data.index = i;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ _FORCE_INLINE_ String _get_script_template_path() {
|
||||||
return templates_search_path.path_join("BTTask").path_join("custom_task.gd");
|
return templates_search_path.path_join("BTTask").path_join("custom_task.gd");
|
||||||
}
|
}
|
||||||
|
|
||||||
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
|
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task, bool p_as_sibling) {
|
||||||
if (task_tree->get_bt().is_null()) {
|
if (task_tree->get_bt().is_null()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,8 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
|
||||||
undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), 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());
|
undo_redo->add_undo_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), task_tree->get_bt()->get_root_task());
|
||||||
} else {
|
} else {
|
||||||
if (Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT)) && selected->get_parent().is_valid()) {
|
if (p_as_sibling && selected.is_valid() && selected->get_parent().is_valid()) {
|
||||||
// When shift is pressed, insert task after the currently selected and on the same level.
|
// Insert task after the currently selected and on the same level (usually when shift is pressed).
|
||||||
parent = selected->get_parent();
|
parent = selected->get_parent();
|
||||||
insert_idx = selected->get_index() + 1;
|
insert_idx = selected->get_index() + 1;
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,12 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
|
||||||
_mark_as_dirty(true);
|
_mark_as_dirty(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LimboAIEditor::_add_task_with_prototype(const Ref<BTTask> &p_prototype) {
|
||||||
|
Ref<BTTask> 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<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const {
|
Ref<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const {
|
||||||
Ref<BTTask> ret;
|
Ref<BTTask> ret;
|
||||||
|
|
||||||
|
@ -139,7 +145,9 @@ Ref<BTTask> 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) {
|
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<BTTask> 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<BTTask> &p_task) {
|
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
|
||||||
|
@ -368,6 +376,14 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
|
||||||
|
|
||||||
if (LW_IS_SHORTCUT("limbo_ai/rename_task", p_event)) {
|
if (LW_IS_SHORTCUT("limbo_ai/rename_task", p_event)) {
|
||||||
_action_selected(ACTION_RENAME);
|
_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)) {
|
} else if (LW_IS_SHORTCUT("limbo_ai/move_task_up", p_event)) {
|
||||||
_action_selected(ACTION_MOVE_UP);
|
_action_selected(ACTION_MOVE_UP);
|
||||||
} else if (LW_IS_SHORTCUT("limbo_ai/move_task_down", p_event)) {
|
} 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->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->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_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_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);
|
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);
|
LimboUtility::get_singleton()->open_doc_class(help_class);
|
||||||
} break;
|
} break;
|
||||||
|
case ACTION_COPY: {
|
||||||
|
Ref<BTTask> 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: {
|
case ACTION_MOVE_UP: {
|
||||||
Ref<BTTask> sel = task_tree->get_selected();
|
Ref<BTTask> sel = task_tree->get_selected();
|
||||||
if (sel.is_valid() && sel->get_parent().is_valid()) {
|
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();
|
extract_dialog->popup_centered_ratio();
|
||||||
}
|
}
|
||||||
} break;
|
} break;
|
||||||
|
case ACTION_CUT:
|
||||||
case ACTION_REMOVE: {
|
case ACTION_REMOVE: {
|
||||||
Ref<BTTask> sel = task_tree->get_selected();
|
Ref<BTTask> sel = task_tree->get_selected();
|
||||||
if (sel.is_valid()) {
|
if (sel.is_valid()) {
|
||||||
|
if (p_id == ACTION_CUT) {
|
||||||
|
clipboard_task = sel->clone();
|
||||||
|
}
|
||||||
|
|
||||||
undo_redo->create_action(TTR("Remove BT Task"));
|
undo_redo->create_action(TTR("Remove BT Task"));
|
||||||
if (sel->is_root()) {
|
if (sel->is_root()) {
|
||||||
undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), Variant());
|
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.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.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.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.behavior_tree_icon = LimboUtility::get_singleton()->get_task_icon("BehaviorTree");
|
||||||
theme_cache.percent_icon = LimboUtility::get_singleton()->get_task_icon("LimboPercent");
|
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/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/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/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/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)));
|
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)));
|
||||||
|
|
|
@ -77,6 +77,10 @@ private:
|
||||||
ACTION_CHANGE_TYPE,
|
ACTION_CHANGE_TYPE,
|
||||||
ACTION_EDIT_SCRIPT,
|
ACTION_EDIT_SCRIPT,
|
||||||
ACTION_OPEN_DOC,
|
ACTION_OPEN_DOC,
|
||||||
|
ACTION_CUT,
|
||||||
|
ACTION_COPY,
|
||||||
|
ACTION_PASTE,
|
||||||
|
ACTION_PASTE_AFTER,
|
||||||
ACTION_MOVE_UP,
|
ACTION_MOVE_UP,
|
||||||
ACTION_MOVE_DOWN,
|
ACTION_MOVE_DOWN,
|
||||||
ACTION_DUPLICATE,
|
ACTION_DUPLICATE,
|
||||||
|
@ -109,12 +113,16 @@ private:
|
||||||
Ref<Texture2D> change_type_icon;
|
Ref<Texture2D> change_type_icon;
|
||||||
Ref<Texture2D> extract_subtree_icon;
|
Ref<Texture2D> extract_subtree_icon;
|
||||||
Ref<Texture2D> behavior_tree_icon;
|
Ref<Texture2D> behavior_tree_icon;
|
||||||
|
Ref<Texture2D> cut_icon;
|
||||||
|
Ref<Texture2D> copy_icon;
|
||||||
|
Ref<Texture2D> paste_icon;
|
||||||
} theme_cache;
|
} theme_cache;
|
||||||
|
|
||||||
EditorPlugin *plugin;
|
EditorPlugin *plugin;
|
||||||
Vector<Ref<BehaviorTree>> history;
|
Vector<Ref<BehaviorTree>> history;
|
||||||
int idx_history;
|
int idx_history;
|
||||||
HashSet<Ref<BehaviorTree>> dirty;
|
HashSet<Ref<BehaviorTree>> dirty;
|
||||||
|
Ref<BTTask> clipboard_task;
|
||||||
|
|
||||||
VBoxContainer *vbox;
|
VBoxContainer *vbox;
|
||||||
Button *header;
|
Button *header;
|
||||||
|
@ -155,11 +163,11 @@ private:
|
||||||
|
|
||||||
AcceptDialog *info_dialog;
|
AcceptDialog *info_dialog;
|
||||||
|
|
||||||
void _add_task(const Ref<BTTask> &p_task);
|
void _add_task(const Ref<BTTask> &p_task, bool p_as_sibling);
|
||||||
|
void _add_task_with_prototype(const Ref<BTTask> &p_prototype);
|
||||||
Ref<BTTask> _create_task_by_class_or_path(const String &p_class_or_path) const;
|
Ref<BTTask> _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 _add_task_by_class_or_path(const String &p_class_or_path);
|
||||||
void _remove_task(const Ref<BTTask> &p_task);
|
void _remove_task(const Ref<BTTask> &p_task);
|
||||||
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
|
|
||||||
void _update_header() const;
|
void _update_header() const;
|
||||||
void _update_history_buttons();
|
void _update_history_buttons();
|
||||||
void _update_favorite_tasks();
|
void _update_favorite_tasks();
|
||||||
|
|
|
@ -42,6 +42,9 @@ LimboStringNames::LimboStringNames() {
|
||||||
_update_banners = SN("_update_banners");
|
_update_banners = SN("_update_banners");
|
||||||
_weight_ = SN("_weight_");
|
_weight_ = SN("_weight_");
|
||||||
accent_color = SN("accent_color");
|
accent_color = SN("accent_color");
|
||||||
|
ActionCopy = SN("ActionCopy");
|
||||||
|
ActionCut = SN("ActionCut");
|
||||||
|
ActionPaste = SN("ActionPaste");
|
||||||
Add = SN("Add");
|
Add = SN("Add");
|
||||||
add_child = SN("add_child");
|
add_child = SN("add_child");
|
||||||
add_child_at_index = SN("add_child_at_index");
|
add_child_at_index = SN("add_child_at_index");
|
||||||
|
|
|
@ -56,6 +56,9 @@ public:
|
||||||
StringName _update;
|
StringName _update;
|
||||||
StringName _weight_;
|
StringName _weight_;
|
||||||
StringName accent_color;
|
StringName accent_color;
|
||||||
|
StringName ActionCopy;
|
||||||
|
StringName ActionCut;
|
||||||
|
StringName ActionPaste;
|
||||||
StringName add_child_at_index;
|
StringName add_child_at_index;
|
||||||
StringName add_child;
|
StringName add_child;
|
||||||
StringName Add;
|
StringName Add;
|
||||||
|
|
Loading…
Reference in New Issue