Compare commits

...

52 Commits

Author SHA1 Message Date
speak 304d7b898b
Merge 2dfe775f7b into ba90deaa6a 2024-11-15 16:03:58 -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
Serhii Snitsaruk f90c48eb81
Merge pull request #242 from limbonaut/fix-hsm-reparenting-saga-episode-three
Fix re-parenting an agent interrupts its state machine
2024-11-01 11:11:45 -07:00
Serhii Snitsaruk 162de0f868
Fix re-parenting agent interrupts its state machine 2024-11-01 18:47:36 +01:00
Serhii Snitsaruk 423c4ce7a4
Merge pull request #241 from limbonaut/hsm-transition-guards
`LimboHSM`: Ability to specify per-transition guard function
2024-11-01 07:49:20 -07:00
Serhii Snitsaruk a5d59a0a51
Update docs 2024-11-01 15:24:32 +01:00
Serhii Snitsaruk 85eda3c804
Tests for per-transition guards 2024-11-01 14:59:10 +01:00
Serhii Snitsaruk 6de8b9e4c4
Merge pull request #227 from limbonaut/fix-bttask-setup-overriding
Call both native and script `_setup/_enter/_exit` in BTTask
2024-11-01 06:19:13 -07:00
Serhii Snitsaruk bbe71bb378
Add transition guards 2024-11-01 14:00:30 +01:00
Serhii Snitsaruk 7feff38d6b
Call both native and script _enter/_exit in BTTask 2024-11-01 13:50:08 +01:00
Serhii Snitsaruk 632e26c922
Call both native and script's `_setup()` in BTTask
Current behavior is overriding, which is not correct as it's an initializer function.
2024-11-01 13:42:49 +01:00
Serhii Snitsaruk 106608aca9
Merge pull request #239 from limbonaut/fix-clone-bttask-and-arrays
Duplicate `BBParam` instances in typed arrays in `BTTask::clone()`
2024-10-28 09:17:39 -07:00
Serhii Snitsaruk d5bc62830a
Duplicate BBParam instances inside arrays in `BTTask::clone()`
Fixes issue with `CallMethod` arguments being shared between tasks after duplication.
2024-10-28 15:06:29 +01:00
Serhii Snitsaruk 3b15abf2c9
GHA: Fix vulkan sdk in ios packaging 2024-10-19 22:09:03 +02:00
Serhii Snitsaruk 6d049a3701
Merge pull request #237 from limbonaut/gha-ios-vulkan
GHA: Fix Vulkan SDK on iOS
2024-10-19 11:35:28 -07:00
Serhii Snitsaruk 65454c36a8
GHA: Fix Vulkan SDK on iOS 2024-10-19 20:04:48 +02:00
Serhii Snitsaruk e0e15b0ec4
Merge pull request #163 from limbonaut/gha-gdext-ios
GHA: Build iOS GDExtension libraries
2024-10-17 06:23:06 -07:00
Serhii Snitsaruk ee8c773e71
GHA: Build with ios_simulator=yes flag 2024-10-16 14:32:39 +02:00
Serhii Snitsaruk 7f38fe2b8b
GHA: Change iOS arch to arm64 & build simulator libs 2024-10-16 14:32:39 +02:00
Serhii Snitsaruk 500775eb14
Add iOS builds to the GDExtension manifest 2024-10-16 14:32:39 +02:00
Serhii Snitsaruk 1cdde4d5a9
GHA: Add iOS builds to GDExtension workflow 2024-10-16 14:31:56 +02:00
Serhii Snitsaruk eaa43020f5
Merge pull request #235 from limbonaut/doc-get-var-fix
Doc: Clarify `Blackboard.get_var()` and `set_var()`
2024-10-13 12:27:12 -07:00
Serhii Snitsaruk b5b1ac7289
Doc: Clarify `Blackboard.get_var()` and `set_var()` 2024-10-13 21:04:59 +02:00
Serhii Snitsaruk ece17d68d9
Merge pull request #234 from limbonaut/doc-fixes
GHA: Code style checks for PRs and commits
2024-10-13 12:03:17 -07:00
Serhii Snitsaruk 85787616e7
GHA: Add code style check for PRs and direct commits 2024-10-13 20:43:59 +02:00
Serhii Snitsaruk 11abf36c99
Clang format 2024-10-13 20:25:03 +02:00
Serhii Snitsaruk 19d771fef2
Merge pull request #229 from monxa/tasktree-search
Implement Tree Search Functionality with Highlighting and Filtering
2024-10-13 11:18:37 -07:00
Serhii Snitsaruk 2b89d1d23e
Merge pull request #233 from limbonaut/gha-fix-macos-vulkan
GHA: Fix Vulkan SDK installation step
2024-10-13 10:48:31 -07:00
Serhii Snitsaruk 6f318b83b8
GHA: Fix Vulkan SDK installation step 2024-10-13 18:25:54 +02:00
Serhii Snitsaruk 7a1b56f9c8
GHA: Switch workflows to Godot 4.3 branch 2024-10-13 17:06:25 +02:00
Alexander Montag 2b86928737
Add tooltip to explain case sensitivity behavior 2024-10-13 07:02:59 +00:00
Alexander Montag 8c557f87f7
Address 2. review for TreeSearch
Remove redundant comment

Prune tab_search_context

Fix restore tab on `_tab_closed`

Add break statement

Pass callable by reference in _draw_highlight_item

Refactor _initialize_controls into constructor

Remove redundant if (!line_edit_search)-check
2024-10-13 07:31:35 +02:00
Alexander Montag 6776319472
Implement Tree Search Functionality with Highlighting and Filtering
This commit introduces a comprehensive Tree Search feature, including:
- Tree highlighting: Highlights items that match the search query.
- Tree filtering: Filters items so only matches and descendants are
  shown.
