Compare commits

..

25 Commits

Author SHA1 Message Date
Wilson E. Alvarez a00eb6df69
Fix forbidden comparisons between Ref and nullptr.
Necessary when compiling with strict_checks=yes.

Due to upstream change:

	df29cc696f
2024-11-15 15:57:26 -05:00
Wilson E. Alvarez 70994acd15
Fix internal Button set_icon calls to set_button_icon
Due to upstream change:

    562c666e3d
2024-11-15 15:57:23 -05:00
Wilson E. Alvarez 9a869115b4
Fix unhandled tool button property hint
Due to upstream change:

	85dfd89653
2024-11-15 15:54:55 -05:00
Wilson E. Alvarez 364620fee0
Fix unhandled dictionary property hint
Due to upstream change:

	9853a69144
2024-11-15 15:54:55 -05:00
Wilson E. Alvarez 63544281e5
Update EditorMainScreen calls after its extraction
Due to upstream change:

	5e1c9d68aa
2024-11-15 15:54:55 -05:00
Serhii Snitsaruk ba90deaa6a
Merge pull request #245 from limbonaut/editor-prop-binding
Feature: Bind `BlackboardPlan` variables to node properties in the Inspector
2024-11-15 11:35:13 -08:00
Serhii Snitsaruk 08d8fcdf92
Don't show binding/mapping for hidden variables 2024-11-15 18:50:42 +01:00
Serhii Snitsaruk 51878e4b6e
Limit a variable to either binding or mapping, but not both at once 2024-11-15 18:15:40 +01:00
Serhii Snitsaruk 008702b1e4
Fix template compilation 2024-11-11 11:43:26 +01:00
Serhii Snitsaruk 6e8c22d598
Clean up & strengthen code 2024-11-11 10:24:14 +01:00
Serhii Snitsaruk 0b3b11a383
Enable binding in BehaviorTree-owned blackboard plan 2024-11-11 10:13:41 +01:00
Serhii Snitsaruk 4cac6276aa
Fix GDExtension issues 2024-11-11 10:01:20 +01:00
Serhii Snitsaruk ea671dd54b
Improve binding presentation within inspector, fix compilation issues 2024-11-11 09:49:01 +01:00
Serhii Snitsaruk bc7f677810
Specialized inspector property editor for NodePath 2024-11-11 08:24:54 +01:00
Serhii Snitsaruk 057d4e669c
Bind variables to scene node properties upon runtime Blackboard creation 2024-11-10 19:58:35 +01:00
Serhii Snitsaruk f600d633a3
Store property bindings in BlackboardPlan 2024-11-10 19:08:56 +01:00
Serhii Snitsaruk ae61d551c0
Doc: Clarify when `LimboState::_exit` is called 2024-11-05 19:41:16 +01:00
Serhii Snitsaruk a14cc4cc68
GHA: Use 4.3-stable in "All builds" 2024-11-05 19:16:41 +01:00
Serhii Snitsaruk d304f957ef
Merge pull request #244 from limbonaut/save-builtin-bt-on-ctrls
Improve workflow with built-in BT resources
2024-11-05 01:40:51 -08:00
Serhii Snitsaruk bf85350260
Open the owner scene when switching to a built-in BT 2024-11-04 19:45:54 +01:00
Serhii Snitsaruk 03476721d9
Change shortcut for "Jump to Owner"
Ctrl+J is no longer available by default
2024-11-04 15:52:42 +01:00
Serhii Snitsaruk bebd6a15eb
Save edited built-in BT resource on `Ctrl+S` 2024-11-04 14:52:35 +01:00
Serhii Snitsaruk 4c9028fc66
Merge pull request #243 from limbonaut/fix-builtin-resource-save
Improve saving for built-in BTs attached to scenes
2024-11-03 01:59:52 -08:00
Serhii Snitsaruk e21156df35
Save external BTs on plugin callback
Also fixes dirty state handling, however it's currently unused.
2024-11-02 15:41:34 +01:00
Serhii Snitsaruk 591a1fe672
Improve saving for built-in BTs attached to scenes 2024-11-01 22:22:23 +01:00
15 changed files with 644 additions and 95 deletions

View File

@ -5,7 +5,7 @@ on:
godot-ref: godot-ref:
description: A tag, branch or commit hash in the Godot repository. description: A tag, branch or commit hash in the Godot repository.
type: string type: string
default: 4.3 default: 4.3-stable
limboai-ref: limboai-ref:
description: A tag, branch or commit hash in the LimboAI repository. description: A tag, branch or commit hash in the LimboAI repository.
type: string type: string

View File

