Compare commits

..

3 Commits

Author SHA1 Message Date
Wilson E. Alvarez b5fe6bb77f
Fix unhandled tool button property hint
Due to upstream change:

	85dfd89653
2024-09-30 08:04:06 -04:00
Wilson E. Alvarez cae111ffa4
Fix unhandled dictionary property hint
Due to upstream change:

	9853a69144
2024-09-30 08:04:06 -04:00
Wilson E. Alvarez fb26b79482
Update EditorMainScreen calls after its extraction
Due to upstream change:

	5e1c9d68aa
2024-09-30 08:04:06 -04:00
25 changed files with 119 additions and 1153 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

@ -168,36 +168,6 @@ jobs:
arch: x86_32 arch: x86_32
should-build: ${{ !inputs.test-build }} should-build: ${{ !inputs.test-build }}
- name: 🍏 iOS (arm64, release)
runner: macos-latest
platform: ios
target: template_release
arch: arm64
should-build: ${{ !inputs.test-build }}
- name: 🍏 iOS (arm64, debug)
runner: macos-latest
platform: ios
target: template_debug
arch: arm64
should-build: ${{ !inputs.test-build }}
- name: 🍏 iOS (simulator, release)
runner: macos-latest
platform: ios
target: template_release
arch: universal
scons-flags: ios_simulator=yes
should-build: ${{ !inputs.test-build }}
- name: 🍏 iOS (simulator, debug)
runner: macos-latest
platform: ios
target: template_debug
arch: universal
scons-flags: ios_simulator=yes
should-build: ${{ !inputs.test-build }}
exclude: exclude:
- { opts: { should-build: false } } - { opts: { should-build: false } }
@ -301,7 +271,7 @@ jobs:
DEBUG_FLAGS: ${{ inputs.debug-symbols && 'debug_symbols=yes symbols_visibility=visible' || 'debug_symbols=no' }} DEBUG_FLAGS: ${{ inputs.debug-symbols && 'debug_symbols=yes symbols_visibility=visible' || 'debug_symbols=no' }}
run: | run: |
PATH=${GITHUB_WORKSPACE}/buildroot/bin:$PATH PATH=${GITHUB_WORKSPACE}/buildroot/bin:$PATH
scons platform=${{matrix.opts.platform}} target=${{matrix.opts.target}} arch=${{matrix.opts.arch}} ${{env.DEBUG_FLAGS}} ${{matrix.opts.scons-flags}} ${{env.SCONSFLAGS}} scons platform=${{matrix.opts.platform}} target=${{matrix.opts.target}} arch=${{matrix.opts.arch}} ${{env.DEBUG_FLAGS}} ${{env.SCONSFLAGS}}
- name: Prepare artifact - name: Prepare artifact
shell: bash shell: bash

View File

@ -136,14 +136,7 @@ jobs:
- name: Set up Vulkan SDK - name: Set up Vulkan SDK
run: | run: |
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being. sh misc/scripts/install_vulkan_sdk_macos.sh
#sh misc/scripts/install_vulkan_sdk_macos.sh
curl -L "https://sdk.lunarg.com/sdk/download/latest/mac/vulkan-sdk.zip" -o /tmp/vulkan-sdk.zip
unzip /tmp/vulkan-sdk.zip -d /tmp
/tmp/InstallVulkan.app/Contents/MacOS/InstallVulkan --accept-licenses --default-answer --confirm-command install
rm -Rf /tmp/InstallVulkan.app
rm -f /tmp/vulkan-sdk.zip
- name: Set up scons cache - name: Set up scons cache
uses: actions/cache@v4 uses: actions/cache@v4
@ -199,14 +192,7 @@ jobs:
- name: Set up Vulkan SDK - name: Set up Vulkan SDK
run: | run: |
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being. sh misc/scripts/install_vulkan_sdk_macos.sh
#sh misc/scripts/install_vulkan_sdk_macos.sh
curl -L "https://sdk.lunarg.com/sdk/download/latest/mac/vulkan-sdk.zip" -o /tmp/vulkan-sdk.zip
unzip /tmp/vulkan-sdk.zip -d /tmp
/tmp/InstallVulkan.app/Contents/MacOS/InstallVulkan --accept-licenses --default-answer --confirm-command install
rm -Rf /tmp/InstallVulkan.app
rm -f /tmp/vulkan-sdk.zip
- name: Download templates artifact - name: Download templates artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4

View File

@ -165,14 +165,7 @@ jobs:
- name: Set up Vulkan SDK - name: Set up Vulkan SDK
run: | run: |
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being. sh misc/scripts/install_vulkan_sdk_macos.sh
#sh misc/scripts/install_vulkan_sdk_macos.sh
curl -L "https://sdk.lunarg.com/sdk/download/latest/mac/vulkan-sdk.zip" -o /tmp/vulkan-sdk.zip
unzip /tmp/vulkan-sdk.zip -d /tmp
/tmp/InstallVulkan.app/Contents/MacOS/InstallVulkan --accept-licenses --default-answer --confirm-command install
rm -Rf /tmp/InstallVulkan.app
rm -f /tmp/vulkan-sdk.zip
- name: Set up scons cache - name: Set up scons cache
uses: actions/cache@v4 uses: actions/cache@v4

View File

@ -26,7 +26,7 @@ concurrency:
# Global Settings. # Global Settings.
env: env:
GODOT_REF: "4.3" GODOT_REF: "4.3-stable"
GODOT_CPP_REF: "godot-4.3-stable" GODOT_CPP_REF: "godot-4.3-stable"
jobs: jobs:
@ -101,18 +101,6 @@ jobs:
run: | run: |
bin/${{ env.BIN }} --test --headless bin/${{ env.BIN }} --test --headless
static-checks:
name: ⚙️ Static checks
runs-on: ubuntu-20.04
steps:
- name: Clone LimboAI module
uses: actions/checkout@v4
- name: Code style checks
uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
cache-env: cache-env:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:

View File

@ -172,8 +172,9 @@ void BTTask::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node
get_child(i)->initialize(p_agent, p_blackboard, p_scene_root); get_child(i)->initialize(p_agent, p_blackboard, p_scene_root);
} }
if (!GDVIRTUAL_CALL(_setup)) {
_setup(); _setup();
GDVIRTUAL_CALL(_setup); }
} }
Ref<BTTask> BTTask::clone() const { Ref<BTTask> BTTask::clone() const {
@ -181,48 +182,61 @@ Ref<BTTask> BTTask::clone() const {
// * Children are duplicated via children property. See _set_children(). // * Children are duplicated via children property. See _set_children().
// * Make BBParam properties unique.
HashMap<Ref<Resource>, Ref<Resource>> duplicates;
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
// Make BBParam properties unique.
List<PropertyInfo> props; List<PropertyInfo> props;
inst->get_property_list(&props); inst->get_property_list(&props);
HashMap<Ref<Resource>, Ref<Resource>> duplicates;
for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) { for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
PropertyInfo prop = E->get(); if (!(E->get().usage & PROPERTY_USAGE_STORAGE)) {
#elif LIMBOAI_GDEXTENSION
TypedArray<Dictionary> props = inst->get_property_list();
for (int i = 0; i < props.size(); i++) {
PropertyInfo prop = PropertyInfo::from_dict(props[i]);
#endif
if (!(prop.usage & PROPERTY_USAGE_STORAGE)) {
continue; continue;
} }
Variant prop_value = inst->get(prop.name); Variant v = inst->get(E->get().name);
Ref<Resource> res = prop_value;
if (v.is_ref_counted()) {
Ref<RefCounted> ref = v;
if (ref.is_valid()) {
Ref<Resource> res = ref;
if (res.is_valid() && res->is_class("BBParam")) { if (res.is_valid() && res->is_class("BBParam")) {
// Duplicate BBParam
if (!duplicates.has(res)) { if (!duplicates.has(res)) {
duplicates[res] = res->duplicate(); duplicates[res] = res->duplicate();
} }
res = duplicates[res]; res = duplicates[res];
inst->set(prop.name, res); inst->set(E->get().name, res);
} else if (prop_value.get_type() == Variant::ARRAY) {
// Duplicate BBParams instances inside an array.
// - This code doesn't handle arrays of arrays.
// - A partial workaround for: https://github.com/godotengine/godot/issues/74918
// - We actually don't want to duplicate resources in clone() except for BBParam subtypes.
Array arr = prop_value;
if (arr.is_typed() && ClassDB::is_parent_class(arr.get_typed_class_name(), LW_NAME(BBParam))) {
for (int j = 0; j < arr.size(); j++) {
Ref<Resource> bb_param = arr[j];
if (bb_param.is_valid()) {
arr[j] = bb_param->duplicate();
} }
} }
} }
} }
#elif LIMBOAI_GDEXTENSION
// Make BBParam properties unique.
TypedArray<Dictionary> props = inst->get_property_list();
HashMap<Ref<Resource>, Ref<Resource>> duplicates;
for (int i = 0; i < props.size(); i++) {
Dictionary prop = props[i];
if (!(int(prop["usage"]) & PROPERTY_USAGE_STORAGE)) {
continue;
} }
StringName prop_name = prop["name"];
Variant v = inst->get(prop_name);
if (v.get_type() == Variant::OBJECT && int(prop["hint"]) == PROPERTY_HINT_RESOURCE_TYPE) {
Ref<RefCounted> ref = v;
if (ref.is_valid()) {
Ref<Resource> res = ref;
if (res.is_valid() && res->is_class("BBParam")) {
if (!duplicates.has(res)) {
duplicates[res] = res->duplicate();
}
res = duplicates[res];
inst->set(prop_name, res);
}
}
}
}
#endif // LIMBOAI_MODULE & LIMBOAI_GDEXTENSION
return inst; return inst;
} }
@ -234,9 +248,9 @@ BT::Status BTTask::execute(double p_delta) {
data.children.get(i)->abort(); data.children.get(i)->abort();
} }
} }
// First native, then script. if (!GDVIRTUAL_CALL(_enter)) {
_enter(); _enter();
GDVIRTUAL_CALL(_enter); }
} else { } else {
data.elapsed += p_delta; data.elapsed += p_delta;
} }
@ -246,9 +260,9 @@ BT::Status BTTask::execute(double p_delta) {
} }
if (data.status != RUNNING) { if (data.status != RUNNING) {
// First script, then native. if (!GDVIRTUAL_CALL(_exit)) {
GDVIRTUAL_CALL(_exit);
_exit(); _exit();
}
data.elapsed = 0.0; data.elapsed = 0.0;
} }
return data.status; return data.status;
@ -259,10 +273,10 @@ void BTTask::abort() {
get_child(i)->abort(); get_child(i)->abort();
} }
if (data.status == RUNNING) { if (data.status == RUNNING) {
// First script, then native. if (!GDVIRTUAL_CALL(_exit)) {
GDVIRTUAL_CALL(_exit);
_exit(); _exit();
} }
}
data.status = FRESH; data.status = FRESH;
data.elapsed = 0.0; data.elapsed = 0.0;
} }