- Counting descendants: Shows the number of matching items within collapsed branches.
- Jump to next match: on enter.
- (Limbo-)Shortcut: Default CTRL-F.
- Menu entry: Misc->Search Tree.
- Remember separate SearchInfo for each tab.

Key implementation details:
- Optimized performance for large trees
- Implemented recursive filtering for efficiency
- Added UI elements including next/previous match buttons

Development History:
- Initial implementation of highlighting and filtering
- Multiple rounds of performance optimization
- Bug fixes and refactoring for correctness
- UI enhancements and polish
- Code cleanup and style improvements
2024-10-06 06:57:11 +02:00
30 changed files with 1788 additions and 205 deletions

View File

@ -168,6 +168,36 @@ jobs:
arch: x86_32
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:
- { opts: { should-build: false } }
@ -271,7 +301,7 @@ jobs:
DEBUG_FLAGS: ${{ inputs.debug-symbols && 'debug_symbols=yes symbols_visibility=visible' || 'debug_symbols=no' }}
run: |
PATH=${GITHUB_WORKSPACE}/buildroot/bin:$PATH
scons platform=${{matrix.opts.platform}} target=${{matrix.opts.target}} arch=${{matrix.opts.arch}} ${{env.DEBUG_FLAGS}} ${{env.SCONSFLAGS}}
scons platform=${{matrix.opts.platform}} target=${{matrix.opts.target}} arch=${{matrix.opts.arch}} ${{env.DEBUG_FLAGS}} ${{matrix.opts.scons-flags}} ${{env.SCONSFLAGS}}
- name: Prepare artifact
shell: bash

View File

@ -136,7 +136,14 @@ jobs:
- name: Set up Vulkan SDK
run: |
sh misc/scripts/install_vulkan_sdk_macos.sh
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being.
#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
uses: actions/cache@v4
@ -192,7 +199,14 @@ jobs:
- name: Set up Vulkan SDK
run: |
sh misc/scripts/install_vulkan_sdk_macos.sh
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being.
#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
uses: actions/download-artifact@v4

View File

@ -165,7 +165,14 @@ jobs:
- name: Set up Vulkan SDK
run: |
sh misc/scripts/install_vulkan_sdk_macos.sh
# ! Note: Vulkan SDK changed packaging, so we need to inline these steps for the time being.
#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
uses: actions/cache@v4

View File

@ -26,7 +26,7 @@ concurrency:
# Global Settings.
env:
GODOT_REF: "4.3-stable"
GODOT_REF: "4.3"
GODOT_CPP_REF: "godot-4.3-stable"
jobs:
@ -101,6 +101,18 @@ jobs:
run: |
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:
runs-on: ubuntu-latest
outputs:

View File

@ -13,9 +13,20 @@
#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) {
String name_str = p_name;
#ifdef TOOLS_ENABLED
// * Editor
if (var_map.has(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;
}
#endif // TOOLS_ENABLED
// * Mapping
if (name_str.begins_with("mapping/")) {
StringName mapped_var_name = name_str.get_slicec('/', 1);
StringName value = p_value;
bool properties_changed = false;
bool prop_list_changed = false;
if (value == StringName()) {
if (parent_scope_mapping.has(mapped_var_name)) {
properties_changed = true;
prop_list_changed = true;
parent_scope_mapping.erase(mapped_var_name);
}
} else {
if (!parent_scope_mapping.has(mapped_var_name)) {
properties_changed = true;
prop_list_changed = true;
}
parent_scope_mapping[mapped_var_name] = value;
}
if (properties_changed) {
if (prop_list_changed) {
notify_property_list_changed();
}
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
if (name_str.begins_with("var/")) {
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);
} else if (what == "hint_string") {
var_map[var_name].set_hint_string(p_value);
} else if (what == "property_binding") {
property_bindings[var_name] = NodePath(p_value);
} else {
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 {
String name_str = p_name;
#ifdef TOOLS_ENABLED
// * Editor
if (var_map.has(p_name)) {
if (has_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 {
r_ret = var_map[p_name].get_value();
}
return true;
}
#endif // TOOLS_ENABLED
// * Mapping
if (name_str.begins_with("mapping/")) {
StringName mapped_var_name = name_str.get_slicec('/', 1);
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];
} else if (has_property_binding(mapped_var_name)) {
r_ret = RTR("Already bound to property.");
} else {
r_ret = StringName();
}
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
if (!name_str.begins_with("var/")) {
return false;
@ -127,14 +198,16 @@ void BlackboardPlan::_get_property_list(List<PropertyInfo> *p_list) const {
String var_name = p.first;
BBVariable var = p.second;
#ifdef TOOLS_ENABLED
// * Editor
if (var.get_type() != Variant::NIL && (!is_derived() || !var_name.begins_with("_"))) {
if (has_mapping(var_name)) {
if (!_is_var_hidden(var_name, var)) {
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));
} else {
p_list->push_back(PropertyInfo(var.get_type(), var_name, var.get_hint(), var.get_hint_string(), PROPERTY_USAGE_EDITOR));
}
}
#endif // TOOLS_ENABLED
// * Storage
if (is_derived() && (!var.is_value_changed() || var.get_value() == base->var_map[var_name].get_value())) {
@ -153,9 +226,32 @@ void BlackboardPlan::_get_property_list(List<PropertyInfo> *p_list) const {
if (is_mapping_enabled()) {
p_list->push_back(PropertyInfo(Variant::NIL, "Mapping", PROPERTY_HINT_NONE, "mapping/", PROPERTY_USAGE_GROUP));
for (const Pair<StringName, BBVariable> &p : var_list) {
// Serialize only non-empty mappings.
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));
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.
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));
}
}
}
// * 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));
}
}
}
@ -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();
}
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) {
prefetch_nodepath_vars = p_enable;
emit_changed();
@ -410,8 +511,9 @@ void BlackboardPlan::populate_blackboard(const Ref<Blackboard> &p_blackboard, bo
#endif
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 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.
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() == nullptr, 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);
}
} 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.
// A derived plan can only have variables that exist in the base plan,
// 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;
// Mapping between variables in this plan and their parent scope names.
// Used for linking variables to their parent scope counterparts upon Blackboard creation/population.
HashMap<StringName, StringName> parent_scope_mapping;
// Fetcher function for the parent scope plan. Funtion should return a Ref<BlackboardPlan>.
// Used in the inspector. When set, mapping feature becomes available.
// Fetcher function for the parent scope plan. Function should return a Ref<BlackboardPlan>.
// Used in the inspector: enables mapping feature when set.
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).
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:
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 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);
bool is_prefetching_nodepath_vars() const;