@ -13,9 +13,20 @@
#include "../util/limbo_utility.h" #include "../util/limbo_utility.h"
#ifdef LIMBOAI_MODULE
#include "editor/editor_inspector.h"
#include "editor/editor_interface.h"
#elif LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_inspector.hpp>
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#endif
bool BlackboardPlan::_set(const StringName &p_name, const Variant &p_value) { bool BlackboardPlan::_set(const StringName &p_name, const Variant &p_value) {
String name_str = p_name; String name_str = p_name;
#ifdef TOOLS_ENABLED
// * Editor // * Editor
if (var_map.has(p_name)) { if (var_map.has(p_name)) {
BBVariable &var = var_map[p_name]; BBVariable &var = var_map[p_name];
@ -26,29 +37,51 @@ bool BlackboardPlan::_set(const StringName &p_name, const Variant &p_value) {
} }
return true; return true;
} }
#endif // TOOLS_ENABLED
// * Mapping // * Mapping
if (name_str.begins_with("mapping/")) { if (name_str.begins_with("mapping/")) {
StringName mapped_var_name = name_str.get_slicec('/', 1); StringName mapped_var_name = name_str.get_slicec('/', 1);
StringName value = p_value; StringName value = p_value;
bool properties_changed = false; bool prop_list_changed = false;
if (value == StringName()) { if (value == StringName()) {
if (parent_scope_mapping.has(mapped_var_name)) { if (parent_scope_mapping.has(mapped_var_name)) {
properties_changed = true; prop_list_changed = true;
parent_scope_mapping.erase(mapped_var_name); parent_scope_mapping.erase(mapped_var_name);
} }
} else { } else {
if (!parent_scope_mapping.has(mapped_var_name)) { if (!parent_scope_mapping.has(mapped_var_name)) {
properties_changed = true; prop_list_changed = true;
} }
parent_scope_mapping[mapped_var_name] = value; parent_scope_mapping[mapped_var_name] = value;
} }
if (properties_changed) { if (prop_list_changed) {
notify_property_list_changed(); notify_property_list_changed();
} }
return true; return true;
} }
// * Binding
if (name_str.begins_with("binding/")) {
StringName bound_var = name_str.get_slicec('/', 1);
NodePath value = p_value;
bool prop_list_changed = false;
if (value.is_empty()) {
if (property_bindings.has(bound_var)) {
prop_list_changed = true;
property_bindings.erase(bound_var);
}
} else {
if (!property_bindings.has(bound_var)) {
prop_list_changed = true;
}
property_bindings[bound_var] = value;
}
if (prop_list_changed) {
notify_property_list_changed();
}
}
// * Storage // * Storage
if (name_str.begins_with("var/")) { if (name_str.begins_with("var/")) {
StringName var_name = name_str.get_slicec('/', 1); StringName var_name = name_str.get_slicec('/', 1);
@ -66,6 +99,8 @@ bool BlackboardPlan::_set(const StringName &p_name, const Variant &p_value) {
var_map[var_name].set_hint((PropertyHint)(int)p_value); var_map[var_name].set_hint((PropertyHint)(int)p_value);
} else if (what == "hint_string") { } else if (what == "hint_string") {
var_map[var_name].set_hint_string(p_value); var_map[var_name].set_hint_string(p_value);
} else if (what == "property_binding") {
property_bindings[var_name] = NodePath(p_value);
} else { } else {
return false; return false;
} }
@ -78,28 +113,64 @@ bool BlackboardPlan::_set(const StringName &p_name, const Variant &p_value) {
bool BlackboardPlan::_get(const StringName &p_name, Variant &r_ret) const { bool BlackboardPlan::_get(const StringName &p_name, Variant &r_ret) const {
String name_str = p_name; String name_str = p_name;
#ifdef TOOLS_ENABLED
// * Editor // * Editor
if (var_map.has(p_name)) { if (var_map.has(p_name)) {
if (has_mapping(p_name)) { if (has_mapping(p_name)) {
r_ret = "Mapped to " + LimboUtility::get_singleton()->decorate_var(parent_scope_mapping[p_name]); r_ret = "Mapped to " + LimboUtility::get_singleton()->decorate_var(parent_scope_mapping[p_name]);
} else if (has_property_binding(p_name)) {
const NodePath &binding = property_bindings[p_name];
Node *edited_node = Object::cast_to<Node>(EditorInterface::get_singleton()->get_inspector()->get_edited_object());
if (!edited_node) {
edited_node = SCENE_TREE()->get_edited_scene_root();
}
Node *bound_node = edited_node ? edited_node->get_node_or_null(binding) : nullptr;
String shortened_path;
if (bound_node) {
shortened_path = (String)bound_node->get_name() +
":" + (String)binding.get_concatenated_subnames();
} else {
shortened_path = (String)binding.get_name(binding.get_name_count() - 1) +
":" + (String)binding.get_concatenated_subnames();
}
r_ret = String::utf8("🔗 ") + shortened_path;
} else { } else {
r_ret = var_map[p_name].get_value(); r_ret = var_map[p_name].get_value();
} }
return true; return true;
} }
#endif // TOOLS_ENABLED
// * Mapping // * Mapping
if (name_str.begins_with("mapping/")) { if (name_str.begins_with("mapping/")) {
StringName mapped_var_name = name_str.get_slicec('/', 1); StringName mapped_var_name = name_str.get_slicec('/', 1);
ERR_FAIL_COND_V(mapped_var_name == StringName(), false); ERR_FAIL_COND_V(mapped_var_name == StringName(), false);
if (parent_scope_mapping.has(mapped_var_name)) { if (has_mapping(mapped_var_name)) {
r_ret = parent_scope_mapping[mapped_var_name]; r_ret = parent_scope_mapping[mapped_var_name];
} else if (has_property_binding(mapped_var_name)) {
r_ret = RTR("Already bound to property.");
} else { } else {
r_ret = StringName(); r_ret = StringName();
} }
return true; return true;
} }
// * Binding
if (name_str.begins_with("binding/")) {
StringName bound_var = name_str.get_slicec('/', 1);
ERR_FAIL_COND_V(bound_var == StringName(), false);
if (has_property_binding(bound_var)) {
r_ret = property_bindings[bound_var];
} else if (has_mapping(bound_var)) {
r_ret = RTR("Already mapped to variable.");
} else {
r_ret = NodePath();
}
return true;
}
// * Storage // * Storage
if (!name_str.begins_with("var/")) { if (!name_str.begins_with("var/")) {
return false; return false;
@ -127,14 +198,16 @@ void BlackboardPlan::_get_property_list(List<PropertyInfo> *p_list) const {
String var_name = p.first; String var_name = p.first;
BBVariable var = p.second; BBVariable var = p.second;
#ifdef TOOLS_ENABLED
// * Editor // * Editor
if (var.get_type() != Variant::NIL && (!is_derived() || !var_name.begins_with("_"))) { if (!_is_var_hidden(var_name, var)) {
if (has_mapping(var_name)) { if (has_mapping(var_name) || has_property_binding(var_name)) {
p_list->push_back(PropertyInfo(Variant::STRING, var_name, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)); p_list->push_back(PropertyInfo(Variant::STRING, var_name, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY));
} else { } else {
p_list->push_back(PropertyInfo(var.get_type(), var_name, var.get_hint(), var.get_hint_string(), PROPERTY_USAGE_EDITOR)); p_list->push_back(PropertyInfo(var.get_type(), var_name, var.get_hint(), var.get_hint_string(), PROPERTY_USAGE_EDITOR));
} }
} }
#endif // TOOLS_ENABLED
// * Storage // * Storage
if (is_derived() && (!var.is_value_changed() || var.get_value() == base->var_map[var_name].get_value())) { if (is_derived() && (!var.is_value_changed() || var.get_value() == base->var_map[var_name].get_value())) {
@ -153,6 +226,12 @@ void BlackboardPlan::_get_property_list(List<PropertyInfo> *p_list) const {
if (is_mapping_enabled()) { if (is_mapping_enabled()) {
p_list->push_back(PropertyInfo(Variant::NIL, "Mapping", PROPERTY_HINT_NONE, "mapping/", PROPERTY_USAGE_GROUP)); p_list->push_back(PropertyInfo(Variant::NIL, "Mapping", PROPERTY_HINT_NONE, "mapping/", PROPERTY_USAGE_GROUP));
for (const Pair<StringName, BBVariable> &p : var_list) { for (const Pair<StringName, BBVariable> &p : var_list) {
if (_is_var_hidden(p.first, p.second)) {
continue;
}
if (unlikely(has_property_binding(p.first))) {
p_list->push_back(PropertyInfo(Variant::STRING, "mapping/" + p.first, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY));
} else {
// Serialize only non-empty mappings. // Serialize only non-empty mappings.
PropertyUsageFlags usage = has_mapping(p.first) ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_EDITOR; PropertyUsageFlags usage = has_mapping(p.first) ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_EDITOR;
p_list->push_back(PropertyInfo(Variant::STRING_NAME, "mapping/" + p.first, PROPERTY_HINT_NONE, "", usage)); p_list->push_back(PropertyInfo(Variant::STRING_NAME, "mapping/" + p.first, PROPERTY_HINT_NONE, "", usage));
@ -160,6 +239,23 @@ void BlackboardPlan::_get_property_list(List<PropertyInfo> *p_list) const {
} }
} }
// * Binding
p_list->push_back(PropertyInfo(Variant::NIL, "Binding", PROPERTY_HINT_NONE, "binding/", PROPERTY_USAGE_GROUP));
for (const Pair<StringName, BBVariable> &p : var_list) {
if (_is_var_hidden(p.first, p.second)) {
continue;
}
if (unlikely(has_mapping(p.first))) {
p_list->push_back(PropertyInfo(Variant::STRING, "binding/" + p.first, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY));
} else {
PropertyUsageFlags usage = has_property_binding(p.first) ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_EDITOR;
// PROPERTY_HINT_LINK is used to signal that NodePath should point to a property.
// Our inspector plugin will know how to handle it.
p_list->push_back(PropertyInfo(Variant::NODE_PATH, "binding/" + p.first, PROPERTY_HINT_LINK, itos(p.second.get_type()), usage));
}
}
}
bool BlackboardPlan::_property_can_revert(const StringName &p_name) const { bool BlackboardPlan::_property_can_revert(const StringName &p_name) const {
if (String(p_name).begins_with("mapping/")) { if (String(p_name).begins_with("mapping/")) {
return true; return true;
@ -199,6 +295,11 @@ bool BlackboardPlan::has_mapping(const StringName &p_name) const {
return is_mapping_enabled() && parent_scope_mapping.has(p_name) && parent_scope_mapping[p_name] != StringName(); return is_mapping_enabled() && parent_scope_mapping.has(p_name) && parent_scope_mapping[p_name] != StringName();
} }
void BlackboardPlan::set_property_binding(const StringName &p_name, const NodePath &p_path) {
property_bindings[p_name] = p_path;
emit_changed();
}
void BlackboardPlan::set_prefetch_nodepath_vars(bool p_enable) { void BlackboardPlan::set_prefetch_nodepath_vars(bool p_enable) {
prefetch_nodepath_vars = p_enable; prefetch_nodepath_vars = p_enable;
emit_changed(); emit_changed();
@ -410,8 +511,9 @@ void BlackboardPlan::populate_blackboard(const Ref<Blackboard> &p_blackboard, bo
#endif #endif
continue; continue;
} }
bool is_bound = has_property_binding(p.first) || (is_derived() && get_base_plan()->has_property_binding(p.first));
bool has_mapping = parent_scope_mapping.has(p.first); bool has_mapping = parent_scope_mapping.has(p.first);
bool do_prefetch = !has_mapping && prefetch_nodepath_vars; bool do_prefetch = !is_bound && !has_mapping && prefetch_nodepath_vars;
// Add a variable duplicate to the blackboard, optionally with NodePath prefetch. // Add a variable duplicate to the blackboard, optionally with NodePath prefetch.
BBVariable var = p.second.duplicate(true); BBVariable var = p.second.duplicate(true);
@ -433,6 +535,24 @@ void BlackboardPlan::populate_blackboard(const Ref<Blackboard> &p_blackboard, bo
ERR_CONTINUE_MSG(p_blackboard->get_parent().is_null(), vformat("BlackboardPlan: Cannot link variable %s to parent scope because the parent scope is not set.", LimboUtility::get_singleton()->decorate_var(p.first))); ERR_CONTINUE_MSG(p_blackboard->get_parent().is_null(), vformat("BlackboardPlan: Cannot link variable %s to parent scope because the parent scope is not set.", LimboUtility::get_singleton()->decorate_var(p.first)));
p_blackboard->link_var(p.first, p_blackboard->get_parent(), target_var); p_blackboard->link_var(p.first, p_blackboard->get_parent(), target_var);
} }
} else if (is_bound) {
// Bind variable to a property of a scene node.
NodePath binding_path;
Node *binding_root;
if (has_property_binding(p.first)) {
binding_path = property_bindings[p.first];
binding_root = p_prefetch_root;
} else {
binding_path = get_base_plan()->property_bindings[p.first];
binding_root = p_prefetch_root_for_base_plan;
}
ERR_CONTINUE_MSG(binding_path.get_subname_count() != 1, vformat("BlackboardPlan: Can't bind variable %s using property path that contains multiple sub-names: %s", LimboUtility::get_singleton()->decorate_var(p.first), binding_path));
NodePath node_path{ binding_path.get_concatenated_names() };
StringName prop_name = binding_path.get_subname(0);
// TODO: Implement binding for base plan as well.
Node *n = binding_root->get_node_or_null(node_path);
ERR_CONTINUE_MSG(n == nullptr, vformat("BlackboardPlan: Binding failed for variable %s using property path: %s", LimboUtility::get_singleton()->decorate_var(p.first), binding_path));
var.bind(n, prop_name);
} }
} }
} }

View File

@ -34,18 +34,25 @@ private:
// When base is not null, the plan is considered to be derived from the base plan. // When base is not null, the plan is considered to be derived from the base plan.
// A derived plan can only have variables that exist in the base plan, // A derived plan can only have variables that exist in the base plan,
// and only the values can be different in those variables. // and only the values can be different in those variables.
// The derived plan is synced with the base plan to maintain consistency.
Ref<BlackboardPlan> base; Ref<BlackboardPlan> base;
// Mapping between variables in this plan and their parent scope names. // Mapping between variables in this plan and their parent scope names.
// Used for linking variables to their parent scope counterparts upon Blackboard creation/population. // Used for linking variables to their parent scope counterparts upon Blackboard creation/population.
HashMap<StringName, StringName> parent_scope_mapping; HashMap<StringName, StringName> parent_scope_mapping;
// Fetcher function for the parent scope plan. Funtion should return a Ref<BlackboardPlan>. // Fetcher function for the parent scope plan. Function should return a Ref<BlackboardPlan>.
// Used in the inspector. When set, mapping feature becomes available. // Used in the inspector: enables mapping feature when set.
Callable parent_scope_plan_provider; Callable parent_scope_plan_provider;
// Bindings to properties in the scene to which this plan belongs.
HashMap<StringName, NodePath> property_bindings;
bool property_binding_enabled = false;
// If true, NodePath variables will be prefetched, so that the vars will contain node pointers instead (upon BB creation/population). // If true, NodePath variables will be prefetched, so that the vars will contain node pointers instead (upon BB creation/population).
bool prefetch_nodepath_vars = true; bool prefetch_nodepath_vars = true;
_FORCE_INLINE_ bool _is_var_hidden(const String &p_name, const BBVariable &p_var) const { return p_var.get_type() == Variant::NIL || (is_derived() && p_name.begins_with("_")); }
protected: protected:
static void _bind_methods(); static void _bind_methods();
@ -69,6 +76,10 @@ public:
bool is_mapping_enabled() const { return parent_scope_plan_provider.is_valid() && (parent_scope_plan_provider.call() != Ref<BlackboardPlan>()); } bool is_mapping_enabled() const { return parent_scope_plan_provider.is_valid() && (parent_scope_plan_provider.call() != Ref<BlackboardPlan>()); }
bool has_mapping(const StringName &p_name) const; bool has_mapping(const StringName &p_name) const;
bool has_property_binding(const StringName &p_name) const { return property_bindings.has(p_name); }
void set_property_binding(const StringName &p_name, const NodePath &p_path);
NodePath get_property_binding(const StringName &p_name) const { return property_bindings.has(p_name) ? property_bindings[p_name] : NodePath(); }
void set_prefetch_nodepath_vars(bool p_enable); void set_prefetch_nodepath_vars(bool p_enable);
bool is_prefetching_nodepath_vars() const; bool is_prefetching_nodepath_vars() const;

View File

@ -234,7 +234,7 @@ Called when the state is entered.
|void| **_exit**\ (\ ) |virtual| :ref:`🔗<class_LimboState_private_method__exit>` |void| **_exit**\ (\ ) |virtual| :ref:`🔗<class_LimboState_private_method__exit>`
Called when the state is exited. Called when the state is exited. This happens on a transition to another state, and when the state machine is removed from the scene tree (e.g., when the node is freed with :ref:`Node.queue_free<class_Node_method_queue_free>` or the scene changes). Due to implementation details, :ref:`_exit<class_LimboState_private_method__exit>` will not be called on :ref:`Object.free<class_Object_method_free>`!
.. rst-class:: classref-item-separator .. rst-class:: classref-item-separator

View File

@ -20,7 +20,7 @@
<method name="_exit" qualifiers="virtual"> <method name="_exit" qualifiers="virtual">
<return type="void" /> <return type="void" />
<description> <description>
Called when the state is exited. Called when the state is exited. This happens on a transition to another state, and when the state machine is removed from the scene tree (e.g., when the node is freed with [method Node.queue_free] or the scene changes). Due to implementation details, [method _exit] will not be called on [method Object.free]!
</description> </description>
</method> </method>
<method name="_setup" qualifiers="virtual"> <method name="_setup" qualifiers="virtual">

View File

@ -0,0 +1,238 @@
/**
* editor_property_path.cpp
* =============================================================================
* Copyright 2021-2024 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 "editor_property_property_path.h"
#include "../util/limbo_compat.h"
#include "../util/limbo_string_names.h"
#ifdef LIMBOAI_MODULE
#include "editor/editor_data.h"
#include "editor/editor_interface.h"
#include "servers/display_server.h"
#endif
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/display_server.hpp>
#include <godot_cpp/classes/editor_inspector.hpp>
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/editor_selection.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/popup_menu.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#endif // LIMBOAI_MODULE
#ifdef TOOLS_ENABLED
namespace {
Node *_get_base_node(Object *p_edited_object, SceneTree *p_scene_tree) {
Node *base_node = Object::cast_to<Node>(p_edited_object);
if (!base_node) {
base_node = Object::cast_to<Node>(EditorInterface::get_singleton()->get_inspector()->get_edited_object());
}
if (!base_node) {
base_node = p_scene_tree->get_edited_scene_root();
}
return base_node;
}
} // unnamed namespace
Node *EditorPropertyPropertyPath::_get_selected_node() {
ERR_FAIL_NULL_V(get_edited_object(), nullptr);
NodePath path = get_edited_object()->get(get_edited_property());
if (path.is_empty()) {
return nullptr;
}
Node *base_node = _get_base_node(get_edited_object(), get_tree());
ERR_FAIL_NULL_V(base_node, nullptr);
Node *selected_node = base_node->get_node_or_null(path);
return selected_node;
}
void EditorPropertyPropertyPath::_action_selected(int p_idx) {
switch (p_idx) {
case ACTION_CLEAR: {
emit_changed(get_edited_property(), NodePath());
} break;
case ACTION_COPY: {
DisplayServer::get_singleton()->clipboard_set(get_edited_object()->get(get_edited_property()));
} break;
case ACTION_EDIT: {
assign_button->hide();
action_menu->hide();
path_edit->show();
path_edit->set_text(get_edited_object()->get(get_edited_property()));
path_edit->grab_focus();
} break;
case ACTION_SELECT: {
Node *selected_node = _get_selected_node();
if (selected_node) {
EditorInterface::get_singleton()->get_selection()->clear();
EditorInterface::get_singleton()->get_selection()->add_node(selected_node);
}
} break;
}
}
void EditorPropertyPropertyPath::_accept_text() {
path_edit->hide();
assign_button->show();
action_menu->show();
emit_changed(get_edited_property(), path_edit->get_text());
}
void EditorPropertyPropertyPath::_property_selected(const NodePath &p_property_path, const NodePath &p_node_path) {
if (p_property_path.is_empty()) {
return;
}
Node *base_node = _get_base_node(get_edited_object(), get_tree());
ERR_FAIL_NULL(base_node);
Node *selected_node = get_tree()->get_edited_scene_root()->get_node_or_null(p_node_path);
ERR_FAIL_NULL(selected_node);
NodePath path = String(base_node->get_path_to(selected_node)) + String(p_property_path);
emit_changed(get_edited_property(), path);
update_property();
}
void EditorPropertyPropertyPath::_node_selected(const NodePath &p_path) {
if (p_path.is_empty()) {
return;
}
Node *selected_node = get_tree()->get_edited_scene_root()->get_node_or_null(p_path);
ERR_FAIL_NULL(selected_node);
EditorInterface::get_singleton()->popup_property_selector(
selected_node,
callable_mp(this, &EditorPropertyPropertyPath::_property_selected).bind(p_path),
valid_types);
}
void EditorPropertyPropertyPath::_choose_property() {
EditorInterface::get_singleton()->popup_node_selector(callable_mp(this, &EditorPropertyPropertyPath::_node_selected));
}
void EditorPropertyPropertyPath::_set_read_only(bool p_read_only) {
assign_button->set_disabled(p_read_only);
action_menu->set_disabled(p_read_only);
}
#ifdef LIMBOAI_MODULE
void EditorPropertyPropertyPath::update_property() {
#elif LIMBOAI_GDEXTENSION
void EditorPropertyPropertyPath::_update_property() {
#endif
NodePath path = get_edited_object()->get(get_edited_property());
if (path.is_empty()) {
assign_button->set_text(TTR("Bind..."));
} else {
Node *base_node = _get_base_node(get_edited_object(), get_tree());
ERR_FAIL_NULL(base_node);
Node *selected_node = base_node->get_node_or_null(path);
String text;
if (selected_node) {
text = (String)selected_node->get_name() +
":" + (String)path.get_concatenated_subnames();
} else {
text = (String)path;
}
assign_button->set_text(text);
assign_button->set_tooltip_text(path);
}
}
void EditorPropertyPropertyPath::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_ENTER_TREE:
case NOTIFICATION_THEME_CHANGED: {
action_menu->set_button_icon(get_theme_icon(LW_NAME(GuiTabMenuHl), LW_NAME(EditorIcons)));
action_menu->get_popup()->set_item_icon(ACTION_CLEAR, get_theme_icon(LW_NAME(Clear), LW_NAME(EditorIcons)));
action_menu->get_popup()->set_item_icon(ACTION_COPY, get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons)));
action_menu->get_popup()->set_item_icon(ACTION_EDIT, get_theme_icon(LW_NAME(Edit), LW_NAME(EditorIcons)));
action_menu->get_popup()->set_item_icon(ACTION_SELECT, get_theme_icon(LW_NAME(ExternalLink), LW_NAME(EditorIcons)));
} break;
}
}
void EditorPropertyPropertyPath::setup(const PackedInt32Array &p_valid_types) {
valid_types = p_valid_types;
}
EditorPropertyPropertyPath::EditorPropertyPropertyPath() {
HBoxContainer *hb = memnew(HBoxContainer);
add_child(hb);
hb->add_theme_constant_override(LW_NAME(separation), 0);
assign_button = memnew(Button);
hb->add_child(assign_button);
assign_button->set_flat(true);
assign_button->set_text(TTR("Bind..."));
assign_button->set_clip_text(true);
assign_button->set_h_size_flags(SIZE_EXPAND_FILL);
assign_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
assign_button->connect(LW_NAME(pressed), callable_mp(this, &EditorPropertyPropertyPath::_choose_property));
path_edit = memnew(LineEdit);
hb->add_child(path_edit);
path_edit->set_h_size_flags(SIZE_EXPAND_FILL);
path_edit->connect(LW_NAME(focus_exited), callable_mp(this, &EditorPropertyPropertyPath::_accept_text));
path_edit->connect(LW_NAME(text_submitted), callable_mp(this, &EditorPropertyPropertyPath::_accept_text).unbind(1));
path_edit->hide();
action_menu = memnew(MenuButton);
action_menu->get_popup()->add_item(TTR("Clear"), ACTION_CLEAR);
action_menu->get_popup()->add_item(TTR("Copy as Text"), ACTION_COPY);
action_menu->get_popup()->add_item(TTR("Edit"), ACTION_EDIT);
action_menu->get_popup()->add_item(TTR("Show Node in Tree"), ACTION_SELECT);
action_menu->get_popup()->connect(LW_NAME(id_pressed), callable_mp(this, &EditorPropertyPropertyPath::_action_selected));
hb->add_child(action_menu);
}
//***** EditorInspectorPluginPropertyPath
#ifdef LIMBOAI_MODULE
bool EditorInspectorPluginPropertyPath::can_handle(Object *p_object) {
#elif LIMBOAI_GDEXTENSION
bool EditorInspectorPluginPropertyPath::_can_handle(Object *p_object) const {
#endif
return true;
}
#ifdef LIMBOAI_MODULE
bool EditorInspectorPluginPropertyPath::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
#elif LIMBOAI_GDEXTENSION
bool EditorInspectorPluginPropertyPath::_parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
#endif
if (p_type != Variant::NODE_PATH || p_hint != PROPERTY_HINT_LINK) {
return false;
}
EditorPropertyPropertyPath *ed = memnew(EditorPropertyPropertyPath);
// Convert the hint text to an array of valid types.
PackedInt32Array valid_types;
PackedStringArray type_specifiers = p_hint_text.split(",");
for (const String &t : type_specifiers) {
if (t.is_valid_int()) {
valid_types.append(t.to_int());
}
}
ed->setup(valid_types);
add_property_editor(p_path, ed);
return true;
}
#endif // TOOLS_ENABLED

View File

@ -0,0 +1,98 @@
/**
* editor_property_path.h
* =============================================================================
* Copyright 2021-2024 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 EDITOR_PROPERTY_PROPERTY_PATH
#define EDITOR_PROPERTY_PROPERTY_PATH
#ifdef TOOLS_ENABLED
#ifdef LIMBOAI_MODULE
#include "editor/editor_inspector.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/menu_button.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/button.hpp>
#include <godot_cpp/classes/editor_inspector_plugin.hpp>
#include <godot_cpp/classes/editor_property.hpp>
#include <godot_cpp/classes/line_edit.hpp>
#include <godot_cpp/classes/menu_button.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION
// Specialized property editor for NodePath properties that represent a path to a specific property instead of just a node.
// Handles NodePath properties that have PROPERTY_HINT_LINK.
// Hint string can list the valid Variant types as comma-separated integers.
class EditorPropertyPropertyPath : public EditorProperty {
GDCLASS(EditorPropertyPropertyPath, EditorProperty);
private:
enum Action {
ACTION_CLEAR,
ACTION_COPY,
ACTION_EDIT,
ACTION_SELECT,
};
Button *assign_button;
MenuButton *action_menu;
LineEdit *path_edit;
PackedInt32Array valid_types;
Node *_get_selected_node();
void _action_selected(int p_idx);
void _accept_text();
void _property_selected(const NodePath &p_property_path, const NodePath &p_node_path);
void _node_selected(const NodePath &p_path);
void _choose_property();
protected:
static void _bind_methods() {}
void _notification(int p_what);
public:
// Note: Needs to be public in GDExtension.
virtual void _set_read_only(bool p_read_only) override;
#ifdef LIMBOAI_MODULE
virtual void update_property() override;
#elif LIMBOAI_GDEXTENSION
virtual void _update_property() override;
#endif
void setup(const PackedInt32Array &p_valid_types);
EditorPropertyPropertyPath();
};
class EditorInspectorPluginPropertyPath : public EditorInspectorPlugin {
GDCLASS(EditorInspectorPluginPropertyPath, EditorInspectorPlugin);
private:
protected:
static void _bind_methods() {}
public:
#ifdef LIMBOAI_MODULE
virtual bool can_handle(Object *p_object) override;
virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide = false) override;
#elif LIMBOAI_GDEXTENSION
virtual bool _can_handle(Object *p_object) const override;
virtual bool _parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide = false) override;
#endif
EditorInspectorPluginPropertyPath() = default;
};
#endif // TOOLS_ENABLED
#endif // EDITOR_PROPERTY_PROPERTY_PATH

View File

@ -245,6 +245,11 @@ bool EditorInspectorPluginVariableName::parse_property(Object *p_object, const V
#elif LIMBOAI_GDEXTENSION #elif LIMBOAI_GDEXTENSION
bool EditorInspectorPluginVariableName::_parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) { bool EditorInspectorPluginVariableName::_parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
#endif #endif
if (p_usage & PROPERTY_USAGE_READ_ONLY) {
// Don't handle read-only properties using this plugin.
return false;
}
bool is_mapping = p_path.begins_with("mapping/"); bool is_mapping = p_path.begins_with("mapping/");
if (!(p_type == Variant::Type::STRING_NAME || p_type == Variant::Type::STRING) || !(is_mapping || p_path.ends_with("_var") || p_path.ends_with("variable"))) { if (!(p_type == Variant::Type::STRING_NAME || p_type == Variant::Type::STRING) || !(is_mapping || p_path.ends_with("_var") || p_path.ends_with("variable"))) {
return false; return false;

View File

@ -25,6 +25,7 @@
#include "blackboard_plan_editor.h" #include "blackboard_plan_editor.h"
#include "debugger/limbo_debugger_plugin.h" #include "debugger/limbo_debugger_plugin.h"
#include "editor_property_bb_param.h" #include "editor_property_bb_param.h"
#include "editor_property_property_path.h"
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
#include "core/config/project_settings.h" #include "core/config/project_settings.h"
@ -66,6 +67,7 @@
#include <godot_cpp/classes/ref_counted.hpp> #include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/classes/resource_loader.hpp> #include <godot_cpp/classes/resource_loader.hpp>
#include <godot_cpp/classes/resource_saver.hpp> #include <godot_cpp/classes/resource_saver.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#include <godot_cpp/classes/script.hpp> #include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/script_editor.hpp> #include <godot_cpp/classes/script_editor.hpp>
#include <godot_cpp/classes/script_editor_base.hpp> #include <godot_cpp/classes/script_editor_base.hpp>
@ -73,6 +75,21 @@
#include <godot_cpp/core/error_macros.hpp> #include <godot_cpp/core/error_macros.hpp>
#endif // LIMBOAI_GDEXTENSION #endif // LIMBOAI_GDEXTENSION
namespace {
// If built-in resource - switch to the owner scene (open it if not already).
inline void _switch_to_owner_scene_if_builtin(const Ref<BehaviorTree> &p_behavior_tree) {
if (p_behavior_tree.is_valid() && p_behavior_tree->get_path().contains("::")) {
String current_scene = SCENE_TREE()->get_edited_scene_root()->get_scene_file_path();
String scene_path = p_behavior_tree->get_path().get_slice("::", 0);
if (current_scene != scene_path) {
EditorInterface::get_singleton()->open_scene_from_path(scene_path);
}
}
}
} // unnamed namespace
//**** LimboAIEditor //**** LimboAIEditor
_FORCE_INLINE_ String _get_script_template_path() { _FORCE_INLINE_ String _get_script_template_path() {
@ -97,7 +114,7 @@ void LimboAIEditor::_commit_action_with_update(EditorUndoRedoManager *p_undo_red
p_undo_redo->add_do_method(this, LW_NAME(_update_task_tree), task_tree->get_bt()); p_undo_redo->add_do_method(this, LW_NAME(_update_task_tree), task_tree->get_bt());
p_undo_redo->add_undo_method(this, LW_NAME(_update_task_tree), task_tree->get_bt()); p_undo_redo->add_undo_method(this, LW_NAME(_update_task_tree), task_tree->get_bt());
p_undo_redo->commit_action(); p_undo_redo->commit_action();
_mark_as_dirty(true); _set_as_dirty(task_tree->get_bt(), true);
} }
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task, bool p_as_sibling) { void LimboAIEditor::_add_task(const Ref<BTTask> &p_task, bool p_as_sibling) {
@ -201,20 +218,58 @@ void LimboAIEditor::_new_bt() {
EDIT_RESOURCE(bt); EDIT_RESOURCE(bt);
} }
void LimboAIEditor::_save_bt(String p_path) { void LimboAIEditor::_save_bt(const Ref<BehaviorTree> &p_bt, const String &p_path) {
ERR_FAIL_COND_MSG(p_path.is_empty(), "Empty p_path"); ERR_FAIL_COND(p_path.is_empty());
ERR_FAIL_COND_MSG(task_tree->get_bt().is_null(), "Behavior Tree is null."); ERR_FAIL_COND(!p_path.begins_with("res://"));
ERR_FAIL_COND(p_bt.is_null());
if (p_bt->get_path() != p_path) {
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
task_tree->get_bt()->set_path(p_path, true); task_tree->get_bt()->set_path(p_path, true);
#elif LIMBOAI_GDEXTENSION #elif LIMBOAI_GDEXTENSION
task_tree->get_bt()->take_over_path(p_path); task_tree->get_bt()->take_over_path(p_path);
#endif #endif
RESOURCE_SAVE(task_tree->get_bt(), p_path, ResourceSaver::FLAG_CHANGE_PATH);
_update_tabs();
_mark_as_dirty(false);
} }
void LimboAIEditor::_load_bt(String p_path) { // This is a workaround, because EditorNode::save_resource() function is not accessible in GDExtension.
if (RESOURCE_IS_BUILT_IN(p_bt)) {
// If built-in resource - save the containing resource instead.
String file_path = p_path.get_slice("::", 0);
ERR_FAIL_COND_MSG(!RESOURCE_EXISTS(file_path, "Resource"), "LimboAI: SAVE FAILED - resource file doesn't exist: " + file_path);
if (RESOURCE_IS_SCENE_FILE(file_path)) {
// Packed scene - save the scene instead.
if (EditorInterface::get_singleton()->get_open_scenes().has(file_path)) {
// If scene is open, switch to it first, and then ask to save.
// This is needed because saving the currently edited scene can have complications.
EditorInterface::get_singleton()->open_scene_from_path(file_path);
EditorInterface::get_singleton()->save_scene();
} else {
// If scene is not currently open in the editor, load and resave it.
Ref<Resource> scene = RESOURCE_LOAD(file_path, "PackedScene");
RESOURCE_SAVE(scene, file_path, ResourceSaver::FLAG_NONE);
}
} else {
// Not a packed scene - save the containing resource to file.
Ref<Resource> res = RESOURCE_LOAD(file_path, "Resource");
RESOURCE_SAVE(res, file_path, ResourceSaver::FLAG_NONE);
}
} else {
// If external resource - save to file.
RESOURCE_SAVE(p_bt, p_path, ResourceSaver::FLAG_CHANGE_PATH);
}
_set_as_dirty(p_bt, false);
_update_tabs();
}
void LimboAIEditor::_save_current_bt(const String &p_path) {
ERR_FAIL_COND_MSG(p_path.is_empty(), "LimboAI: SAVE FAILED - p_path is empty");
ERR_FAIL_COND_MSG(task_tree->get_bt().is_null(), "LimboAI: SAVE FAILED - bt is null");
_save_bt(task_tree->get_bt(), p_path);
}
void LimboAIEditor::_load_bt(const String &p_path) {
ERR_FAIL_COND_MSG(p_path.is_empty(), "Empty p_path"); ERR_FAIL_COND_MSG(p_path.is_empty(), "Empty p_path");
Ref<BehaviorTree> bt = RESOURCE_LOAD(p_path, "BehaviorTree"); Ref<BehaviorTree> bt = RESOURCE_LOAD(p_path, "BehaviorTree");
ERR_FAIL_COND(!bt.is_valid()); ERR_FAIL_COND(!bt.is_valid());
@ -253,6 +308,8 @@ void LimboAIEditor::_disable_editing() {
void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_force_refresh) { void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_force_refresh) {
ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "p_behavior_tree is null"); ERR_FAIL_COND_MSG(p_behavior_tree.is_null(), "p_behavior_tree is null");
_switch_to_owner_scene_if_builtin(p_behavior_tree);
if (!p_force_refresh && task_tree->get_bt() == p_behavior_tree) { if (!p_force_refresh && task_tree->get_bt() == p_behavior_tree) {
return; return;
} }
@ -268,8 +325,8 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
task_tree->load_bt(p_behavior_tree); task_tree->load_bt(p_behavior_tree);
if (task_tree->get_bt().is_valid() && !task_tree->get_bt()->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty))) { if (task_tree->get_bt().is_valid() && !task_tree->get_bt()->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty))) {
task_tree->get_bt()->connect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty).bind(true)); task_tree->get_bt()->connect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty).bind(task_tree->get_bt(), true));
} }
int idx = history.find(p_behavior_tree); int idx = history.find(p_behavior_tree);
@ -334,12 +391,11 @@ void LimboAIEditor::get_window_layout(const Ref<ConfigFile> &p_configuration) {
p_configuration->set_value("LimboAI", "bteditor_hsplit", split_offset); p_configuration->set_value("LimboAI", "bteditor_hsplit", split_offset);
} }
void LimboAIEditor::_mark_as_dirty(bool p_dirty) { void LimboAIEditor::_set_as_dirty(const Ref<BehaviorTree> &p_bt, bool p_dirty) {
Ref<BehaviorTree> bt = task_tree->get_bt(); if (p_dirty && !dirty.has(p_bt)) {
if (p_dirty && !dirty.has(bt)) { dirty.insert(p_bt);
dirty.insert(bt); } else if (p_dirty == false && dirty.has(p_bt)) {
} else if (p_dirty == false && dirty.has(bt)) { dirty.erase(p_bt);
dirty.erase(bt);
} }
} }
@ -439,6 +495,14 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
} else if (LW_IS_SHORTCUT("limbo_ai/close_tab", p_event)) { } else if (LW_IS_SHORTCUT("limbo_ai/close_tab", p_event)) {
_tab_menu_option_selected(TAB_CLOSE); _tab_menu_option_selected(TAB_CLOSE);
handled = true; handled = true;
} else if (LW_IS_SHORTCUT("limbo_ai/editor_save_scene", p_event)) {
// This intercepts the editor save action, but does not set the event as handled because we don't know the user's intention.
// We just want to save the currently edited BT as well, which may cause a loop with built-in resource if done from "_save_external_data".
// Workaround for: https://github.com/limbonaut/limboai/issues/240#issuecomment-2453087424
if (task_tree->get_bt().is_valid() && RESOURCE_IS_BUILT_IN(task_tree->get_bt())) {
_on_save_pressed();
}
handled = false; // intentionally not set as handled
} }
} }
@ -837,6 +901,7 @@ void LimboAIEditor::_on_tree_task_activated() {
void LimboAIEditor::_on_visibility_changed() { void LimboAIEditor::_on_visibility_changed() {
if (task_tree->is_visible_in_tree()) { if (task_tree->is_visible_in_tree()) {
_switch_to_owner_scene_if_builtin(task_tree->get_bt());
Ref<BTTask> sel = task_tree->get_selected(); Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) { if (sel.is_valid()) {
EDIT_RESOURCE(sel); EDIT_RESOURCE(sel);
@ -854,16 +919,6 @@ void LimboAIEditor::_on_visibility_changed() {
} }
} }
void LimboAIEditor::_on_header_pressed() {
task_tree->clear_selection();
#ifdef LIMBOAI_MODULE
if (task_tree->get_bt().is_valid()) {
task_tree->get_bt()->editor_set_section_unfold("blackboard_plan", true);
}
#endif // LIMBOAI_MODULE
EDIT_RESOURCE(task_tree->get_bt());
}
void LimboAIEditor::_on_save_pressed() { void LimboAIEditor::_on_save_pressed() {
if (task_tree->get_bt().is_null()) { if (task_tree->get_bt().is_null()) {
return; return;
@ -872,7 +927,7 @@ void LimboAIEditor::_on_save_pressed() {
if (path.is_empty()) { if (path.is_empty()) {
save_dialog->popup_centered_ratio(); save_dialog->popup_centered_ratio();
} else { } else {
_save_bt(path); _save_current_bt(path);
} }
} }
@ -1060,8 +1115,8 @@ void LimboAIEditor::_tab_clicked(int p_tab) {
void LimboAIEditor::_tab_closed(int p_tab) { void LimboAIEditor::_tab_closed(int p_tab) {
ERR_FAIL_INDEX(p_tab, history.size()); ERR_FAIL_INDEX(p_tab, history.size());
Ref<BehaviorTree> history_bt = history[p_tab]; Ref<BehaviorTree> history_bt = history[p_tab];
if (history_bt.is_valid() && history_bt->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty))) { if (history_bt.is_valid() && history_bt->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty))) {
history_bt->disconnect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty)); history_bt->disconnect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty));
} }
if (tab_search_context.has(history_bt)) { if (tab_search_context.has(history_bt)) {
tab_search_context.erase(history_bt); tab_search_context.erase(history_bt);
@ -1241,10 +1296,13 @@ void LimboAIEditor::_reload_modified() {
void LimboAIEditor::_resave_modified(String _str) { void LimboAIEditor::_resave_modified(String _str) {
for (const String &res_path : disk_changed_files) { for (const String &res_path : disk_changed_files) {
Ref<BehaviorTree> res = RESOURCE_LOAD(res_path, "BehaviorTree"); Ref<BehaviorTree> bt = RESOURCE_LOAD(res_path, "BehaviorTree");
if (res.is_valid()) { if (bt.is_valid()) {
ERR_FAIL_COND(!res->is_class("BehaviorTree")); ERR_FAIL_COND(!bt->is_class("BehaviorTree"));
RESOURCE_SAVE(res, res->get_path(), 0); if (RESOURCE_IS_EXTERNAL(bt)) {
// Only resave external - scene files are handled by the editor.
_save_bt(bt, bt->get_path());
}
} }
} }
task_tree->update_tree(); task_tree->update_tree();
@ -1269,14 +1327,13 @@ void LimboAIEditor::_rename_task_confirmed() {
undo_redo->commit_action(); undo_redo->commit_action();
} }
void LimboAIEditor::apply_changes() { void LimboAIEditor::save_all(bool p_external_only) {
for (int i = 0; i < history.size(); i++) { for (int i = 0; i < history.size(); i++) {
Ref<BehaviorTree> bt = history.get(i); Ref<BehaviorTree> bt = history.get(i);
String path = bt->get_path(); String path = bt->get_path();
if (RESOURCE_EXISTS(path, "BehaviorTree")) { if (RESOURCE_EXISTS(path, "BehaviorTree") && (!p_external_only || RESOURCE_PATH_IS_EXTERNAL(path))) {
RESOURCE_SAVE(bt, path, 0); _save_bt(bt, path);
} }
dirty.clear();
} }
} }
@ -1423,14 +1480,14 @@ void LimboAIEditor::_notification(int p_what) {
case NOTIFICATION_EXIT_TREE: { case NOTIFICATION_EXIT_TREE: {
task_tree->unload(); task_tree->unload();
for (int i = 0; i < history.size(); i++) { for (int i = 0; i < history.size(); i++) {
if (history[i]->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty))) { if (history[i]->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty))) {
history[i]->disconnect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty)); history[i]->disconnect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_set_as_dirty));
} }
} }
} break; } break;
case NOTIFICATION_READY: { case NOTIFICATION_READY: {
// **** Signals // **** Signals
save_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_save_bt)); save_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_save_current_bt));
load_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_load_bt)); load_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_load_bt));
extract_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_extract_subtree)); extract_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_extract_subtree));
new_btn->connect(LW_NAME(pressed), callable_mp(this, &LimboAIEditor::_new_bt)); new_btn->connect(LW_NAME(pressed), callable_mp(this, &LimboAIEditor::_new_bt));
@ -1494,7 +1551,7 @@ void LimboAIEditor::_bind_methods() {
ClassDB::bind_method(D_METHOD("_remove_task", "task"), &LimboAIEditor::_remove_task); ClassDB::bind_method(D_METHOD("_remove_task", "task"), &LimboAIEditor::_remove_task);
ClassDB::bind_method(D_METHOD("_add_task_with_prototype", "prototype_task"), &LimboAIEditor::_add_task_with_prototype); ClassDB::bind_method(D_METHOD("_add_task_with_prototype", "prototype_task"), &LimboAIEditor::_add_task_with_prototype);
ClassDB::bind_method(D_METHOD("_new_bt"), &LimboAIEditor::_new_bt); ClassDB::bind_method(D_METHOD("_new_bt"), &LimboAIEditor::_new_bt);
ClassDB::bind_method(D_METHOD("_save_bt", "path"), &LimboAIEditor::_save_bt); ClassDB::bind_method(D_METHOD("_save_bt", "path"), &LimboAIEditor::_save_current_bt);
ClassDB::bind_method(D_METHOD("_load_bt", "path"), &LimboAIEditor::_load_bt); ClassDB::bind_method(D_METHOD("_load_bt", "path"), &LimboAIEditor::_load_bt);
ClassDB::bind_method(D_METHOD("_update_task_tree", "bt", "specific_task"), &LimboAIEditor::_update_task_tree, DEFVAL(Variant())); ClassDB::bind_method(D_METHOD("_update_task_tree", "bt", "specific_task"), &LimboAIEditor::_update_task_tree, DEFVAL(Variant()));
ClassDB::bind_method(D_METHOD("edit_bt", "behavior_tree", "force_refresh"), &LimboAIEditor::edit_bt, Variant(false)); ClassDB::bind_method(D_METHOD("edit_bt", "behavior_tree", "force_refresh"), &LimboAIEditor::edit_bt, Variant(false));
@ -1541,11 +1598,14 @@ LimboAIEditor::LimboAIEditor() {
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)));
LW_SHORTCUT("limbo_ai/load_behavior_tree", TTR("Load Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(L))); LW_SHORTCUT("limbo_ai/load_behavior_tree", TTR("Load Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(L)));
LW_SHORTCUT("limbo_ai/open_debugger", TTR("Open Debugger"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(D))); LW_SHORTCUT("limbo_ai/open_debugger", TTR("Open Debugger"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(D)));
LW_SHORTCUT("limbo_ai/jump_to_owner", TTR("Jump to Owner"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(J))); LW_SHORTCUT("limbo_ai/jump_to_owner", TTR("Jump to Owner"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(G)));
LW_SHORTCUT("limbo_ai/close_tab", TTR("Close Tab"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(W))); LW_SHORTCUT("limbo_ai/close_tab", TTR("Close Tab"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(W)));
LW_SHORTCUT("limbo_ai/find_task", TTR("Find Task"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(F))); LW_SHORTCUT("limbo_ai/find_task", TTR("Find Task"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(F)));
LW_SHORTCUT("limbo_ai/hide_tree_search", TTR("Close Search"), (Key)(LW_KEY(ESCAPE))); LW_SHORTCUT("limbo_ai/hide_tree_search", TTR("Close Search"), (Key)(LW_KEY(ESCAPE)));
// Intercept editor save scene action.
LW_SHORTCUT("limbo_ai/editor_save_scene", TTR("Save Scene"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(S)));
set_process_shortcut_input(true); set_process_shortcut_input(true);
save_dialog = memnew(FileDialog); save_dialog = memnew(FileDialog);
@ -1851,14 +1911,6 @@ LimboAIEditor::~LimboAIEditor() {
//**** LimboAIEditorPlugin //**** LimboAIEditorPlugin
#ifdef LIMBOAI_MODULE
void LimboAIEditorPlugin::apply_changes() {
#elif LIMBOAI_GDEXTENSION
void LimboAIEditorPlugin::_apply_changes() {
#endif
limbo_ai_editor->apply_changes();
}
void LimboAIEditorPlugin::_bind_methods() { void LimboAIEditorPlugin::_bind_methods() {
} }
@ -1867,9 +1919,13 @@ void LimboAIEditorPlugin::_notification(int p_notification) {
case NOTIFICATION_READY: { case NOTIFICATION_READY: {
add_debugger_plugin(memnew(LimboDebuggerPlugin)); add_debugger_plugin(memnew(LimboDebuggerPlugin));
add_inspector_plugin(memnew(EditorInspectorPluginBBPlan)); add_inspector_plugin(memnew(EditorInspectorPluginBBPlan));
EditorInspectorPluginVariableName *var_plugin = memnew(EditorInspectorPluginVariableName); EditorInspectorPluginVariableName *var_plugin = memnew(EditorInspectorPluginVariableName);
var_plugin->set_editor_plan_provider(Callable(limbo_ai_editor, "get_edited_blackboard_plan")); var_plugin->set_editor_plan_provider(Callable(limbo_ai_editor, "get_edited_blackboard_plan"));
add_inspector_plugin(var_plugin); add_inspector_plugin(var_plugin);
EditorInspectorPluginPropertyPath *path_plugin = memnew(EditorInspectorPluginPropertyPath);
add_inspector_plugin(path_plugin);
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
// ! Only used in the module version. // ! Only used in the module version.
EditorInspectorPluginBBParam *param_plugin = memnew(EditorInspectorPluginBBParam); EditorInspectorPluginBBParam *param_plugin = memnew(EditorInspectorPluginBBParam);
@ -1917,8 +1973,9 @@ void LimboAIEditorPlugin::edit(Object *p_object) {
#elif LIMBOAI_GDEXTENSION #elif LIMBOAI_GDEXTENSION
void LimboAIEditorPlugin::_edit(Object *p_object) { void LimboAIEditorPlugin::_edit(Object *p_object) {
#endif #endif
if (Object::cast_to<BehaviorTree>(p_object)) { Ref<BehaviorTree> bt = Object::cast_to<BehaviorTree>(p_object);
limbo_ai_editor->edit_bt(Object::cast_to<BehaviorTree>(p_object)); if (bt.is_valid()) {
limbo_ai_editor->edit_bt(bt);
} }
} }
@ -1933,6 +1990,14 @@ bool LimboAIEditorPlugin::_handles(Object *p_object) const {
return false; return false;
} }
#ifdef LIMBOAI_MODULE
void LimboAIEditorPlugin::save_external_data() {
#elif LIMBOAI_GDEXTENSION
void LimboAIEditorPlugin::_save_external_data() {
#endif
limbo_ai_editor->save_all(true);
}
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
Ref<Texture2D> LimboAIEditorPlugin::_get_plugin_icon() const { Ref<Texture2D> LimboAIEditorPlugin::_get_plugin_icon() const {
return LimboUtility::get_singleton()->get_task_icon("LimboAI"); return LimboUtility::get_singleton()->get_task_icon("LimboAI");

View File

@ -207,11 +207,12 @@ private:
void _update_misc_menu(); void _update_misc_menu();
void _update_banners(); void _update_banners();
void _new_bt(); void _new_bt();
void _save_bt(String p_path); void _save_bt(const Ref<BehaviorTree> &p_bt, const String &p_path);
void _load_bt(String p_path); void _save_current_bt(const String &p_path);
void _load_bt(const String &p_path);
void _update_task_tree(const Ref<BehaviorTree> &p_bt, const Ref<BTTask> &p_specific_task = nullptr); void _update_task_tree(const Ref<BehaviorTree> &p_bt, const Ref<BTTask> &p_specific_task = nullptr);
void _disable_editing(); void _disable_editing();
void _mark_as_dirty(bool p_dirty); void _set_as_dirty(const Ref<BehaviorTree> &p_bt, bool p_dirty);
void _create_user_task_dir(); void _create_user_task_dir();
void _remove_task_from_favorite(const String &p_task); void _remove_task_from_favorite(const String &p_task);
void _save_and_restart(); void _save_and_restart();
@ -242,7 +243,6 @@ private:
void _on_tree_task_selected(const Ref<BTTask> &p_task); void _on_tree_task_selected(const Ref<BTTask> &p_task);
void _on_tree_task_activated(); void _on_tree_task_activated();
void _on_visibility_changed(); void _on_visibility_changed();
void _on_header_pressed();
void _on_save_pressed(); void _on_save_pressed();
void _on_history_back(); void _on_history_back();
void _on_history_forward(); void _on_history_forward();
@ -273,7 +273,7 @@ public:
void set_window_layout(const Ref<ConfigFile> &p_configuration); void set_window_layout(const Ref<ConfigFile> &p_configuration);
void get_window_layout(const Ref<ConfigFile> &p_configuration); void get_window_layout(const Ref<ConfigFile> &p_configuration);
void apply_changes(); void save_all(bool p_external_only = false);
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
virtual void _shortcut_input(const Ref<InputEvent> &p_event) override { _process_shortcut_input(p_event); } virtual void _shortcut_input(const Ref<InputEvent> &p_event) override { _process_shortcut_input(p_event); }
@ -299,23 +299,23 @@ public:
virtual String get_name() const override { return "LimboAI"; } virtual String get_name() const override { return "LimboAI"; }
virtual void make_visible(bool p_visible) override; virtual void make_visible(bool p_visible) override;
virtual void apply_changes() override;
virtual void edit(Object *p_object) override; virtual void edit(Object *p_object) override;
virtual bool handles(Object *p_object) const override; virtual bool handles(Object *p_object) const override;
virtual void set_window_layout(Ref<ConfigFile> p_configuration) override; virtual void set_window_layout(Ref<ConfigFile> p_configuration) override;
virtual void get_window_layout(Ref<ConfigFile> p_configuration) override; virtual void get_window_layout(Ref<ConfigFile> p_configuration) override;
virtual void save_external_data() override;
#elif LIMBOAI_GDEXTENSION #elif LIMBOAI_GDEXTENSION
bool _has_main_screen() const override { return true; } bool _has_main_screen() const override { return true; }
virtual String _get_plugin_name() const override { return "LimboAI"; } virtual String _get_plugin_name() const override { return "LimboAI"; }
virtual void _make_visible(bool p_visible) override; virtual void _make_visible(bool p_visible) override;
virtual void _apply_changes() override;
virtual void _edit(Object *p_object) override; virtual void _edit(Object *p_object) override;
virtual bool _handles(Object *p_object) const override; virtual bool _handles(Object *p_object) const override;
virtual Ref<Texture2D> _get_plugin_icon() const override; virtual Ref<Texture2D> _get_plugin_icon() const override;
virtual void _set_window_layout(const Ref<ConfigFile> &p_configuration) override; virtual void _set_window_layout(const Ref<ConfigFile> &p_configuration) override;
virtual void _get_window_layout(const Ref<ConfigFile> &p_configuration) override; virtual void _get_window_layout(const Ref<ConfigFile> &p_configuration) override;
virtual void _save_external_data() override;
#endif // LIMBOAI_MODULE & LIMBOAI_GDEXTENSION #endif // LIMBOAI_MODULE & LIMBOAI_GDEXTENSION
LimboAIEditorPlugin(); LimboAIEditorPlugin();

View File

@ -95,7 +95,7 @@ protected:
public: public:
void load_bt(const Ref<BehaviorTree> &p_behavior_tree); void load_bt(const Ref<BehaviorTree> &p_behavior_tree);
void unload(); void unload();
Ref<BehaviorTree> get_bt() const { return bt; } _FORCE_INLINE_ Ref<BehaviorTree> get_bt() const { return bt; }
void update_tree() { _update_tree(); } void update_tree() { _update_tree(); }
void update_task(const Ref<BTTask> &p_task); void update_task(const Ref<BTTask> &p_task);
void add_selection(const Ref<BTTask> &p_task); void add_selection(const Ref<BTTask> &p_task);

View File

@ -120,6 +120,7 @@
#endif // LIMBOAI_MODULE #endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
#include "editor/editor_property_property_path.h"
#include <godot_cpp/classes/engine.hpp> #include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/core/class_db.hpp> #include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/memory.hpp> #include <godot_cpp/core/memory.hpp>
@ -250,24 +251,26 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) { if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
GDREGISTER_CLASS(TaskTree); GDREGISTER_INTERNAL_CLASS(TaskTree);
GDREGISTER_CLASS(TaskButton); GDREGISTER_INTERNAL_CLASS(TaskButton);
GDREGISTER_CLASS(TaskPaletteSection); GDREGISTER_INTERNAL_CLASS(TaskPaletteSection);
GDREGISTER_CLASS(TaskPalette); GDREGISTER_INTERNAL_CLASS(TaskPalette);
GDREGISTER_CLASS(ActionBanner); GDREGISTER_INTERNAL_CLASS(ActionBanner);
GDREGISTER_CLASS(ModeSwitchButton); GDREGISTER_INTERNAL_CLASS(ModeSwitchButton);
GDREGISTER_CLASS(CompatShortcutBin); GDREGISTER_INTERNAL_CLASS(CompatShortcutBin);
GDREGISTER_CLASS(CompatScreenSelect); GDREGISTER_INTERNAL_CLASS(CompatScreenSelect);
GDREGISTER_CLASS(CompatWindowWrapper); GDREGISTER_INTERNAL_CLASS(CompatWindowWrapper);
GDREGISTER_CLASS(LimboDebuggerTab); GDREGISTER_INTERNAL_CLASS(LimboDebuggerTab);
GDREGISTER_CLASS(LimboDebuggerPlugin); GDREGISTER_INTERNAL_CLASS(LimboDebuggerPlugin);
GDREGISTER_CLASS(BlackboardPlanEditor); GDREGISTER_INTERNAL_CLASS(BlackboardPlanEditor);
GDREGISTER_CLASS(EditorInspectorPluginBBPlan); GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginBBPlan);
GDREGISTER_CLASS(EditorPropertyVariableName); GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginPropertyPath);
GDREGISTER_CLASS(EditorInspectorPluginVariableName); GDREGISTER_INTERNAL_CLASS(EditorPropertyPropertyPath);
GDREGISTER_CLASS(OwnerPicker); GDREGISTER_INTERNAL_CLASS(EditorPropertyVariableName);
GDREGISTER_CLASS(LimboAIEditor); GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginVariableName);
GDREGISTER_CLASS(LimboAIEditorPlugin); GDREGISTER_INTERNAL_CLASS(OwnerPicker);
GDREGISTER_INTERNAL_CLASS(LimboAIEditor);
GDREGISTER_INTERNAL_CLASS(LimboAIEditorPlugin);
GDREGISTER_INTERNAL_CLASS(TreeSearchPanel); GDREGISTER_INTERNAL_CLASS(TreeSearchPanel);
GDREGISTER_INTERNAL_CLASS(TreeSearch); GDREGISTER_INTERNAL_CLASS(TreeSearch);
#endif // LIMBOAI_GDEXTENSION #endif // LIMBOAI_GDEXTENSION

View File

@ -150,6 +150,7 @@ Variant _GLOBAL_DEF(const PropertyInfo &p_info, const Variant &p_default, bool p
#define EDSCALE (EditorInterface::get_singleton()->get_editor_scale()) #define EDSCALE (EditorInterface::get_singleton()->get_editor_scale())
String TTR(const String &p_text, const String &p_context = ""); String TTR(const String &p_text, const String &p_context = "");
#define RTR(m_text) TTR(m_text)
#endif // ! LIMBOAI_GDEXTENSION #endif // ! LIMBOAI_GDEXTENSION
@ -173,7 +174,9 @@ Variant VARIANT_DEFAULT(Variant::Type p_type);
#define IS_RESOURCE_FILE(m_path) (m_path.begins_with("res://") && m_path.find("::") == -1) #define IS_RESOURCE_FILE(m_path) (m_path.begins_with("res://") && m_path.find("::") == -1)
#define RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type) #define RESOURCE_TYPE_HINT(m_type) vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, m_type)
#define RESOURCE_IS_BUILT_IN(m_res) (m_res->get_path().is_empty() || m_res->get_path().contains("::")) #define RESOURCE_IS_BUILT_IN(m_res) (m_res->get_path().is_empty() || m_res->get_path().contains("::"))
#define RESOURCE_IS_EXTERNAL(m_res) (!RESOURCE_IS_BUILT_IN(m_res))
#define RESOURCE_PATH_IS_BUILT_IN(m_path) (m_path.is_empty() || m_path.contains("::")) #define RESOURCE_PATH_IS_BUILT_IN(m_path) (m_path.is_empty() || m_path.contains("::"))
#define RESOURCE_PATH_IS_EXTERNAL(m_path) (!RESOURCE_PATH_IS_BUILT_IN(m_path))
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED

View File

@ -47,6 +47,7 @@ LimboStringNames::LimboStringNames() {
button_up = SN("button_up"); button_up = SN("button_up");
call_deferred = SN("call_deferred"); call_deferred = SN("call_deferred");
changed = SN("changed"); changed = SN("changed");
Clear = SN("Clear");
Close = SN("Close"); Close = SN("Close");
dark_color_2 = SN("dark_color_2"); dark_color_2 = SN("dark_color_2");
Debug = SN("Debug"); Debug = SN("Debug");
@ -67,6 +68,7 @@ LimboStringNames::LimboStringNames() {
EVENT_FINISHED = SN("finished"); EVENT_FINISHED = SN("finished");
EVENT_SUCCESS = SN("success"); EVENT_SUCCESS = SN("success");
exited = SN("exited"); exited = SN("exited");
ExternalLink = SN("ExternalLink");
favorite_tasks_changed = SN("favorite_tasks_changed"); favorite_tasks_changed = SN("favorite_tasks_changed");
Favorites = SN("Favorites"); Favorites = SN("Favorites");
FlatButton = SN("FlatButton"); FlatButton = SN("FlatButton");
@ -78,6 +80,7 @@ LimboStringNames::LimboStringNames() {
freed = SN("freed"); freed = SN("freed");
gui_input = SN("gui_input"); gui_input = SN("gui_input");
GuiOptionArrow = SN("GuiOptionArrow"); GuiOptionArrow = SN("GuiOptionArrow");
GuiTabMenuHl = SN("GuiTabMenuHl");
GuiTreeArrowDown = SN("GuiTreeArrowDown"); GuiTreeArrowDown = SN("GuiTreeArrowDown");
GuiTreeArrowRight = SN("GuiTreeArrowRight"); GuiTreeArrowRight = SN("GuiTreeArrowRight");
HeaderSmall = SN("HeaderSmall"); HeaderSmall = SN("HeaderSmall");

View File

@ -63,6 +63,7 @@ public:
StringName button_up; StringName button_up;
StringName call_deferred; StringName call_deferred;
StringName changed; StringName changed;
StringName Clear;
StringName Close; StringName Close;
StringName dark_color_2; StringName dark_color_2;
StringName Debug; StringName Debug;
@ -83,6 +84,7 @@ public:
StringName EVENT_FINISHED; StringName EVENT_FINISHED;
StringName EVENT_SUCCESS; StringName EVENT_SUCCESS;
StringName exited; StringName exited;
StringName ExternalLink;
StringName favorite_tasks_changed; StringName favorite_tasks_changed;
StringName Favorites; StringName Favorites;
StringName FlatButton; StringName FlatButton;
@ -94,6 +96,7 @@ public:
StringName freed; StringName freed;
StringName gui_input; StringName gui_input;
StringName GuiOptionArrow; StringName GuiOptionArrow;
StringName GuiTabMenuHl;
StringName GuiTreeArrowDown; StringName GuiTreeArrowDown;
StringName GuiTreeArrowRight; StringName GuiTreeArrowRight;
StringName HeaderSmall; StringName HeaderSmall;