View File

@ -126,7 +126,7 @@ Returns a Blackboard that serves as the parent scope for this instance.
``Variant`` **get_var**\ (\ var_name\: ``StringName``, default\: ``Variant`` = null, complain\: ``bool`` = true\ ) |const| :ref:`🔗<class_Blackboard_method_get_var>` ``Variant`` **get_var**\ (\ var_name\: ``StringName``, default\: ``Variant`` = null, complain\: ``bool`` = true\ ) |const| :ref:`🔗<class_Blackboard_method_get_var>`
Returns variable value or ``default`` if variable doesn't exist. If ``complain`` is ``true``, an error will be printed if variable doesn't exist. If the variable doesn't exist in the current **Blackboard** scope, it will look in the parent scope **Blackboard** to find it. Returns variable value or ``default`` if variable doesn't exist. If ``complain`` is ``true``, an error will be printed if variable doesn't exist.
.. rst-class:: classref-item-separator .. rst-class:: classref-item-separator
@ -212,7 +212,7 @@ Assigns the parent scope. If a value isn't in the current Blackboard scope, it w
|void| **set_var**\ (\ var_name\: ``StringName``, value\: ``Variant``\ ) :ref:`🔗<class_Blackboard_method_set_var>` |void| **set_var**\ (\ var_name\: ``StringName``, value\: ``Variant``\ ) :ref:`🔗<class_Blackboard_method_set_var>`
Assigns a value to a variable in the current Blackboard scope. If the variable doesn't exist, it will be created. If the variable already exists in the parent scope, the parent scope value will NOT be changed. Assigns a value to a Blackboard variable.
.. rst-class:: classref-item-separator .. rst-class:: classref-item-separator

View File