View File

@ -172,9 +172,8 @@ void BTTask::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard, Node
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 {
@ -182,60 +181,47 @@ Ref<BTTask> BTTask::clone() const {
// * Children are duplicated via children property. See _set_children().
// * Make BBParam properties unique.
HashMap<Ref<Resource>, Ref<Resource>> duplicates;
#ifdef LIMBOAI_MODULE
// Make BBParam properties unique.
List<PropertyInfo> props;
inst->get_property_list(&props);
HashMap<Ref<Resource>, Ref<Resource>> duplicates;
for (List<PropertyInfo>::Element *E = props.front(); E; E = E->next()) {
if (!(E->get().usage & PROPERTY_USAGE_STORAGE)) {
continue;
}
Variant v = inst->get(E->get().name);
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 (!duplicates.has(res)) {
duplicates[res] = res->duplicate();
}
res = duplicates[res];
inst->set(E->get().name, res);
}
}
}
}
PropertyInfo prop = E->get();
#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)) {
PropertyInfo prop = PropertyInfo::from_dict(props[i]);
#endif
if (!(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();
Variant prop_value = inst->get(prop.name);
Ref<Resource> res = prop_value;
if (res.is_valid() && res->is_class("BBParam")) {
// Duplicate BBParam
if (!duplicates.has(res)) {
duplicates[res] = res->duplicate();
}
res = duplicates[res];
inst->set(prop.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();
}
res = duplicates[res];
inst->set(prop_name, res);
}
}
}
}
#endif // LIMBOAI_MODULE & LIMBOAI_GDEXTENSION
return inst;
}
@ -248,9 +234,9 @@ BT::Status BTTask::execute(double p_delta) {
data.children.get(i)->abort();
}
}
if (!GDVIRTUAL_CALL(_enter)) {
_enter();
}
// First native, then script.
_enter();
GDVIRTUAL_CALL(_enter);
} else {
data.elapsed += p_delta;
}
@ -260,9 +246,9 @@ BT::Status BTTask::execute(double p_delta) {
}
if (data.status != RUNNING) {
if (!GDVIRTUAL_CALL(_exit)) {
_exit();
}
// First script, then native.
GDVIRTUAL_CALL(_exit);
_exit();
data.elapsed = 0.0;
}
return data.status;
@ -273,9 +259,9 @@ void BTTask::abort() {
get_child(i)->abort();
}
if (data.status == RUNNING) {
if (!GDVIRTUAL_CALL(_exit)) {
_exit();
}
// First script, then native.
GDVIRTUAL_CALL(_exit);
_exit();
}
data.status = FRESH;
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>`
Returns variable value or ``default`` if variable doesn't exist. If ``complain`` is ``true``, an error will be printed if variable doesn't exist.
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.
.. 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>`
Assigns a value to a Blackboard variable.
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.
.. rst-class:: classref-item-separator

View File

@ -45,27 +45,27 @@ Methods
.. table::
: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``\ ) |
+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |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_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| |
+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ``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:`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:`update<class_LimboHSM_method_update>`\ (\ delta\: ``float``\ ) |
+-------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |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:`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_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| |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| ``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:`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:`update<class_LimboHSM_method_update>`\ (\ delta\: ``float``\ ) |
+-------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
.. rst-class:: classref-section-separator
@ -191,9 +191,16 @@ Method Descriptions
.. rst-class:: classref-method
|void| **add_transition**\ (\ from_state\: :ref:`LimboState<class_LimboState>`, to_state\: :ref:`LimboState<class_LimboState>`, event\: ``StringName``\ ) :ref:`🔗<class_LimboHSM_method_add_transition>`
|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>`
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.
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**.
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

View File

@ -234,7 +234,7 @@ Called when the state is entered.
|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

View File

@ -46,7 +46,7 @@
<param index="1" name="default" type="Variant" default="null" />
<param index="2" name="complain" type="bool" default="true" />
<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.
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.
</description>
</method>
<method name="get_vars_as_dict" qualifiers="const">
@ -98,7 +98,7 @@
<param index="0" name="var_name" type="StringName" />
<param index="1" name="value" type="Variant" />
<description>
Assigns a value to a Blackboard variable.
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.
</description>
</method>
<method name="top" qualifiers="const">

View File

@ -14,8 +14,14 @@
<param index="0" name="from_state" type="LimboState" />
<param index="1" name="to_state" type="LimboState" />
<param index="2" name="event" type="StringName" />
<param index="3" name="guard" type="Callable" default="Callable()" />
<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 state.
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].
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>
</method>
<method name="change_active_state">

View File

@ -20,7 +20,7 @@
<method name="_exit" qualifiers="virtual">
<return type="void" />
<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>
</method>
<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: {
BUTTON_SET_ICON(action_menu, 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
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
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/");
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;

View File

@ -25,6 +25,7 @@
#include "blackboard_plan_editor.h"
#include "debugger/limbo_debugger_plugin.h"
#include "editor_property_bb_param.h"
#include "editor_property_property_path.h"
#ifdef LIMBOAI_MODULE
#include "core/config/project_settings.h"
@ -66,6 +67,7 @@
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/classes/resource_loader.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_editor.hpp>
#include <godot_cpp/classes/script_editor_base.hpp>
@ -73,6 +75,21 @@
#include <godot_cpp/core/error_macros.hpp>
#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
_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_undo_method(this, LW_NAME(_update_task_tree), task_tree->get_bt());
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) {
@ -201,20 +218,58 @@ void LimboAIEditor::_new_bt() {
EDIT_RESOURCE(bt);
}
void LimboAIEditor::_save_bt(String p_path) {
ERR_FAIL_COND_MSG(p_path.is_empty(), "Empty p_path");
ERR_FAIL_COND_MSG(task_tree->get_bt().is_null(), "Behavior Tree is null.");
void LimboAIEditor::_save_bt(const Ref<BehaviorTree> &p_bt, const String &p_path) {
ERR_FAIL_COND(p_path.is_empty());
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
task_tree->get_bt()->set_path(p_path, true);
task_tree->get_bt()->set_path(p_path, true);
#elif LIMBOAI_GDEXTENSION
task_tree->get_bt()->take_over_path(p_path);
task_tree->get_bt()->take_over_path(p_path);
#endif
RESOURCE_SAVE(task_tree->get_bt(), p_path, ResourceSaver::FLAG_CHANGE_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();
_mark_as_dirty(false);
}
void LimboAIEditor::_load_bt(String p_path) {
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");
Ref<BehaviorTree> bt = RESOURCE_LOAD(p_path, "BehaviorTree");
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) {
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) {
return;
}
@ -261,11 +318,15 @@ 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->notify_property_list_changed();
#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);
if (task_tree->get_bt().is_valid() && !task_tree->get_bt()->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty))) {
task_tree->get_bt()->connect(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty).bind(true));
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::_set_as_dirty).bind(task_tree->get_bt(), true));
}
int idx = history.find(p_behavior_tree);
@ -280,6 +341,15 @@ void LimboAIEditor::edit_bt(const Ref<BehaviorTree> &p_behavior_tree, bool p_for
task_tree->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();
}
@ -321,12 +391,11 @@ void LimboAIEditor::get_window_layout(const Ref<ConfigFile> &p_configuration) {
p_configuration->set_value("LimboAI", "bteditor_hsplit", split_offset);
}
void LimboAIEditor::_mark_as_dirty(bool p_dirty) {
Ref<BehaviorTree> bt = task_tree->get_bt();
if (p_dirty && !dirty.has(bt)) {
dirty.insert(bt);
} else if (p_dirty == false && dirty.has(bt)) {
dirty.erase(bt);
void LimboAIEditor::_set_as_dirty(const Ref<BehaviorTree> &p_bt, bool p_dirty) {
if (p_dirty && !dirty.has(p_bt)) {
dirty.insert(p_bt);
} else if (p_dirty == false && dirty.has(p_bt)) {
dirty.erase(p_bt);
}
}
@ -426,6 +495,14 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
} else if (LW_IS_SHORTCUT("limbo_ai/close_tab", p_event)) {
_tab_menu_option_selected(TAB_CLOSE);
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
}
}
@ -457,6 +534,8 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
_on_save_pressed();
} else if (LW_IS_SHORTCUT("limbo_ai/load_behavior_tree", p_event)) {
_popup_file_dialog(load_dialog);
} else if (LW_IS_SHORTCUT("limbo_ai/find_task", p_event)) {
task_tree->tree_search_show_and_focus();
} else {
handled = false;
}
@ -799,6 +878,9 @@ void LimboAIEditor::_misc_option_selected(int p_id) {
EDITOR_FILE_SYSTEM()->scan();
EDIT_SCRIPT(template_path);
} break;
case MISC_SEARCH_TREE: {
task_tree->tree_search_show_and_focus();
} break;
}
}
@ -819,6 +901,7 @@ void LimboAIEditor::_on_tree_task_activated() {
void LimboAIEditor::_on_visibility_changed() {
if (task_tree->is_visible_in_tree()) {
_switch_to_owner_scene_if_builtin(task_tree->get_bt());
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) {
EDIT_RESOURCE(sel);
@ -836,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() {
if (task_tree->get_bt().is_null()) {
return;
@ -854,7 +927,7 @@ void LimboAIEditor::_on_save_pressed() {
if (path.is_empty()) {
save_dialog->popup_centered_ratio();
} else {
_save_bt(path);
_save_current_bt(path);
}
}
@ -1042,16 +1115,25 @@ void LimboAIEditor::_tab_clicked(int p_tab) {
void LimboAIEditor::_tab_closed(int p_tab) {
ERR_FAIL_INDEX(p_tab, history.size());
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))) {
history_bt->disconnect(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::_set_as_dirty));
}
if (tab_search_context.has(history_bt)) {
tab_search_context.erase(history_bt);
}
history.remove_at(p_tab);
idx_history = MIN(idx_history, history.size() - 1);
TreeSearch::SearchInfo search_info_opened_tab;
if (idx_history < 0) {
_disable_editing();
} else {
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();
}
@ -1214,10 +1296,13 @@ void LimboAIEditor::_reload_modified() {
void LimboAIEditor::_resave_modified(String _str) {
for (const String &res_path : disk_changed_files) {
Ref<BehaviorTree> res = RESOURCE_LOAD(res_path, "BehaviorTree");
if (res.is_valid()) {
ERR_FAIL_COND(!res->is_class("BehaviorTree"));
RESOURCE_SAVE(res, res->get_path(), 0);
Ref<BehaviorTree> bt = RESOURCE_LOAD(res_path, "BehaviorTree");
if (bt.is_valid()) {
ERR_FAIL_COND(!bt->is_class("BehaviorTree"));
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();
@ -1242,14 +1327,13 @@ void LimboAIEditor::_rename_task_confirmed() {
undo_redo->commit_action();
}
void LimboAIEditor::apply_changes() {
void LimboAIEditor::save_all(bool p_external_only) {
for (int i = 0; i < history.size(); i++) {
Ref<BehaviorTree> bt = history.get(i);
String path = bt->get_path();
if (RESOURCE_EXISTS(path, "BehaviorTree")) {
RESOURCE_SAVE(bt, path, 0);
if (RESOURCE_EXISTS(path, "BehaviorTree") && (!p_external_only || RESOURCE_PATH_IS_EXTERNAL(path))) {
_save_bt(bt, path);
}
dirty.clear();
}
}
@ -1319,6 +1403,9 @@ void LimboAIEditor::_update_misc_menu() {
misc_menu->add_item(
FILE_EXISTS(_get_script_template_path()) ? TTR("Edit Script Template") : TTR("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() {
@ -1381,6 +1468,7 @@ void LimboAIEditor::_do_update_theme_item_cache() {
theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), LW_NAME(EditorIcons));
theme_cache.copy_icon = get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons));
theme_cache.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons));
theme_cache.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.percent_icon = LimboUtility::get_singleton()->get_task_icon("LimboPercent");
@ -1392,14 +1480,14 @@ void LimboAIEditor::_notification(int p_what) {
case NOTIFICATION_EXIT_TREE: {
task_tree->unload();
for (int i = 0; i < history.size(); i++) {
if (history[i]->is_connected(LW_NAME(changed), callable_mp(this, &LimboAIEditor::_mark_as_dirty))) {
history[i]->disconnect(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::_set_as_dirty));
}
}
} break;
case NOTIFICATION_READY: {
// **** 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));
extract_dialog->connect("file_selected", callable_mp(this, &LimboAIEditor::_extract_subtree));
new_btn->connect(LW_NAME(pressed), callable_mp(this, &LimboAIEditor::_new_bt));
@ -1463,7 +1551,7 @@ void LimboAIEditor::_bind_methods() {
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("_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("_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));
@ -1510,8 +1598,13 @@ 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/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/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/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)));
// 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);
@ -1818,14 +1911,6 @@ LimboAIEditor::~LimboAIEditor() {
//**** 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() {
}
@ -1834,9 +1919,13 @@ void LimboAIEditorPlugin::_notification(int p_notification) {
case NOTIFICATION_READY: {
add_debugger_plugin(memnew(LimboDebuggerPlugin));
add_inspector_plugin(memnew(EditorInspectorPluginBBPlan));
EditorInspectorPluginVariableName *var_plugin = memnew(EditorInspectorPluginVariableName);
var_plugin->set_editor_plan_provider(Callable(limbo_ai_editor, "get_edited_blackboard_plan"));
add_inspector_plugin(var_plugin);
EditorInspectorPluginPropertyPath *path_plugin = memnew(EditorInspectorPluginPropertyPath);
add_inspector_plugin(path_plugin);
#ifdef LIMBOAI_MODULE
// ! Only used in the module version.
EditorInspectorPluginBBParam *param_plugin = memnew(EditorInspectorPluginBBParam);
@ -1884,8 +1973,9 @@ void LimboAIEditorPlugin::edit(Object *p_object) {
#elif LIMBOAI_GDEXTENSION
void LimboAIEditorPlugin::_edit(Object *p_object) {
#endif
if (Object::cast_to<BehaviorTree>(p_object)) {
limbo_ai_editor->edit_bt(Object::cast_to<BehaviorTree>(p_object));
Ref<BehaviorTree> bt = Object::cast_to<BehaviorTree>(p_object);
if (bt.is_valid()) {
limbo_ai_editor->edit_bt(bt);
}
}
@ -1900,6 +1990,14 @@ bool LimboAIEditorPlugin::_handles(Object *p_object) const {
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
Ref<Texture2D> LimboAIEditorPlugin::_get_plugin_icon() const {
return LimboUtility::get_singleton()->get_task_icon("LimboAI");

View File

@ -20,6 +20,7 @@
#include "owner_picker.h"
#include "task_palette.h"
#include "task_tree.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE
#include "core/object/class_db.h"
@ -48,6 +49,7 @@
#ifdef LIMBOAI_GDEXTENSION
#include "godot_cpp/classes/accept_dialog.hpp"
#include <godot_cpp/classes/config_file.hpp>
#include <godot_cpp/classes/control.hpp>
#include <godot_cpp/classes/editor_plugin.hpp>
#include <godot_cpp/classes/editor_spin_slider.hpp>
@ -64,7 +66,6 @@
#include <godot_cpp/classes/texture2d.hpp>
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/variant.hpp>
#include <godot_cpp/classes/config_file.hpp>
using namespace godot;
@ -101,6 +102,7 @@ private:
MISC_LAYOUT_WIDESCREEN_OPTIMIZED,
MISC_PROJECT_SETTINGS,
MISC_CREATE_SCRIPT_TEMPLATE,
MISC_SEARCH_TREE
};
enum TabMenu {
@ -135,12 +137,14 @@ private:
Ref<Texture2D> cut_icon;
Ref<Texture2D> copy_icon;
Ref<Texture2D> paste_icon;
Ref<Texture2D> search_icon;
} theme_cache;
EditorPlugin *plugin;
EditorLayout editor_layout;
Vector<Ref<BehaviorTree>> history;
int idx_history;
HashMap<Ref<BehaviorTree>, TreeSearch::SearchInfo> tab_search_context;
bool updating_tabs = false;
bool request_update_tabs = false;
HashSet<Ref<BehaviorTree>> dirty;
@ -202,11 +206,12 @@ private:
void _update_misc_menu();
void _update_banners();
void _new_bt();
void _save_bt(String p_path);
void _load_bt(String p_path);
void _save_bt(const Ref<BehaviorTree> &p_bt, const 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 _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 _remove_task_from_favorite(const String &p_task);
void _save_and_restart();
@ -237,7 +242,6 @@ private:
void _on_tree_task_selected(const Ref<BTTask> &p_task);
void _on_tree_task_activated();
void _on_visibility_changed();
void _on_header_pressed();
void _on_save_pressed();
void _on_history_back();
void _on_history_forward();
@ -268,7 +272,7 @@ public:
void set_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
virtual void _shortcut_input(const Ref<InputEvent> &p_event) override { _process_shortcut_input(p_event); }
@ -294,23 +298,23 @@ public:
virtual String get_name() const override { return "LimboAI"; }
virtual void make_visible(bool p_visible) override;
virtual void apply_changes() override;
virtual void edit(Object *p_object) override;
virtual bool handles(Object *p_object) const override;
virtual void set_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
bool _has_main_screen() const override { return true; }
virtual String _get_plugin_name() const override { return "LimboAI"; }
virtual void _make_visible(bool p_visible) override;
virtual void _apply_changes() override;
virtual void _edit(Object *p_object) override;
virtual bool _handles(Object *p_object) const override;
virtual Ref<Texture2D> _get_plugin_icon() const 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 _save_external_data() override;
#endif // LIMBOAI_MODULE & LIMBOAI_GDEXTENSION
LimboAIEditorPlugin();

View File

@ -17,22 +17,23 @@
#include "../bt/tasks/composites/bt_probability_selector.h"
#include "../util/limbo_compat.h"
#include "../util/limbo_utility.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE
#include "core/object/script_language.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/label.h"
#include "scene/gui/texture_rect.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/label.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION
@ -46,6 +47,12 @@ TreeItem *TaskTree::_create_tree(const Ref<BTTask> &p_task, TreeItem *p_parent,
_create_tree(p_task->get_child(i), item);
}
_update_item(item);
// update TreeSearch if root task was created
if (tree->get_root() == item) {
tree_search->update_search(tree);
}
return item;
}
@ -105,6 +112,7 @@ void TaskTree::_update_item(TreeItem *p_item) {
if (!warning_text.is_empty()) {
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() {
@ -434,7 +442,7 @@ void TaskTree::_normalize_drop(TreeItem *item, int type, int &to_pos, Ref<BTTask
to_pos = to_task->get_index();
{
Vector<Ref<BTTask>> selected = get_selected_tasks();
if (to_task == selected[selected.size()-1]) {
if (to_task == selected[selected.size() - 1]) {
to_pos += 1;
}
}
@ -530,6 +538,8 @@ void TaskTree::_notification(int p_what) {
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_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;
case NOTIFICATION_THEME_CHANGED: {
_do_update_theme_item_cache();
@ -562,12 +572,38 @@ void TaskTree::_bind_methods() {
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() {
editable = true;
updating_tree = false;
VBoxContainer *vbox_container = memnew(VBoxContainer);
add_child(vbox_container);
vbox_container->set_anchors_preset(PRESET_FULL_RECT);
tree = memnew(Tree);
add_child(tree);
tree->set_v_size_flags(Control::SIZE_EXPAND_FILL);
vbox_container->add_child(tree);
tree->set_columns(2);
tree->set_column_expand(0, true);
tree->set_column_expand(1, false);
@ -578,6 +614,10 @@ TaskTree::TaskTree() {
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_search_panel = memnew(TreeSearchPanel);
tree_search = Ref(memnew(TreeSearch(tree_search_panel)));
vbox_container->add_child(tree_search_panel);
}
TaskTree::~TaskTree() {

View File

@ -9,9 +9,13 @@
* =============================================================================
*/
#ifndef TASK_TREE_H
#define TASK_TREE_H
#ifdef TOOLS_ENABLED
#include "../bt/behavior_tree.h"
#include "tree_search.h"
#ifdef LIMBOAI_MODULE
#include "scene/gui/control.h"
@ -43,6 +47,9 @@ private:
bool updating_tree;
HashMap<RECT_CACHE_KEY, Rect2> probability_rect_cache;
Ref<TreeSearch> tree_search;
TreeSearchPanel *tree_search_panel;
struct ThemeCache {
Ref<Font> comment_font;
Ref<Font> name_font;
@ -88,7 +95,7 @@ protected:
public:
void load_bt(const Ref<BehaviorTree> &p_behavior_tree);
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_task(const Ref<BTTask> &p_task);
void add_selection(const Ref<BTTask> &p_task);
@ -96,12 +103,16 @@ public:
Ref<BTTask> get_selected() const;
Vector<Ref<BTTask>> get_selected_tasks() const;
void clear_selection();
Rect2 get_selected_probability_rect() const;
double get_selected_probability_weight() const;
double get_selected_probability_percent() 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; }
TaskTree();
@ -109,3 +120,4 @@ public:
};
#endif // ! TOOLS_ENABLED
#endif // ! TASK_TREE_H

649
editor/tree_search.cpp Normal file
View File

@ -0,0 +1,649 @@
/**
* 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

160
editor/tree_search.h Normal file
View File

@ -0,0 +1,160 @@
/**
* 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,6 +25,10 @@ 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.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"
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.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) {
void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state, const StringName &p_event, const Callable &p_guard) {
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->get_parent() != this, "LimboHSM: Unable to add a transition to a state that is not an immediate child of mine.");
@ -108,8 +108,13 @@ void LimboHSM::add_transition(LimboState *p_from_state, LimboState *p_to_state,
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.");
// Note: Explicit casting needed for GDExtension.
transitions[key] = { p_from_state != nullptr ? ObjectID(p_from_state->get_instance_id()) : ObjectID(), ObjectID(p_to_state->get_instance_id()), p_event };
// Note: Explicit ObjectID casting needed for GDExtension.
transitions[key] = {
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) {
@ -166,13 +171,13 @@ bool LimboHSM::_dispatch(const StringName &p_event, const Variant &p_cargo) {
Transition transition;
_get_transition(active_state, p_event, transition);
if (transition.is_valid()) {
if (transition.is_valid() && transition.is_allowed()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
}
if (to_state == nullptr) {
// Get ANYSTATE transition.
_get_transition(nullptr, p_event, transition);
if (transition.is_valid()) {
if (transition.is_valid() && transition.is_allowed()) {
to_state = Object::cast_to<LimboState>(ObjectDB::get_instance(transition.to_state));
if (to_state == active_state) {
// Transitions to self are not allowed with ANYSTATE.
@ -260,22 +265,46 @@ 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) {
switch (p_what) {
case NOTIFICATION_POST_ENTER_TREE: {
if (was_active && is_root()) {
// Re-activate the root HSM if it was previously active.
// Typically, this happens when the node is re-entered scene repeatedly (e.g., re-parenting, pooling).
// Typically, this happens when the node is re-entered scene repeatedly (such as with object pooling).
set_active(true);
}
} break;
case NOTIFICATION_EXIT_TREE: {
if (is_root()) {
// Remember active status for re-parenting and exit state machine
// to release resources and signal connections if active.
was_active = active;
// Exit the state machine if the root HSM is no longer in the scene tree (except when being reparented).
// This ensures that resources and signal connections are released if active.
was_active = is_active();
if (is_active()) {
_exit();
// 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();
} 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;
@ -300,7 +329,7 @@ void LimboHSM::_bind_methods() {
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("update", "delta"), &LimboHSM::update);
ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event"), &LimboHSM::add_transition);
ClassDB::bind_method(D_METHOD("add_transition", "from_state", "to_state", "event", "guard"), &LimboHSM::add_transition, DEFVAL(Callable()));
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("anystate"), &LimboHSM::anystate);

View File

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

View File

@ -101,6 +101,7 @@
#include "editor/debugger/limbo_debugger.h"
#include "editor/debugger/limbo_debugger_plugin.h"
#include "editor/mode_switch_button.h"
#include "editor/tree_search.h"
#include "hsm/limbo_hsm.h"
#include "hsm/limbo_state.h"
#include "util/limbo_string_names.h"
@ -119,6 +120,7 @@
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include "editor/editor_property_property_path.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/memory.hpp>
@ -249,24 +251,28 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) {
#ifdef TOOLS_ENABLED
if (p_level == MODULE_INITIALIZATION_LEVEL_EDITOR) {
#ifdef LIMBOAI_GDEXTENSION
GDREGISTER_CLASS(TaskTree);
GDREGISTER_CLASS(TaskButton);
GDREGISTER_CLASS(TaskPaletteSection);
GDREGISTER_CLASS(TaskPalette);
GDREGISTER_CLASS(ActionBanner);
GDREGISTER_CLASS(ModeSwitchButton);
GDREGISTER_CLASS(CompatShortcutBin);
GDREGISTER_CLASS(CompatScreenSelect);
GDREGISTER_CLASS(CompatWindowWrapper);
GDREGISTER_CLASS(LimboDebuggerTab);
GDREGISTER_CLASS(LimboDebuggerPlugin);
GDREGISTER_CLASS(BlackboardPlanEditor);
GDREGISTER_CLASS(EditorInspectorPluginBBPlan);
GDREGISTER_CLASS(EditorPropertyVariableName);
GDREGISTER_CLASS(EditorInspectorPluginVariableName);
GDREGISTER_CLASS(OwnerPicker);
GDREGISTER_CLASS(LimboAIEditor);
GDREGISTER_CLASS(LimboAIEditorPlugin);
GDREGISTER_INTERNAL_CLASS(TaskTree);
GDREGISTER_INTERNAL_CLASS(TaskButton);
GDREGISTER_INTERNAL_CLASS(TaskPaletteSection);
GDREGISTER_INTERNAL_CLASS(TaskPalette);
GDREGISTER_INTERNAL_CLASS(ActionBanner);
GDREGISTER_INTERNAL_CLASS(ModeSwitchButton);
GDREGISTER_INTERNAL_CLASS(CompatShortcutBin);
GDREGISTER_INTERNAL_CLASS(CompatScreenSelect);
GDREGISTER_INTERNAL_CLASS(CompatWindowWrapper);
GDREGISTER_INTERNAL_CLASS(LimboDebuggerTab);
GDREGISTER_INTERNAL_CLASS(LimboDebuggerPlugin);
GDREGISTER_INTERNAL_CLASS(BlackboardPlanEditor);
GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginBBPlan);
GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginPropertyPath);
GDREGISTER_INTERNAL_CLASS(EditorPropertyPropertyPath);
GDREGISTER_INTERNAL_CLASS(EditorPropertyVariableName);
GDREGISTER_INTERNAL_CLASS(EditorInspectorPluginVariableName);
GDREGISTER_INTERNAL_CLASS(OwnerPicker);
GDREGISTER_INTERNAL_CLASS(LimboAIEditor);
GDREGISTER_INTERNAL_CLASS(LimboAIEditorPlugin);
GDREGISTER_INTERNAL_CLASS(TreeSearchPanel);
GDREGISTER_INTERNAL_CLASS(TreeSearch);
#endif // LIMBOAI_GDEXTENSION
EditorPlugins::add_by_type<LimboAIEditorPlugin>();

View File

@ -215,7 +215,7 @@ TEST_CASE("[Modules][LimboAI] HSM") {
CHECK(beta_updates->num_callbacks == 0);
CHECK(beta_exits->num_callbacks == 1); // * exited
}
SUBCASE("Test transition with guard") {
SUBCASE("Test transition with state-wide guard") {
Ref<TestGuard> guard = memnew(TestGuard);
state_beta->set_guard(callable_mp(guard.ptr(), &TestGuard::can_enter));
@ -234,6 +234,25 @@ TEST_CASE("[Modules][LimboAI] HSM") {
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") {
hsm->dispatch("not_found");
CHECK(alpha_exits->num_callbacks == 0);

View File

@ -114,12 +114,13 @@ using namespace godot;
#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 OBJECT_DB_GET_INSTANCE(m_id) ObjectDB::get_instance(m_id)
#define EDITOR_DEF(m_setting, m_value) 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); \
if (!EDITOR_SETTINGS()->has_setting(m_setting)) { \
EDITOR_SETTINGS()->set_setting(m_setting, m_value); \
} \
} while(0)
#define EDITOR_DEF(m_setting, m_value) \
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); \
if (!EDITOR_SETTINGS()->has_setting(m_setting)) { \
EDITOR_SETTINGS()->set_setting(m_setting, m_value); \
} \
} while (0)
_FORCE_INLINE_ bool OBJECT_HAS_PROPERTY(Object *p_obj, const StringName &p_prop) {
return Variant(p_obj).has_key(p_prop);
@ -151,6 +152,7 @@ Variant _GLOBAL_DEF(const PropertyInfo &p_info, const Variant &p_default, bool p
#define EDSCALE (EditorInterface::get_singleton()->get_editor_scale())
String TTR(const String &p_text, const String &p_context = "");
#define RTR(m_text) TTR(m_text)
#endif // ! LIMBOAI_GDEXTENSION
@ -174,7 +176,9 @@ Variant VARIANT_DEFAULT(Variant::Type p_type);
#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_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_EXTERNAL(m_path) (!RESOURCE_PATH_IS_BUILT_IN(m_path))
#ifdef TOOLS_ENABLED

View File

@ -40,12 +40,15 @@ LimboStringNames::LimboStringNames() {
add_child = SN("add_child");
add_child_at_index = SN("add_child_at_index");
AnimationFilter = SN("AnimationFilter");
BBParam = SN("BBParam");
behavior_tree_finished = SN("behavior_tree_finished");
bold = SN("bold");
button_down = SN("button_down");
button_up = SN("button_up");
call_deferred = SN("call_deferred");
changed = SN("changed");
Clear = SN("Clear");
Close = SN("Close");
dark_color_2 = SN("dark_color_2");
Debug = SN("Debug");
disabled_font_color = SN("disabled_font_color");
@ -58,14 +61,18 @@ LimboStringNames::LimboStringNames() {
EditorFonts = SN("EditorFonts");
EditorIcons = SN("EditorIcons");
EditorStyles = SN("EditorStyles");
emit_signal = SN("emit_signal");
entered = SN("entered");
error_value = SN("error_value");
EVENT_FAILURE = SN("failure");
EVENT_FINISHED = SN("finished");
EVENT_SUCCESS = SN("success");
exited = SN("exited");
ExternalLink = SN("ExternalLink");
favorite_tasks_changed = SN("favorite_tasks_changed");
Favorites = SN("Favorites");
FlatButton = SN("FlatButton");
Focus = SN("Focus");
focus_exited = SN("focus_exited");
font = SN("font");
font_color = SN("font_color");
@ -73,10 +80,12 @@ LimboStringNames::LimboStringNames() {
freed = SN("freed");
gui_input = SN("gui_input");
GuiOptionArrow = SN("GuiOptionArrow");
GuiTabMenuHl = SN("GuiTabMenuHl");
GuiTreeArrowDown = SN("GuiTreeArrowDown");
GuiTreeArrowRight = SN("GuiTreeArrowRight");
HeaderSmall = SN("HeaderSmall");
Help = SN("Help");
h_separation = SN("h_separation");
icon_max_width = SN("icon_max_width");
class_icon_size = SN("class_icon_size");
id_pressed = SN("id_pressed");
@ -120,6 +129,7 @@ LimboStringNames::LimboStringNames() {
separation = SN("separation");
set_custom_name = SN("set_custom_name");
set_root_task = SN("set_root_task");
set_visible = SN("set_visible");
set_v_scroll = SN("set_v_scroll");
setup = SN("setup");
started = SN("started");

View File

@ -56,12 +56,15 @@ public:
StringName add_child;
StringName Add;
StringName AnimationFilter;
StringName BBParam;
StringName behavior_tree_finished;
StringName bold;
StringName button_down;
StringName button_up;
StringName call_deferred;
StringName changed;
StringName Clear;
StringName Close;
StringName dark_color_2;
StringName Debug;
StringName disabled_font_color;
@ -74,14 +77,18 @@ public:
StringName EditorFonts;
StringName EditorIcons;
StringName EditorStyles;
StringName emit_signal;
StringName entered;
StringName error_value;
StringName EVENT_FAILURE;
StringName EVENT_FINISHED;
StringName EVENT_SUCCESS;
StringName exited;
StringName ExternalLink;
StringName favorite_tasks_changed;
StringName Favorites;
StringName FlatButton;
StringName Focus;
StringName focus_exited;
StringName font_color;
StringName font_size;
@ -89,10 +96,12 @@ public:
StringName freed;
StringName gui_input;
StringName GuiOptionArrow;
StringName GuiTabMenuHl;
StringName GuiTreeArrowDown;
StringName GuiTreeArrowRight;
StringName HeaderSmall;
StringName Help;
StringName h_separation;
StringName icon_max_width;
StringName class_icon_size;
StringName id_pressed;
@ -136,6 +145,7 @@ public:
StringName separation;
StringName set_custom_name;
StringName set_root_task;
StringName set_visible;
StringName set_v_scroll;
StringName setup;
StringName started;