@ -45,27 +45,27 @@ Methods
.. table:: .. table::
:widths: auto :widths: auto
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`add_transition<class_LimboHSM_method_add_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, to_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``, guard\: ``Callable`` = Callable()\ ) | | |void| | :ref:`add_transition<class_LimboHSM_method_add_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, to_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`change_active_state<class_LimboHSM_method_change_active_state>`\ (\ state\: :ref:`LimboState<class_LimboState>`\ ) | | |void| | :ref:`change_active_state<class_LimboHSM_method_change_active_state>`\ (\ state\: :ref:`LimboState<class_LimboState>`\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| :ref:`LimboState<class_LimboState>` | :ref:`get_active_state<class_LimboHSM_method_get_active_state>`\ (\ ) |const| | | :ref:`LimboState<class_LimboState>` | :ref:`get_active_state<class_LimboHSM_method_get_active_state>`\ (\ ) |const| |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| :ref:`LimboState<class_LimboState>` | :ref:`get_leaf_state<class_LimboHSM_method_get_leaf_state>`\ (\ ) |const| | | :ref:`LimboState<class_LimboState>` | :ref:`get_leaf_state<class_LimboHSM_method_get_leaf_state>`\ (\ ) |const| |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| :ref:`LimboState<class_LimboState>` | :ref:`get_previous_active_state<class_LimboHSM_method_get_previous_active_state>`\ (\ ) |const| | | :ref:`LimboState<class_LimboState>` | :ref:`get_previous_active_state<class_LimboHSM_method_get_previous_active_state>`\ (\ ) |const| |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ``bool`` | :ref:`has_transition<class_LimboHSM_method_has_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) |const| | | ``bool`` | :ref:`has_transition<class_LimboHSM_method_has_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) |const| |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`initialize<class_LimboHSM_method_initialize>`\ (\ agent\: ``Node``, parent_scope\: :ref:`Blackboard<class_Blackboard>` = null\ ) | | |void| | :ref:`initialize<class_LimboHSM_method_initialize>`\ (\ agent\: ``Node``, parent_scope\: :ref:`Blackboard<class_Blackboard>` = null\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`remove_transition<class_LimboHSM_method_remove_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) | | |void| | :ref:`remove_transition<class_LimboHSM_method_remove_transition>`\ (\ from_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`set_active<class_LimboHSM_method_set_active>`\ (\ active\: ``bool``\ ) | | |void| | :ref:`set_active<class_LimboHSM_method_set_active>`\ (\ active\: ``bool``\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |void| | :ref:`update<class_LimboHSM_method_update>`\ (\ delta\: ``float``\ ) | | |void| | :ref:`update<class_LimboHSM_method_update>`\ (\ delta\: ``float``\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. rst-class:: classref-section-separator .. rst-class:: classref-section-separator
@ -191,16 +191,9 @@ Method Descriptions
.. rst-class:: classref-method .. rst-class:: classref-method
|void| **add_transition**\ (\ from_state\: :ref:`LimboState<class_LimboState>`, to_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``, guard\: ``Callable`` = Callable()\ ) :ref:`🔗<class_LimboHSM_method_add_transition>` |void| **add_transition**\ (\ from_state\: :ref:`LimboState<class_LimboState>`, to_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) :ref:`🔗<class_LimboHSM_method_add_transition>`
Establishes a transition from one state to another when ``event`` is dispatched. Both ``from_state`` and ``to_state`` must be immediate children of this **LimboHSM**. Establishes a transition from one state to another when ``event`` is dispatched. Both ``from_state`` and ``to_state`` must be immediate children of this state.
Optionally, a ``guard`` function can be specified, which must return a boolean value. If the guard function returns ``false``, the transition will not occur. The guard function is called immediately before the transition is considered. For a state-wide guard function, check out :ref:`LimboState.set_guard<class_LimboState_method_set_guard>`.
::
func my_guard() -> bool:
return is_some_condition_met()
.. rst-class:: classref-item-separator .. rst-class:: classref-item-separator

View File

@ -46,7 +46,7 @@
<param index="1" name="default" type="Variant" default="null" /> <param index="1" name="default" type="Variant" default="null" />
<param index="2" name="complain" type="bool" default="true" /> <param index="2" name="complain" type="bool" default="true" />
<description> <description>
Returns variable value or [param default] if variable doesn't exist. If [param complain] is [code]true[/code], an error will be printed if variable doesn't exist. If the variable doesn't exist in the current [Blackboard] scope, it will look in the parent scope [Blackboard] to find it. Returns variable value or [param default] if variable doesn't exist. If [param complain] is [code]true[/code], an error will be printed if variable doesn't exist.
</description> </description>
</method> </method>
<method name="get_vars_as_dict" qualifiers="const"> <method name="get_vars_as_dict" qualifiers="const">
@ -98,7 +98,7 @@
<param index="0" name="var_name" type="StringName" /> <param index="0" name="var_name" type="StringName" />
<param index="1" name="value" type="Variant" /> <param index="1" name="value" type="Variant" />
<description> <description>
Assigns a value to a variable in the current Blackboard scope. If the variable doesn't exist, it will be created. If the variable already exists in the parent scope, the parent scope value will NOT be changed. Assigns a value to a Blackboard variable.
</description> </description>
</method> </method>
<method name="top" qualifiers="const"> <method name="top" qualifiers="const">

View File

@ -14,14 +14,8 @@
<param index="0" name="from_state" type="LimboState" /> <param index="0" name="from_state" type="LimboState" />
<param index="1" name="to_state" type="LimboState" /> <param index="1" name="to_state" type="LimboState" />
<param index="2" name="event" type="StringName" /> <param index="2" name="event" type="StringName" />
<param index="3" name="guard" type="Callable" default="Callable()" />
<description> <description>
Establishes a transition from one state to another when [param event] is dispatched. Both [param from_state] and [param to_state] must be immediate children of this [LimboHSM]. Establishes a transition from one state to another when [param event] is dispatched. Both [param from_state] and [param to_state] must be immediate children of this state.
Optionally, a [param guard] function can be specified, which must return a boolean value. If the guard function returns [code]false[/code], the transition will not occur. The guard function is called immediately before the transition is considered. For a state-wide guard function, check out [method LimboState.set_guard].
[codeblock]
func my_guard() -&gt; bool:
return is_some_condition_met()
[/codeblock]
</description> </description>
</method> </method>
<method name="change_active_state"> <method name="change_active_state">

View File

@ -287,14 +287,14 @@ void EditorPropertyBBParam::update_property() {
variable_editor->update_property(); variable_editor->update_property();
variable_editor->show(); variable_editor->show();
bottom_container->hide(); bottom_container->hide();
type_choice->set_button_icon(get_editor_theme_icon(SNAME("LimboExtraVariable"))); type_choice->set_icon(get_editor_theme_icon(SNAME("LimboExtraVariable")));
} else { } else {
_create_value_editor(param->get_type()); _create_value_editor(param->get_type());
variable_editor->hide(); variable_editor->hide();
value_editor->show(); value_editor->show();
value_editor->set_object_and_property(param.ptr(), SNAME("saved_value")); value_editor->set_object_and_property(param.ptr(), SNAME("saved_value"));
value_editor->update_property(); value_editor->update_property();
type_choice->set_button_icon(get_editor_theme_icon(Variant::get_type_name(param->get_type()))); type_choice->set_icon(get_editor_theme_icon(Variant::get_type_name(param->get_type())));
} }
} }
@ -316,7 +316,7 @@ void EditorPropertyBBParam::_notification(int p_what) {
{ {
String type = Variant::get_type_name(_get_edited_param()->get_type()); String type = Variant::get_type_name(_get_edited_param()->get_type());
type_choice->set_button_icon(get_editor_theme_icon(type)); type_choice->set_icon(get_editor_theme_icon(type));
} }
// Initialize type choice. // Initialize type choice.

View File

@ -261,10 +261,6 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
p_behavior_tree->editor_set_section_unfold("blackboard_plan", true); p_behavior_tree->editor_set_section_unfold("blackboard_plan", true);
p_behavior_tree->notify_property_list_changed(); p_behavior_tree->notify_property_list_changed();
#endif // LIMBOAI_MODULE #endif // LIMBOAI_MODULE
// Remember current search info.
if (idx_history >= 0 && idx_history < history.size() && task_tree->get_bt() == history[idx_history]) {
tab_search_context.insert(history[idx_history], task_tree->tree_search_get_search_info());
}
task_tree->load_bt(p_behavior_tree); task_tree->load_bt(p_behavior_tree);
@ -284,15 +280,6 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
task_tree->show(); task_tree->show();
task_palette->show(); task_palette->show();
// Restore search info from [tab_search_context].
if (idx_history >= 0 && idx_history < history.size()) {
if (tab_search_context.has(history[idx_history])) {
task_tree->tree_search_set_search_info(tab_search_context[history[idx_history]]);
} else {
task_tree->tree_search_set_search_info(TreeSearch::SearchInfo());
}
}
_update_tabs(); _update_tabs();
} }
@ -470,8 +457,6 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
_on_save_pressed(); _on_save_pressed();
} else if (LW_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) { } else if (LW_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) {
_popup_file_dialog(load_dialog); _popup_file_dialog(load_dialog);
} else if (LW_IS_SHORTCUT("limbo_ai/find_task", p_event)) {
task_tree->tree_search_show_and_focus();
} else { } else {
handled = false; handled = false;
} }
@ -814,9 +799,6 @@ void LimboAIEditor::_misc_option_selected(int p_id) {
EDITOR_FILE_SYSTEM()->scan(); EDITOR_FILE_SYSTEM()->scan();
EDIT_SCRIPT(template_path); EDIT_SCRIPT(template_path);
} break; } break;
case MISC_SEARCH_TREE: {
task_tree->tree_search_show_and_focus();
} break;
} }
} }
@ -1063,22 +1045,13 @@ void LimboAIEditor::_tab_closed(int 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::_mark_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::_mark_as_dirty));
} }
if (tab_search_context.has(history_bt)) {
tab_search_context.erase(history_bt);
}
history.remove_at(p_tab); history.remove_at(p_tab);
idx_history = MIN(idx_history, history.size() - 1); idx_history = MIN(idx_history, history.size() - 1);
TreeSearch::SearchInfo search_info_opened_tab;
if (idx_history < 0) { if (idx_history < 0) {
_disable_editing(); _disable_editing();
} else { } else {
EDIT_RESOURCE(history[idx_history]); EDIT_RESOURCE(history[idx_history]);
ERR_FAIL_COND(!tab_search_context.has(history[idx_history]));
search_info_opened_tab = tab_search_context[history[idx_history]];
} }
task_tree->tree_search_set_search_info(search_info_opened_tab);
_update_tabs(); _update_tabs();
} }
@ -1346,9 +1319,6 @@ void LimboAIEditor::_update_misc_menu() {
misc_menu->add_item( misc_menu->add_item(
FILE_EXISTS(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("Create Script Template"), FILE_EXISTS(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("Create Script Template"),
MISC_CREATE_SCRIPT_TEMPLATE); MISC_CREATE_SCRIPT_TEMPLATE);
misc_menu->add_separator();
misc_menu->add_icon_shortcut(theme_cache.search_icon, LW_GET_SHORTCUT("limbo_ai/find_task"), MISC_SEARCH_TREE);
} }
void LimboAIEditor::_update_banners() { void LimboAIEditor::_update_banners() {
@ -1411,7 +1381,6 @@ void LimboAIEditor::_do_update_theme_item_cache() {
theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), 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.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.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons));
theme_cache.search_icon = get_theme_icon(LW_NAME(Search), 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");
@ -1543,8 +1512,6 @@ LimboAIEditor::LimboAIEditor() {
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(J)));
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/hide_tree_search", TTR("Close Search"), (Key)(LW_KEY(ESCAPE)));
set_process_shortcut_input(true); set_process_shortcut_input(true);

View File

@ -20,7 +20,6 @@
#include "owner_picker.h" #include "owner_picker.h"
#include "task_palette.h" #include "task_palette.h"
#include "task_tree.h" #include "task_tree.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
#include "core/object/class_db.h" #include "core/object/class_db.h"
@ -50,7 +49,6 @@
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
#include "godot_cpp/classes/accept_dialog.hpp" #include "godot_cpp/classes/accept_dialog.hpp"
#include <godot_cpp/classes/config_file.hpp>
#include <godot_cpp/classes/control.hpp> #include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/editor_plugin.hpp> #include <godot_cpp/classes/editor_plugin.hpp>
#include <godot_cpp/classes/editor_spin_slider.hpp> #include <godot_cpp/classes/editor_spin_slider.hpp>
@ -67,6 +65,7 @@
#include <godot_cpp/classes/texture2d.hpp> #include <godot_cpp/classes/texture2d.hpp>
#include <godot_cpp/variant/packed_string_array.hpp> #include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/variant.hpp> #include <godot_cpp/variant/variant.hpp>
#include <godot_cpp/classes/config_file.hpp>
using namespace godot; using namespace godot;
@ -103,7 +102,6 @@ private:
MISC_LAYOUT_WIDESCREEN_OPTIMIZED, MISC_LAYOUT_WIDESCREEN_OPTIMIZED,
MISC_PROJECT_SETTINGS, MISC_PROJECT_SETTINGS,
MISC_CREATE_SCRIPT_TEMPLATE, MISC_CREATE_SCRIPT_TEMPLATE,
MISC_SEARCH_TREE
}; };
enum TabMenu { enum TabMenu {
@ -138,14 +136,12 @@ private:
Ref<Texture2D> cut_icon; Ref<Texture2D> cut_icon;
Ref<Texture2D> copy_icon; Ref<Texture2D> copy_icon;
Ref<Texture2D> paste_icon; Ref<Texture2D> paste_icon;
Ref<Texture2D> search_icon;
} theme_cache; } theme_cache;
EditorPlugin *plugin; EditorPlugin *plugin;
EditorLayout editor_layout; EditorLayout editor_layout;
Vector<Ref<BehaviorTree>> history; Vector<Ref<BehaviorTree>> history;
int idx_history; int idx_history;
HashMap<Ref<BehaviorTree>, TreeSearch::SearchInfo> tab_search_context;
bool updating_tabs = false; bool updating_tabs = false;
bool request_update_tabs = false; bool request_update_tabs = false;
HashSet<Ref<BehaviorTree>> dirty; HashSet<Ref<BehaviorTree>> dirty;

View File

@ -17,23 +17,22 @@
#include "../bt/tasks/composites/bt_probability_selector.h" #include "../bt/tasks/composites/bt_probability_selector.h"
#include "../util/limbo_compat.h" #include "../util/limbo_compat.h"
#include "../util/limbo_utility.h" #include "../util/limbo_utility.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
#include "core/object/script_language.h" #include "core/object/script_language.h"
#include "editor/themes/editor_scale.h" #include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h" #include "scene/gui/box_container.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h" #include "scene/gui/texture_rect.h"
#include "scene/gui/label.h"
#endif // LIMBOAI_MODULE #endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp> #include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/label.hpp>
#include <godot_cpp/classes/script.hpp> #include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/texture_rect.hpp> #include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/v_box_container.hpp> #include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/label.hpp>
using namespace godot; using namespace godot;
#endif // LIMBOAI_GDEXTENSION #endif // LIMBOAI_GDEXTENSION
@ -47,12 +46,6 @@ TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent,
_create_tree(p_task->get_child(i), item); _create_tree(p_task->get_child(i), item);
} }
_update_item(item); _update_item(item);
// update TreeSearch if root task was created
if (tree->get_root() == item) {
tree_search->update_search(tree);
}
return item; return item;
} }
@ -112,7 +105,6 @@ void TaskTree::_update_item(TreeItem *p_item) {
if (!warning_text.is_empty()) { if (!warning_text.is_empty()) {
p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text); p_item->add_button(0, theme_cache.task_warning_icon, 0, false, warning_text);
} }
tree_search->notify_item_edited(p_item); // this is necessary to preserve custom drawing from tree search.
} }
void TaskTree::_update_tree() { void TaskTree::_update_tree() {
@ -538,8 +530,6 @@ void TaskTree::_notification(int p_what) {
tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED); tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED);
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated)); tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed)); tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
tree_search_panel->connect("update_requested", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
tree_search_panel->connect("visibility_changed", callable_mp(tree_search.ptr(), &TreeSearch::update_search).bind(tree));
} break; } break;
case NOTIFICATION_THEME_CHANGED: { case NOTIFICATION_THEME_CHANGED: {
_do_update_theme_item_cache(); _do_update_theme_item_cache();
@ -572,38 +562,12 @@ void TaskTree::_bind_methods() {
PropertyInfo(Variant::INT, "type"))); PropertyInfo(Variant::INT, "type")));
} }
// TreeSearch API
void TaskTree::tree_search_show_and_focus() {
ERR_FAIL_NULL(tree_search);
tree_search_panel->set_visible(true);
tree_search_panel->focus_editor();
}
TreeSearch::SearchInfo TaskTree::tree_search_get_search_info() const {
if (!tree_search.is_valid()) {
return TreeSearch::SearchInfo();
}
return tree_search_panel->get_search_info();
}
void TaskTree::tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info) {
ERR_FAIL_NULL(tree_search);
tree_search_panel->set_search_info(p_search_info);
}
// TreeSearch Api ^
TaskTree::TaskTree() { TaskTree::TaskTree() {
editable = true; editable = true;
updating_tree = false; updating_tree = false;
VBoxContainer *vbox_container = memnew(VBoxContainer);
add_child(vbox_container);
vbox_container->set_anchors_preset(PRESET_FULL_RECT);
tree = memnew(Tree); tree = memnew(Tree);
tree->set_v_size_flags(Control::SIZE_EXPAND_FILL); add_child(tree);
vbox_container->add_child(tree);
tree->set_columns(2); tree->set_columns(2);
tree->set_column_expand(0, true); tree->set_column_expand(0, true);
tree->set_column_expand(1, false); tree->set_column_expand(1, false);
@ -614,10 +578,6 @@ TaskTree::TaskTree() {
tree->set_select_mode(Tree::SelectMode::SELECT_MULTI); tree->set_select_mode(Tree::SelectMode::SELECT_MULTI);
tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw)); tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw));
tree_search_panel = memnew(TreeSearchPanel);
tree_search = Ref(memnew(TreeSearch(tree_search_panel)));
vbox_container->add_child(tree_search_panel);
} }
TaskTree::~TaskTree() { TaskTree::~TaskTree() {

View File

@ -9,13 +9,9 @@
* ============================================================================= * =============================================================================
*/ */
#ifndef TASK_TREE_H
#define TASK_TREE_H
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
#include "../bt/behavior_tree.h" #include "../bt/behavior_tree.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
#include "scene/gui/control.h" #include "scene/gui/control.h"
@ -47,9 +43,6 @@ private:
bool updating_tree; bool updating_tree;
HashMap<RECT_CACHE_KEY, Rect2> probability_rect_cache; HashMap<RECT_CACHE_KEY, Rect2> probability_rect_cache;
Ref<TreeSearch> tree_search;
TreeSearchPanel *tree_search_panel;
struct ThemeCache { struct ThemeCache {
Ref<Font> comment_font; Ref<Font> comment_font;
Ref<Font> name_font; Ref<Font> name_font;
@ -103,16 +96,12 @@ public:
Ref<BTTask> get_selected() const; Ref<BTTask> get_selected() const;
Vector<Ref<BTTask>> get_selected_tasks() const; Vector<Ref<BTTask>> get_selected_tasks() const;
void clear_selection(); void clear_selection();
Rect2 get_selected_probability_rect() const; Rect2 get_selected_probability_rect() const;
double get_selected_probability_weight() const; double get_selected_probability_weight() const;
double get_selected_probability_percent() const; double get_selected_probability_percent() const;
bool selected_has_probability() const; bool selected_has_probability() const;
// TreeSearch API
void tree_search_show_and_focus();
TreeSearch::SearchInfo tree_search_get_search_info() const;
void tree_search_set_search_info(const TreeSearch::SearchInfo &p_search_info);
virtual bool editor_can_reload_from_file() { return false; } virtual bool editor_can_reload_from_file() { return false; }
TaskTree(); TaskTree();
@ -120,4 +109,3 @@ public:
}; };
#endif // ! TOOLS_ENABLED #endif // ! TOOLS_ENABLED
#endif // ! TASK_TREE_H

View File

@ -1,649 +0,0 @@
/**
* tree_search.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.
* =============================================================================
*/
#ifdef TOOLS_ENABLED
#include "tree_search.h"
#include "../util/limbo_compat.h" // for edscale
#include "../util/limbo_string_names.h"
#include "../util/limbo_utility.h"
#ifdef LIMBOAI_MODULE
#include "core/math/math_funcs.h"
#include "editor/editor_interface.h"
#include "editor/themes/editor_scale.h"
#include "scene/main/viewport.h"
#include "scene/resources/font.h"
#include "scene/resources/style_box_flat.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp> // for edge scale
#include <godot_cpp/classes/font.hpp>
#include <godot_cpp/classes/style_box_flat.hpp>
#include <godot_cpp/classes/viewport.hpp>
#include <godot_cpp/core/math.hpp>
#endif // LIMBOAI_GDEXTENSION
#define UPPER_BOUND (1 << 15) // for substring search.
/* ------- TreeSearch ------- */
void TreeSearch::_clean_callable_cache() {
ERR_FAIL_COND(!tree_reference);
HashMap<TreeItem *, Callable> new_callable_cache;
new_callable_cache.reserve(callable_cache.size());
for (int i = 0; i < ordered_tree_items.size(); i++) {
TreeItem *cur_item = ordered_tree_items[i];
if (callable_cache.has(cur_item)) {
new_callable_cache[cur_item] = callable_cache[cur_item];
}
}
callable_cache = new_callable_cache;
}
void TreeSearch::_filter_tree() {
ERR_FAIL_COND(!tree_reference);
if (!tree_reference->get_root()) {
return;
}
if (matching_entries.is_empty()) {
return;
}
_filter_tree(tree_reference->get_root(), false);
}
void TreeSearch::_filter_tree(TreeItem *p_item, bool p_parent_matching) {
bool visible = (number_matches.has(p_item) && (number_matches.get(p_item) > 0)) || p_parent_matching;
p_item->set_visible(visible);
bool is_matching = _vector_has_bsearch(matching_entries, p_item);
for (int i = 0; i < p_item->get_child_count(); i++) {
_filter_tree(p_item->get_child(i), is_matching | p_parent_matching);
}
}
// Makes all tree items visible.
void TreeSearch::_clear_filter() {
ERR_FAIL_COND(!tree_reference);
if (!tree_reference->get_root()) {
return;
}
Vector<TreeItem *> items = { tree_reference->get_root() };
for (int idx = 0; idx < items.size(); idx++) {
TreeItem *cur_item = items[idx];
cur_item->set_visible(true);
for (int i = 0; i < cur_item->get_child_count(); i++) {
items.push_back(cur_item->get_child(i));
}
}
}
void TreeSearch::_highlight_tree() {
ERR_FAIL_COND(!tree_reference);
for (HashMap<TreeItem *, int>::Iterator it = number_matches.begin(); it != number_matches.end(); ++it) {
TreeItem *tree_item = it->key;
_highlight_tree_item(tree_item);
}
tree_reference->queue_redraw();
}
void TreeSearch::_highlight_tree_item(TreeItem *p_tree_item) {
int num_m = number_matches.has(p_tree_item) ? number_matches.get(p_tree_item) : 0;
if (num_m == 0) {
return;
}
// Make sure to also call any draw method already defined.
Callable parent_draw_method;
if (p_tree_item->get_cell_mode(0) == TreeItem::CELL_MODE_CUSTOM) {
parent_draw_method = p_tree_item->get_custom_draw_callback(0);
}
// If the cached draw method is already applied, do nothing.
if (callable_cache.has(p_tree_item) && parent_draw_method == callable_cache.get(p_tree_item)) {
return;
}
Callable draw_callback = callable_mp(this, &TreeSearch::_draw_highlight_item).bind(parent_draw_method);
callable_cache[p_tree_item] = draw_callback;
// This is necessary because of the modularity of this implementation.
// Cache render properties of entry.
String cached_text = p_tree_item->get_text(0);
Ref<Texture2D> cached_icon = p_tree_item->get_icon(0);
int cached_max_width = p_tree_item->get_icon_max_width(0);
// This removes render properties in entry.
p_tree_item->set_custom_draw_callback(0, draw_callback);
p_tree_item->set_cell_mode(0, TreeItem::CELL_MODE_CUSTOM);
// Restore render properties.
p_tree_item->set_text(0, cached_text);
p_tree_item->set_icon(0, cached_icon);
p_tree_item->set_icon_max_width(0, cached_max_width);
}
// Custom draw callback for highlighting (bind the parent_draw_method to this)
void TreeSearch::_draw_highlight_item(TreeItem *p_tree_item, const Rect2 p_rect, const Callable &p_parent_draw_method) {
if (!p_tree_item) {
return;
}
// Call any parent draw methods such as for probability FIRST.
p_parent_draw_method.call(p_tree_item, p_rect);
// First part: outline
if (matching_entries.has(p_tree_item)) {
// Font info
Ref<Font> font = p_tree_item->get_custom_font(0);
if (font.is_null()) {
font = p_tree_item->get_tree()->get_theme_font(LW_NAME(font));
}
ERR_FAIL_NULL(font);
float font_size = p_tree_item->get_custom_font_size(0);
if (font_size == -1) {
font_size = p_tree_item->get_tree()->get_theme_font_size(LW_NAME(font));
}
// Substring size
String string_full = p_tree_item->get_text(0);
StringSearchIndices substring_idx = _substring_bounds(string_full, _get_search_mask());
String substring_match = string_full.substr(substring_idx.lower, substring_idx.upper - substring_idx.lower);
Vector2 substring_match_size = font->get_string_size(substring_match, HORIZONTAL_ALIGNMENT_LEFT, -1.f, font_size);
String substring_before = string_full.substr(0, substring_idx.lower);
Vector2 substring_before_size = font->get_string_size(substring_before, HORIZONTAL_ALIGNMENT_LEFT, -1.f, font_size);
// Stylebox
Ref<StyleBox> stylebox = p_tree_item->get_tree()->get_theme_stylebox(LW_NAME(Focus));
ERR_FAIL_NULL(stylebox);
// Extract separation
float h_sep = p_tree_item->get_tree()->get_theme_constant(LW_NAME(h_separation));
// Compose draw rect
const Vector2 PADDING = Vector2(4., 2.);
Rect2 draw_rect = p_rect;
Vector2 rect_offset = Vector2(substring_before_size.x, 0);
rect_offset.x += p_tree_item->get_icon_max_width(0);
rect_offset.x += (h_sep + 4. * EDSCALE);
rect_offset.y = (p_rect.size.y - substring_match_size.y) / 2; // center box vertically
draw_rect.position += rect_offset - PADDING / 2;
draw_rect.size = substring_match_size + PADDING;
// Draw
stylebox->draw(p_tree_item->get_tree()->get_canvas_item(), draw_rect);
}
// Second part: draw number
int num_mat = number_matches.has(p_tree_item) ? number_matches.get(p_tree_item) : 0;
if (num_mat > 0) {
float h_sep = p_tree_item->get_tree()->get_theme_constant(LW_NAME(h_separation));
Ref<Font> font = tree_reference->get_theme_font(LW_NAME(font));
float font_size = tree_reference->get_theme_font_size(LW_NAME(font)) * 0.75;
String num_string = String::num_int64(num_mat);
Vector2 string_size = font->get_string_size(num_string, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size);
Vector2 text_pos = p_rect.position;
text_pos.x += p_rect.size.x - string_size.x - h_sep;
text_pos.y += font->get_descent(font_size) + p_rect.size.y / 2.; // center vertically
font->draw_string(tree_reference->get_canvas_item(), text_pos, num_string, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size);
}
}
void TreeSearch::_update_matching_entries(const String &p_search_mask) {
Vector<TreeItem *> accum;
_find_matching_entries(tree_reference->get_root(), p_search_mask, accum);
matching_entries = accum;
}
/* Linaerizes the tree into [ordered_tree_items] like so:
- i1
- i2
- i3
- i4 ---> [i1,i2,i3,i4]
*/
void TreeSearch::_update_ordered_tree_items(TreeItem *p_tree_item) {
if (!p_tree_item) {
return;
}
if (p_tree_item == p_tree_item->get_tree()->get_root()) {
ordered_tree_items.clear();
}
// Add the current item to the list.
ordered_tree_items.push_back(p_tree_item);
// Recursively collect items from the first child.
TreeItem *child = p_tree_item->get_first_child();
while (child) {
_update_ordered_tree_items(child);
child = child->get_next();
}
}
void TreeSearch::_update_number_matches() {
ERR_FAIL_COND(!tree_reference);
number_matches.clear();
number_matches.reserve(ordered_tree_items.size());
TreeItem *tree_root = tree_reference->get_root();
if (!tree_root) {
return;
}
_update_number_matches(tree_root);
}
void TreeSearch::_update_number_matches(TreeItem *item) {
ERR_FAIL_COND(!item);
for (int i = 0; i < item->get_child_count(); i++) {
TreeItem *child = item->get_child(i);
_update_number_matches(child);
}
int count = _vector_has_bsearch(matching_entries, item) ? 1 : 0;
for (int i = 0; i < item->get_child_count(); i++) {
TreeItem *child = item->get_child(i);
count += number_matches.has(child) ? number_matches.get(child) : 0;
}
if (count == 0) {
return;
}
number_matches[item] = count;
}
String TreeSearch::_get_search_mask() const {
ERR_FAIL_COND_V(!search_panel, "");
return search_panel->get_text();
}
void TreeSearch::_find_matching_entries(TreeItem *p_tree_item, const String &p_search_mask, Vector<TreeItem *> &p_accum) const {
if (!p_tree_item) {
return;
}
StringSearchIndices item_search_indices = _substring_bounds(p_tree_item->get_text(0), p_search_mask);
if (item_search_indices.hit()) {
p_accum.push_back(p_tree_item);
}
for (int i = 0; i < p_tree_item->get_child_count(); i++) {
TreeItem *child = p_tree_item->get_child(i);
_find_matching_entries(child, p_search_mask, p_accum);
}
// Sort the result if we are at the root.
if (p_tree_item == p_tree_item->get_tree()->get_root()) {
p_accum.sort();
}
return;
}
// Returns the lower and upper bounds of a substring. Does fuzzy search: Simply looks if words exist in right ordering.
// Also ignores case if p_search_mask is lowercase. Example:
// p_searcheable = "TimeLimit 2 sec", p_search_mask = limit 2 sec -> [4,14]. With p_search_mask = "LimiT 2 SEC" or "Limit sec 2" -> [-1,-1]
TreeSearch::StringSearchIndices TreeSearch::_substring_bounds(const String &p_searchable, const String &p_search_mask) const {
StringSearchIndices result;
result.lower = UPPER_BOUND;
result.upper = 0;
if (p_search_mask.is_empty()) {
return result; // Early return if search_mask is empty.
}
// Determine if the search should be case-insensitive.
bool is_case_insensitive = (p_search_mask == p_search_mask.to_lower());
String searchable_processed = is_case_insensitive ? p_searchable.to_lower() : p_searchable;
PackedStringArray words = p_search_mask.split(" ");
int word_position = 0;
for (const String &word : words) {
if (word.is_empty()) {
continue; // Skip empty words.
}
String word_processed = is_case_insensitive ? word.to_lower() : word;
// Find the position of the next word in the searchable string.
word_position = searchable_processed.find(word_processed, word_position);
if (word_position < 0) {
// If any word is not found, return an empty StringSearchIndices.
return StringSearchIndices();
}
// Update lower and upper bounds.
result.lower = MIN(result.lower, word_position);
result.upper = MAX(result.upper, static_cast<int>(word_position + word.length()));
}
return result;
}
void TreeSearch::_select_item(TreeItem *p_item) {
if (!p_item) {
return;
}
ERR_FAIL_COND(!tree_reference || p_item->get_tree() != tree_reference);
// First unfold ancestors
TreeItem *ancestor = p_item->get_parent();
while (ancestor) {
ancestor->set_collapsed(false);
ancestor = ancestor->get_parent();
}
// Then scroll to [item]
tree_reference->scroll_to_item(p_item);
// ...and select it
tree_reference->deselect_all();
tree_reference->set_selected(p_item, 0);
}
void TreeSearch::_select_first_match() {
if (matching_entries.size() == 0) {
return;
}
for (int i = 0; i < ordered_tree_items.size(); i++) {
TreeItem *item = ordered_tree_items[i];
if (!_vector_has_bsearch(matching_entries, item)) {
continue;
}
_select_item(item);
return;
}
}
void TreeSearch::_select_last_match() {
if (matching_entries.size() == 0) {
return;
}
for (int i = ordered_tree_items.size() - 1; i >= 0; i--) {
TreeItem *item = ordered_tree_items[i];
if (!_vector_has_bsearch(matching_entries, item)) {
continue;
}
_select_item(item);
return;
}
}
void TreeSearch::_select_previous_match() {
if (matching_entries.size() == 0) {
return;
}
TreeItem *selected = tree_reference->get_selected();
if (!selected) {
_select_last_match();
return;
}
// Find [selected_idx] among ordered_tree_items.
int selected_idx = 0;
for (int i = ordered_tree_items.size() - 1; i >= 0; i--) {
if (ordered_tree_items[i] == selected) {
selected_idx = i;
break;
}
}
// Find first entry before [selected_idx].
for (int i = MIN(ordered_tree_items.size() - 1, selected_idx) - 1; i >= 0; i--) {
TreeItem *item = ordered_tree_items[i];
if (_vector_has_bsearch(matching_entries, item)) {
_select_item(item);
return;
}
}
// Wrap around.
_select_last_match();
}
void TreeSearch::_select_next_match() {
if (matching_entries.size() == 0) {
return;
}
TreeItem *selected = tree_reference->get_selected();
if (!selected) {
_select_first_match();
return;
}
// Find [selected_idx] among ordered_tree_items
int selected_idx = 0;
for (int i = 0; i < ordered_tree_items.size(); i++) {
if (ordered_tree_items[i] == selected) {
selected_idx = i;
break;
}
}
// Find first entry after [selected_idx].
for (int i = MAX(0, selected_idx) + 1; i < ordered_tree_items.size(); i++) {
TreeItem *item = ordered_tree_items[i];
if (_vector_has_bsearch(matching_entries, item)) {
_select_item(item);
return;
}
}
// Wrap around.
_select_first_match();
}
void TreeSearch::_on_search_panel_closed() {
if (!tree_reference) {
return;
}
tree_reference->grab_focus();
}
template <typename T>
inline bool TreeSearch::_vector_has_bsearch(Vector<T *> &p_vec, T *element) const {
int idx = p_vec.bsearch(element, true);
bool in_array = idx >= 0 && idx < p_vec.size();
return in_array && p_vec[idx] == element;
}
void TreeSearch::notify_item_edited(TreeItem *item) {
if (item->get_cell_mode(0) != TreeItem::CELL_MODE_CUSTOM) {
return;
}
_highlight_tree_item(item);
}
// Called as a post-processing step for the already constructed tree.
void TreeSearch::update_search(Tree *p_tree) {
ERR_FAIL_COND(!search_panel || !p_tree);
tree_reference = p_tree;
if (!tree_reference->get_root()) {
return;
}
if (!search_panel->is_visible() || search_panel->get_text().length() == 0) {
// Clear and redraw if search was active recently.
if (was_searched_recently) {
number_matches.clear();
matching_entries.clear();
_clear_filter();
was_searched_recently = false;
p_tree->queue_redraw();
}
return;
}
was_searched_recently = true;
String search_mask = search_panel->get_text();
TreeSearchMode search_mode = search_panel->get_search_mode();
_update_ordered_tree_items(p_tree->get_root());
_update_matching_entries(search_mask);
_update_number_matches();
_highlight_tree();
if (search_mode == TreeSearchMode::FILTER) {
_filter_tree();
was_filtered_recently = true;
} else if (was_filtered_recently) {
_clear_filter();
was_filtered_recently = false;
}
_clean_callable_cache();
}
TreeSearch::TreeSearch(TreeSearchPanel *p_search_panel) {
search_panel = p_search_panel;
search_panel->connect(LW_NAME(text_submitted), callable_mp(this, &TreeSearch::_select_next_match));
search_panel->connect(LW_NAME(Close), callable_mp(this, &TreeSearch::_on_search_panel_closed));
search_panel->connect("select_previous_match", callable_mp(this, &TreeSearch::_select_previous_match));
}
/* !TreeSearch */
/* ------- TreeSearchPanel ------- */
void TreeSearchPanel::_add_spacer(float p_width_multiplier) {
Control *spacer = memnew(Control);
spacer->set_custom_minimum_size(Vector2(8.0 * EDSCALE * p_width_multiplier, 0.0));
add_child(spacer);
}
void TreeSearchPanel::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_READY: {
// Close callbacks
close_button->connect(LW_NAME(pressed), Callable(this, LW_NAME(set_visible)).bind(false));
close_button->connect(LW_NAME(pressed), Callable(this, LW_NAME(emit_signal)).bind(LW_NAME(Close)));
close_button->set_shortcut(LW_GET_SHORTCUT("limbo_ai/hide_tree_search")); // TODO: use internal shortcut. also sets tooltip...
// Search callbacks
Callable c_update_requested = Callable(this, LW_NAME(emit_signal)).bind("update_requested");
Callable c_text_submitted = Callable(this, LW_NAME(emit_signal)).bind(LW_NAME(text_submitted));
Callable c_select_previous_match = Callable(this, LW_NAME(emit_signal)).bind("select_previous_match");
find_next_button->connect(LW_NAME(pressed), c_text_submitted);
find_prev_button->connect(LW_NAME(pressed), c_select_previous_match);
line_edit_search->connect(LW_NAME(text_changed), c_update_requested.unbind(1));
check_button_filter_highlight->connect(LW_NAME(pressed), c_update_requested);
line_edit_search->connect(LW_NAME(text_submitted), c_text_submitted.unbind(1));
break;
}
case NOTIFICATION_THEME_CHANGED: {
BUTTON_SET_ICON(close_button, get_theme_icon(LW_NAME(Close), LW_NAME(EditorIcons)));
BUTTON_SET_ICON(find_prev_button, get_theme_icon("MoveUp", LW_NAME(EditorIcons)));
BUTTON_SET_ICON(find_next_button, get_theme_icon("MoveDown", LW_NAME(EditorIcons)));
label_filter->set_text(TTR("Filter"));
break;
}
}
}
void TreeSearchPanel::_bind_methods() {
ADD_SIGNAL(MethodInfo("update_requested"));
ADD_SIGNAL(MethodInfo(LW_NAME(text_submitted)));
ADD_SIGNAL(MethodInfo("select_previous_match"));
ADD_SIGNAL(MethodInfo(LW_NAME(Close)));
}
TreeSearchPanel::TreeSearchPanel() {
line_edit_search = memnew(LineEdit);
check_button_filter_highlight = memnew(CheckBox);
close_button = memnew(Button);
find_next_button = memnew(Button);
find_prev_button = memnew(Button);
label_filter = memnew(Label);
line_edit_search->set_placeholder(TTR("Search tree"));
close_button->set_theme_type_variation(LW_NAME(FlatButton));
find_next_button->set_theme_type_variation(LW_NAME(FlatButton));
find_prev_button->set_theme_type_variation(LW_NAME(FlatButton));
find_next_button->set_tooltip_text("Next Match");
find_prev_button->set_tooltip_text("Previous Match");
line_edit_search->set_tooltip_text("Match case if input contains capital letter.");
// Positioning and sizing
set_anchors_and_offsets_preset(LayoutPreset::PRESET_BOTTOM_WIDE);
set_v_size_flags(SIZE_SHRINK_CENTER); // Do not expand vertically
line_edit_search->set_h_size_flags(SIZE_EXPAND_FILL);
_add_spacer(0.1); // -> Otherwise the lineedits expand margin touches the left border.
add_child(line_edit_search);
add_child(find_prev_button);
add_child(find_next_button);
_add_spacer(0.25);
add_child(check_button_filter_highlight);
add_child(label_filter);
_add_spacer(0.25);
add_child(close_button);
set_visible(false);
}
TreeSearch::TreeSearchMode TreeSearchPanel::get_search_mode() const {
if (!check_button_filter_highlight || !check_button_filter_highlight->is_pressed()) {
return TreeSearch::TreeSearchMode::HIGHLIGHT;
}
return TreeSearch::TreeSearchMode::FILTER;
}
String TreeSearchPanel::get_text() const {
return line_edit_search->get_text();
}
TreeSearch::SearchInfo TreeSearchPanel::get_search_info() const {
TreeSearch::SearchInfo result;
result.search_mask = get_text();
result.search_mode = get_search_mode();
result.visible = is_visible();
return result;
}
void TreeSearchPanel::set_search_info(const TreeSearch::SearchInfo &p_search_info) {
line_edit_search->set_text(p_search_info.search_mask);
check_button_filter_highlight->set_pressed(p_search_info.search_mode == TreeSearch::TreeSearchMode::FILTER);
set_visible(p_search_info.visible);
emit_signal("update_requested");
}
void TreeSearchPanel::focus_editor() {
line_edit_search->grab_focus();
}
/* !TreeSearchPanel */
#endif // TOOLS_ENABLED

View File

@ -1,160 +0,0 @@
/**
* tree_search.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.
* =============================================================================
*/
#ifdef TOOLS_ENABLED
#ifndef TREE_SEARCH_H
#define TREE_SEARCH_H
#ifdef LIMBOAI_MODULE
#include "core/templates/hash_map.h"
#include "scene/gui/check_box.h"
#include "scene/gui/flow_container.h"
#include "scene/gui/label.h"
#include "scene/gui/line_edit.h"
#include "scene/gui/tree.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/check_box.hpp>
#include <godot_cpp/classes/h_flow_container.hpp>
#include <godot_cpp/classes/label.hpp>
#include <godot_cpp/classes/line_edit.hpp>
#include <godot_cpp/classes/tree.hpp>
#include <godot_cpp/templates/hash_map.hpp>
#endif // LIMBOAI_GDEXTENSION
using namespace godot;
class TreeSearchPanel;
class TreeSearch : public RefCounted {
GDCLASS(TreeSearch, RefCounted)
private:
struct StringSearchIndices {
// initialize to opposite bounds.
int lower = -1;
int upper = -1;
bool hit() {
return 0 <= lower && lower < upper;
}
};
TreeSearchPanel *search_panel;
// For TaskTree: These are updated when the tree is updated through TaskTree::_create_tree.
Tree *tree_reference;
// Linearized ordering of tree items.
Vector<TreeItem *> ordered_tree_items;
// Entires that match the search mask.
// TODO: Decide if this can be removed. It can be implicitly inferred from number_matches.
Vector<TreeItem *> matching_entries;
// Number of descendant matches for each tree item.
HashMap<TreeItem *, int> number_matches;
// Custom draw-callbacks for each tree item.
HashMap<TreeItem *, Callable> callable_cache;
bool was_searched_recently = false; // Performance
bool was_filtered_recently = false; // Performance
void _clean_callable_cache();
// update_search() calls these
void _filter_tree();
void _filter_tree(TreeItem *item, bool p_parent_matching);
void _clear_filter();
void _highlight_tree();
void _highlight_tree_item(TreeItem *p_tree_item);
// Custom draw-Callback (bind inherited Callable).
void _draw_highlight_item(TreeItem *p_tree_item, const Rect2 p_rect, const Callable &p_parent_draw_method);
void _update_matching_entries(const String &p_search_mask);
void _update_ordered_tree_items(TreeItem *p_tree_item);
void _update_number_matches();
void _update_number_matches(TreeItem *item);
void _find_matching_entries(TreeItem *p_tree_item, const String &p_search_mask, Vector<TreeItem *> &p_accum) const;
String _get_search_mask() const;
StringSearchIndices _substring_bounds(const String &p_searchable, const String &p_search_mask) const;
void _select_item(TreeItem *p_item);
void _select_first_match();
void _select_last_match();
void _select_previous_match();
void _select_next_match();
void _on_search_panel_closed();
// TODO: make p_vec ref `const` once Vector::bsearch is const.
// See: https://github.com/godotengine/godot/pull/90341
template <typename T>
bool _vector_has_bsearch(Vector<T *> &p_vec, T *element) const;
protected:
static void _bind_methods() {}
public:
enum TreeSearchMode {
HIGHLIGHT = 0,
FILTER = 1
};
struct SearchInfo {
String search_mask;
TreeSearchMode search_mode;
bool visible;
};
// Called as a post-processing step for the already constructed tree.
void update_search(Tree *p_tree);
// This restores the highlight-drawing if a single item got edited.
void notify_item_edited(TreeItem *p_item);
TreeSearch() { ERR_FAIL_MSG("TreeSearch needs a TreeSearchPanel to work properly."); }
TreeSearch(TreeSearchPanel *p_search_panel);
};
// --------------------------------------------
class TreeSearchPanel : public HFlowContainer {
GDCLASS(TreeSearchPanel, HFlowContainer)
private:
Button *toggle_button_filter_highlight;
Button *close_button;
Button *find_next_button;
Button *find_prev_button;
Label *label_filter;
LineEdit *line_edit_search;
CheckBox *check_button_filter_highlight;
void _add_spacer(float width_multiplier = 1.f);
void _notification(int p_what);
protected:
static void _bind_methods();
public:
String get_text() const;
TreeSearch::TreeSearchMode get_search_mode() const;
TreeSearch::SearchInfo get_search_info() const;
void set_search_info(const TreeSearch::SearchInfo &p_search_info);
void focus_editor();
TreeSearchPanel();
};
#endif // TREE_SEARCH_H
#endif // ! TOOLS_ENABLED

View File

@ -25,10 +25,6 @@ android.debug.x86_64 = "res://addons/limboai/bin/liblimboai.android.template_deb
android.release.x86_64 = "res://addons/limboai/bin/liblimboai.android.template_release.x86_64.so" android.release.x86_64 = "res://addons/limboai/bin/liblimboai.android.template_release.x86_64.so"
android.debug.x86_32 = "res://addons/limboai/bin/liblimboai.android.template_debug.x86_32.so" android.debug.x86_32 = "res://addons/limboai/bin/liblimboai.android.template_debug.x86_32.so"
android.release.x86_32 = "res://addons/limboai/bin/liblimboai.android.template_release.x86_32.so" android.release.x86_32 = "res://addons/limboai/bin/liblimboai.android.template_release.x86_32.so"
ios.release.arm64 = "res://addons/limboai/bin/liblimboai.ios.template_release.arm64.dylib"
ios.debug.arm64 = "res://addons/limboai/bin/liblimboai.ios.template_debug.arm64.dylib"
ios.release.simulator = "res://addons/limboai/bin/liblimboai.ios.template_release.universal.dylib"
ios.debug.simulator = "res://addons/limboai/bin/liblimboai.ios.template_debug.universal.dylib"
web.debug.wasm32 = "res://addons/limboai/bin/liblimboai.web.template_debug.wasm32.wasm" web.debug.wasm32 = "res://addons/limboai/bin/liblimboai.web.template_debug.wasm32.wasm"
web.release.wasm32 = "res://addons/limboai/bin/liblimboai.web.template_release.wasm32.wasm" web.release.wasm32 = "res://addons/limboai/bin/liblimboai.web.template_release.wasm32.wasm"

View File

@ -100,7 +100,7 @@ void LimboHSM::update(double p_delta) {
} }
} }
void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard) { void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event) {
ERR_FAIL_COND_MSG(p_from_state != nullptr && p_from_state->get_parent() != this, "LimboHSM: Unable to add a transition from a state that is not an immediate child of mine."); ERR_FAIL_COND_MSG(p_from_state != nullptr && p_from_state->get_parent() != this, "LimboHSM: Unable to add a transition from a state that is not an immediate child of mine.");
ERR_FAIL_COND_MSG(p_to_state == nullptr, "LimboHSM: Unable to add a transition to a null state."); ERR_FAIL_COND_MSG(p_to_state == nullptr, "LimboHSM: Unable to add a transition to a null state.");
ERR_FAIL_COND_MSG(p_to_state->get_parent() != this, "LimboHSM: Unable to add a transition to a state that is not an immediate child of mine."); ERR_FAIL_COND_MSG(p_to_state->get_parent() != this, "LimboHSM: Unable to add a transition to a state that is not an immediate child of mine.");
@ -108,13 +108,8 @@ void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state,
TransitionKey key = Transition::make_key(p_from_state, p_event); TransitionKey key = Transition::make_key(p_from_state, p_event);
ERR_FAIL_COND_MSG(transitions.has(key), "LimboHSM: Unable to add another transition with the same event and origin."); ERR_FAIL_COND_MSG(transitions.has(key), "LimboHSM: Unable to add another transition with the same event and origin.");
// Note: Explicit ObjectID casting needed for GDExtension. // Note: Explicit casting needed for GDExtension.
transitions[key] = { transitions[key] = { p_from_state != nullptr ? ObjectID(p_from_state->get_instance_id()) : ObjectID(), ObjectID(p_to_state->get_instance_id()), p_event };
p_from_state != nullptr ? ObjectID(p_from_state->get_instance_id()) : ObjectID(),
ObjectID(p_to_state->get_instance_id()),
p_event,
p_guard
};
} }
void LimboHSM::remove_transition(LimboState *p_from_state, const StringName &p_event) { void LimboHSM::remove_transition(LimboState *p_from_state, const StringName &p_event) {
@ -171,13 +166,13 @@ bool LimboHSM::_dispatch(const StringName &p_event, const Variant &p_cargo) {
Transition transition; Transition transition;
_get_transition(active_state, p_event, transition); _get_transition(active_state, p_event, transition);
if (transition.is_valid() && transition.is_allowed()) { if (transition.is_valid()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state)); to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
} }
if (to_state == nullptr) { if (to_state == nullptr) {
// Get ANYSTATE transition. // Get ANYSTATE transition.
_get_transition(nullptr, p_event, transition); _get_transition(nullptr, p_event, transition);
if (transition.is_valid() && transition.is_allowed()) { if (transition.is_valid()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state)); to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
if (to_state == active_state) { if (to_state == active_state) {
// Transitions to self are not allowed with ANYSTATE. // Transitions to self are not allowed with ANYSTATE.
@ -265,46 +260,22 @@ void LimboHSM::_validate_property(PropertyInfo &p_property) const {
} }
} }
void LimboHSM::_exit_if_not_inside_tree() {
if (is_active() && !is_inside_tree()) {
_exit();
}
}
void LimboHSM::_notification(int p_what) { void LimboHSM::_notification(int p_what) {
switch (p_what) { switch (p_what) {
case NOTIFICATION_POST_ENTER_TREE: { case NOTIFICATION_POST_ENTER_TREE: {
if (was_active && is_root()) { if (was_active && is_root()) {
// Re-activate the root HSM if it was previously active. // Re-activate the root HSM if it was previously active.
// Typically, this happens when the node is re-entered scene repeatedly (such as with object pooling). // Typically, this happens when the node is re-entered scene repeatedly (e.g., re-parenting, pooling).
set_active(true); set_active(true);
} }
} break; } break;
case NOTIFICATION_EXIT_TREE: { case NOTIFICATION_EXIT_TREE: {
if (is_root()) { if (is_root()) {
// Exit the state machine if the root HSM is no longer in the scene tree (except when being reparented). // Remember active status for re-parenting and exit state machine
// This ensures that resources and signal connections are released if active. // to release resources and signal connections if active.
was_active = is_active(); was_active = active;
if (is_active()) { if (is_active()) {
// Check if the HSM node is being deleted.
bool is_being_deleted = false;
Node *node = this;
while (node) {
if (node->is_queued_for_deletion()) {
is_being_deleted = true;
break;
}
node = node->get_parent();
}
if (is_being_deleted) {
// Exit the state machine immediately if the HSM is being deleted.
_exit(); _exit();
} else {
// Use deferred mode to prevent exiting during Node re-parenting.
// This allows the HSM to remain active when it (or one of its parents) is reparented.
callable_mp(this, &LimboHSM::_exit_if_not_inside_tree).call_deferred();
}
} }
} }
} break; } break;
@ -329,7 +300,7 @@ void LimboHSM::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_leaf_state"), &LimboHSM::get_leaf_state); ClassDB::bind_method(D_METHOD("get_leaf_state"), &LimboHSM::get_leaf_state);
ClassDB::bind_method(D_METHOD("set_active", "active"), &LimboHSM::set_active); ClassDB::bind_method(D_METHOD("set_active", "active"), &LimboHSM::set_active);
ClassDB::bind_method(D_METHOD("update", "delta"), &LimboHSM::update); ClassDB::bind_method(D_METHOD("update", "delta"), &LimboHSM::update);
ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event", "guard"), &LimboHSM::add_transition, DEFVAL(Callable())); ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event"), &LimboHSM::add_transition);
ClassDB::bind_method(D_METHOD("remove_transition", "from_state", "event"), &LimboHSM::remove_transition); ClassDB::bind_method(D_METHOD("remove_transition", "from_state", "event"), &LimboHSM::remove_transition);
ClassDB::bind_method(D_METHOD("has_transition", "from_state", "event"), &LimboHSM::has_transition); ClassDB::bind_method(D_METHOD("has_transition", "from_state", "event"), &LimboHSM::has_transition);
ClassDB::bind_method(D_METHOD("anystate"), &LimboHSM::anystate); ClassDB::bind_method(D_METHOD("anystate"), &LimboHSM::anystate);

View File

@ -39,12 +39,9 @@ private:
ObjectID from_state; ObjectID from_state;
ObjectID to_state; ObjectID to_state;
StringName event; StringName event;
Callable guard;
inline bool is_valid() const { return to_state != ObjectID(); } inline bool is_valid() const { return to_state != ObjectID(); }
inline bool is_allowed() const { return guard.is_null() || guard.call(); }
static _FORCE_INLINE_ TransitionKey make_key(LimboState *p_from_state, const StringName &p_event) { static _FORCE_INLINE_ TransitionKey make_key(LimboState *p_from_state, const StringName &p_event) {
return TransitionKey( return TransitionKey(
p_from_state != nullptr ? uint64_t(p_from_state->get_instance_id()) : 0, p_from_state != nullptr ? uint64_t(p_from_state->get_instance_id()) : 0,
@ -63,7 +60,6 @@ private:
HashMap<TransitionKey, Transition, TransitionKeyHasher> transitions; HashMap<TransitionKey, Transition, TransitionKeyHasher> transitions;
void _get_transition(LimboState *p_from_state, const StringName &p_event, Transition &r_transition) const; void _get_transition(LimboState *p_from_state, const StringName &p_event, Transition &r_transition) const;
void _exit_if_not_inside_tree();
protected: protected:
static void _bind_methods(); static void _bind_methods();
@ -97,7 +93,7 @@ public:
void update(double p_delta); void update(double p_delta);
void add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard = Callable()); void add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event);
void remove_transition(LimboState *p_from_state, const StringName &p_event); void remove_transition(LimboState *p_from_state, const StringName &p_event);
bool has_transition(LimboState *p_from_state, const StringName &p_event) const { return transitions.has(Transition::make_key(p_from_state, p_event)); } bool has_transition(LimboState *p_from_state, const StringName &p_event) const { return transitions.has(Transition::make_key(p_from_state, p_event)); }

View File

@ -101,7 +101,6 @@
#include "editor/debugger/limbo_debugger.h" #include "editor/debugger/limbo_debugger.h"
#include "editor/debugger/limbo_debugger_plugin.h" #include "editor/debugger/limbo_debugger_plugin.h"
#include "editor/mode_switch_button.h" #include "editor/mode_switch_button.h"
#include "editor/tree_search.h"
#include "hsm/limbo_hsm.h" #include "hsm/limbo_hsm.h"
#include "hsm/limbo_state.h" #include "hsm/limbo_state.h"
#include "util/limbo_string_names.h" #include "util/limbo_string_names.h"
@ -268,8 +267,6 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(OwnerPicker); GDREGISTER_CLASS(OwnerPicker);
GDREGISTER_CLASS(LimboAIEditor); GDREGISTER_CLASS(LimboAIEditor);
GDREGISTER_CLASS(LimboAIEditorPlugin); GDREGISTER_CLASS(LimboAIEditorPlugin);
GDREGISTER_INTERNAL_CLASS(TreeSearchPanel);
GDREGISTER_INTERNAL_CLASS(TreeSearch);
#endif // LIMBOAI_GDEXTENSION #endif // LIMBOAI_GDEXTENSION
EditorPlugins::add_by_type<LimboAIEditorPlugin>(); EditorPlugins::add_by_type<LimboAIEditorPlugin>();

View File

@ -215,7 +215,7 @@ TEST_CASE("[Modules][LimboAI] HSM") {
CHECK(beta_updates->num_callbacks == 0); CHECK(beta_updates->num_callbacks == 0);
CHECK(beta_exits->num_callbacks == 1); // * exited CHECK(beta_exits->num_callbacks == 1); // * exited
} }
SUBCASE("Test transition with state-wide guard") { SUBCASE("Test transition with guard") {
Ref<TestGuard> guard = memnew(TestGuard); Ref<TestGuard> guard = memnew(TestGuard);
state_beta->set_guard(callable_mp(guard.ptr(), &TestGuard::can_enter)); state_beta->set_guard(callable_mp(guard.ptr(), &TestGuard::can_enter));
@ -234,25 +234,6 @@ TEST_CASE("[Modules][LimboAI] HSM") {
CHECK(beta_entries->num_callbacks == 0); CHECK(beta_entries->num_callbacks == 0);
} }
} }
SUBCASE("Test transition with transition-scoped guard") {
Ref<TestGuard> guard = memnew(TestGuard);
hsm->add_transition(state_alpha, state_beta, "guarded_transition", callable_mp(guard.ptr(), &TestGuard::can_enter));
SUBCASE("When entry is permitted") {
guard->permitted_to_enter = true;
hsm->dispatch("guarded_transition");
CHECK(hsm->get_active_state() == state_beta);
CHECK(alpha_exits->num_callbacks == 1);
CHECK(beta_entries->num_callbacks == 1);
}
SUBCASE("When entry is not permitted") {
guard->permitted_to_enter = false;
hsm->dispatch("guarded_transition");
CHECK(hsm->get_active_state() == state_alpha);
CHECK(alpha_exits->num_callbacks == 0);
CHECK(beta_entries->num_callbacks == 0);
}
}
SUBCASE("When there is no transition for given event") { SUBCASE("When there is no transition for given event") {
hsm->dispatch("not_found"); hsm->dispatch("not_found");
CHECK(alpha_exits->num_callbacks == 0); CHECK(alpha_exits->num_callbacks == 0);

View File

@ -37,7 +37,7 @@
#define IS_CLASS(m_obj, m_class) (m_obj->is_class_ptr(m_class::get_class_ptr_static())) #define IS_CLASS(m_obj, m_class) (m_obj->is_class_ptr(m_class::get_class_ptr_static()))
#define RAND_RANGE(m_from, m_to) (Math::random(m_from, m_to)) #define RAND_RANGE(m_from, m_to) (Math::random(m_from, m_to))
#define RANDF() (Math::randf()) #define RANDF() (Math::randf())
#define BUTTON_SET_ICON(m_btn, m_icon) m_btn->set_button_icon(m_icon) #define BUTTON_SET_ICON(m_btn, m_icon) m_btn->set_icon(m_icon)
#define RESOURCE_LOAD(m_path, m_hint) ResourceLoader::load(m_path, m_hint) #define RESOURCE_LOAD(m_path, m_hint) ResourceLoader::load(m_path, m_hint)
#define RESOURCE_LOAD_NO_CACHE(m_path, m_hint) ResourceLoader::load(m_path, m_hint, ResourceFormatLoader::CACHE_MODE_IGNORE) #define RESOURCE_LOAD_NO_CACHE(m_path, m_hint) ResourceLoader::load(m_path, m_hint, ResourceFormatLoader::CACHE_MODE_IGNORE)
#define RESOURCE_SAVE(m_res, m_path, m_flags) ResourceSaver::save(m_res, m_path, m_flags) #define RESOURCE_SAVE(m_res, m_path, m_flags) ResourceSaver::save(m_res, m_path, m_flags)
@ -114,8 +114,7 @@ using namespace godot;
#define ADD_STYLEBOX_OVERRIDE(m_control, m_name, m_stylebox) (m_control->add_theme_stylebox_override(m_name, m_stylebox)) #define ADD_STYLEBOX_OVERRIDE(m_control, m_name, m_stylebox) (m_control->add_theme_stylebox_override(m_name, m_stylebox))
#define GET_NODE(m_parent, m_path) m_parent->get_node_internal(m_path) #define GET_NODE(m_parent, m_path) m_parent->get_node_internal(m_path)
#define OBJECT_DB_GET_INSTANCE(m_id) ObjectDB::get_instance(m_id) #define OBJECT_DB_GET_INSTANCE(m_id) ObjectDB::get_instance(m_id)
#define EDITOR_DEF(m_setting, m_value) \ #define EDITOR_DEF(m_setting, m_value) do { /* do-while(0) ideom to avoid any potential semicolon errors. */\
do { /* do-while(0) ideom to avoid any potential semicolon errors. */ \
EditorInterface::get_singleton()->get_editor_settings()->set_initial_value(m_setting, m_value, false); \ EditorInterface::get_singleton()->get_editor_settings()->set_initial_value(m_setting, m_value, false); \
if (!EDITOR_SETTINGS()->has_setting(m_setting)) { \ if (!EDITOR_SETTINGS()->has_setting(m_setting)) { \
EDITOR_SETTINGS()->set_setting(m_setting, m_value); \ EDITOR_SETTINGS()->set_setting(m_setting, m_value); \

View File

@ -40,14 +40,12 @@ LimboStringNames::LimboStringNames() {
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");
AnimationFilter = SN("AnimationFilter"); AnimationFilter = SN("AnimationFilter");
BBParam = SN("BBParam");
behavior_tree_finished = SN("behavior_tree_finished"); behavior_tree_finished = SN("behavior_tree_finished");
bold = SN("bold"); bold = SN("bold");
button_down = SN("button_down"); button_down = SN("button_down");
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");
Close = SN("Close");
dark_color_2 = SN("dark_color_2"); dark_color_2 = SN("dark_color_2");
Debug = SN("Debug"); Debug = SN("Debug");
disabled_font_color = SN("disabled_font_color"); disabled_font_color = SN("disabled_font_color");
@ -60,7 +58,6 @@ LimboStringNames::LimboStringNames() {
EditorFonts = SN("EditorFonts"); EditorFonts = SN("EditorFonts");
EditorIcons = SN("EditorIcons"); EditorIcons = SN("EditorIcons");
EditorStyles = SN("EditorStyles"); EditorStyles = SN("EditorStyles");
emit_signal = SN("emit_signal");
entered = SN("entered"); entered = SN("entered");
error_value = SN("error_value"); error_value = SN("error_value");
EVENT_FAILURE = SN("failure"); EVENT_FAILURE = SN("failure");
@ -69,8 +66,6 @@ LimboStringNames::LimboStringNames() {
exited = SN("exited"); exited = SN("exited");
favorite_tasks_changed = SN("favorite_tasks_changed"); favorite_tasks_changed = SN("favorite_tasks_changed");
Favorites = SN("Favorites"); Favorites = SN("Favorites");
FlatButton = SN("FlatButton");
Focus = SN("Focus");
focus_exited = SN("focus_exited"); focus_exited = SN("focus_exited");
font = SN("font"); font = SN("font");
font_color = SN("font_color"); font_color = SN("font_color");
@ -82,7 +77,6 @@ LimboStringNames::LimboStringNames() {
GuiTreeArrowRight = SN("GuiTreeArrowRight"); GuiTreeArrowRight = SN("GuiTreeArrowRight");
HeaderSmall = SN("HeaderSmall"); HeaderSmall = SN("HeaderSmall");
Help = SN("Help"); Help = SN("Help");
h_separation = SN("h_separation");
icon_max_width = SN("icon_max_width"); icon_max_width = SN("icon_max_width");
class_icon_size = SN("class_icon_size"); class_icon_size = SN("class_icon_size");
id_pressed = SN("id_pressed"); id_pressed = SN("id_pressed");
@ -126,7 +120,6 @@ LimboStringNames::LimboStringNames() {
separation = SN("separation"); separation = SN("separation");
set_custom_name = SN("set_custom_name"); set_custom_name = SN("set_custom_name");
set_root_task = SN("set_root_task"); set_root_task = SN("set_root_task");
set_visible = SN("set_visible");
set_v_scroll = SN("set_v_scroll"); set_v_scroll = SN("set_v_scroll");
setup = SN("setup"); setup = SN("setup");
started = SN("started"); started = SN("started");

View File

@ -56,14 +56,12 @@ public:
StringName add_child; StringName add_child;
StringName Add; StringName Add;
StringName AnimationFilter; StringName AnimationFilter;
StringName BBParam;
StringName behavior_tree_finished; StringName behavior_tree_finished;
StringName bold; StringName bold;
StringName button_down; StringName button_down;
StringName button_up; StringName button_up;
StringName call_deferred; StringName call_deferred;
StringName changed; StringName changed;
StringName Close;
StringName dark_color_2; StringName dark_color_2;
StringName Debug; StringName Debug;
StringName disabled_font_color; StringName disabled_font_color;
@ -76,7 +74,6 @@ public:
StringName EditorFonts; StringName EditorFonts;
StringName EditorIcons; StringName EditorIcons;
StringName EditorStyles; StringName EditorStyles;
StringName emit_signal;
StringName entered; StringName entered;
StringName error_value; StringName error_value;
StringName EVENT_FAILURE; StringName EVENT_FAILURE;
@ -85,8 +82,6 @@ public:
StringName exited; StringName exited;
StringName favorite_tasks_changed; StringName favorite_tasks_changed;
StringName Favorites; StringName Favorites;
StringName FlatButton;
StringName Focus;
StringName focus_exited; StringName focus_exited;
StringName font_color; StringName font_color;
StringName font_size; StringName font_size;
@ -98,7 +93,6 @@ public:
StringName GuiTreeArrowRight; StringName GuiTreeArrowRight;
StringName HeaderSmall; StringName HeaderSmall;
StringName Help; StringName Help;
StringName h_separation;
StringName icon_max_width; StringName icon_max_width;
StringName class_icon_size; StringName class_icon_size;
StringName id_pressed; StringName id_pressed;
@ -142,7 +136,6 @@ public:
StringName separation; StringName separation;
StringName set_custom_name; StringName set_custom_name;
StringName set_root_task; StringName set_root_task;
StringName set_visible;
StringName set_v_scroll; StringName set_v_scroll;
StringName setup; StringName setup;
StringName started; StringName started;