From fb958c7c340df4ede4cb8a8ddca73fd8c9a6bdfb Mon Sep 17 00:00:00 2001 From: "Wilson E. Alvarez" Date: Sat, 17 Feb 2024 16:00:25 -0500 Subject: [PATCH] Add BTEvaluateExpression --- bt/tasks/utility/bt_evaluate_expression.cpp | 159 ++++++++++++++ bt/tasks/utility/bt_evaluate_expression.h | 77 +++++++ config.py | 1 + doc_classes/BTEvaluateExpression.xml | 43 ++++ register_types.cpp | 2 + tests/test_evaluate_expression.h | 216 ++++++++++++++++++++ 6 files changed, 498 insertions(+) create mode 100644 bt/tasks/utility/bt_evaluate_expression.cpp create mode 100644 bt/tasks/utility/bt_evaluate_expression.h create mode 100644 doc_classes/BTEvaluateExpression.xml create mode 100644 tests/test_evaluate_expression.h diff --git a/bt/tasks/utility/bt_evaluate_expression.cpp b/bt/tasks/utility/bt_evaluate_expression.cpp new file mode 100644 index 0000000..ad5ce58 --- /dev/null +++ b/bt/tasks/utility/bt_evaluate_expression.cpp @@ -0,0 +1,159 @@ +/** + * bt_evaluate_expression.cpp + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * Copyright 2024 Wilson E. Alvarez + * + * 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 "bt_evaluate_expression.h" + +#include "../../../util/limbo_compat.h" +#include "../../../util/limbo_utility.h" + +#ifdef LIMBOAI_GDEXTENSION +#include "godot_cpp/classes/global_constants.hpp" +#endif // LIMBOAI_GDEXTENSION + +//**** Setters / Getters + +void BTEvaluateExpression::set_expression_string(const String &p_expression_string) { + expression_string = p_expression_string; + emit_changed(); +} + +void BTEvaluateExpression::set_node_param(Ref p_object) { + node_param = p_object; + emit_changed(); + if (Engine::get_singleton()->is_editor_hint() && node_param.is_valid()) { + node_param->connect(LW_NAME(changed), Callable(this, LW_NAME(emit_changed))); + } +} + +void BTEvaluateExpression::set_input_include_delta(bool p_input_include_delta) { + if (input_include_delta != p_input_include_delta) { + processed_input_values.resize(input_values.size() + int(p_input_include_delta)); + } + input_include_delta = p_input_include_delta; + emit_changed(); +} + +void BTEvaluateExpression::set_input_names(const PackedStringArray &p_input_names) { + input_names = p_input_names; + emit_changed(); +} + +void BTEvaluateExpression::set_input_values(const TypedArray &p_input_values) { + if (input_values.size() != p_input_values.size()) { + processed_input_values.resize(p_input_values.size() + int(input_include_delta)); + } + input_values = p_input_values; + emit_changed(); +} + +void BTEvaluateExpression::set_result_var(const String &p_result_var) { + result_var = p_result_var; + emit_changed(); +} + +//**** Task Implementation + +PackedStringArray BTEvaluateExpression::get_configuration_warnings() { + PackedStringArray warnings = BTAction::get_configuration_warnings(); + if (expression_string.is_empty()) { + warnings.append("Expression string is not set."); + } + if (node_param.is_null()) { + warnings.append("Node parameter is not set."); + } else if (node_param->get_value_source() == BBParam::SAVED_VALUE && node_param->get_saved_value() == Variant()) { + warnings.append("Path to node is not set."); + } else if (node_param->get_value_source() == BBParam::BLACKBOARD_VAR && node_param->get_variable() == String()) { + warnings.append("Node blackboard variable is not set."); + } + return warnings; +} + +void BTEvaluateExpression::_setup() { + parse(); + ERR_FAIL_COND_MSG(is_parsed != Error::OK, "BTEvaluateExpression: Failed to parse expression: " + expression.get_error_text()); +} + +Error BTEvaluateExpression::parse() { + PackedStringArray processed_input_names; + processed_input_names.resize(input_names.size() + int(input_include_delta)); + String *processed_input_names_ptr = processed_input_names.ptrw(); + if (input_include_delta) { + processed_input_names_ptr[0] = "delta"; + } + for (int i = 0; i < input_names.size(); ++i) { + processed_input_names_ptr[i + int(input_include_delta)] = input_names[i]; + } + + is_parsed = expression.parse(expression_string, processed_input_names); + return is_parsed; +} + +String BTEvaluateExpression::_generate_name() { + return vformat("EvaluateExpression %s node: %s %s", + !expression_string.is_empty() ? expression_string : "???", + node_param.is_valid() && !node_param->to_string().is_empty() ? node_param->to_string() : "???", + result_var.is_empty() ? "" : LimboUtility::get_singleton()->decorate_output_var(result_var)); +} + +BT::Status BTEvaluateExpression::_tick(double p_delta) { + ERR_FAIL_COND_V_MSG(expression_string.is_empty(), FAILURE, "BTEvaluateExpression: Expression String is not set."); + ERR_FAIL_COND_V_MSG(node_param.is_null(), FAILURE, "BTEvaluateExpression: Node parameter is not set."); + Object *obj = node_param->get_value(get_agent(), get_blackboard()); + ERR_FAIL_COND_V_MSG(obj == nullptr, FAILURE, "BTEvaluateExpression: Failed to get object: " + node_param->to_string()); + ERR_FAIL_COND_V_MSG(is_parsed != Error::OK, FAILURE, "BTEvaluateExpression: Failed to parse expression: " + expression.get_error_text()); + + if (input_include_delta) { + processed_input_values[0] = p_delta; + } + for (int i = 0; i < input_values.size(); ++i) { + const Ref &bb_variant = input_values[i]; + processed_input_values[i + int(input_include_delta)] = bb_variant->get_value(get_agent(), get_blackboard()); + } + + Variant result = expression.execute(processed_input_values, obj, false); + ERR_FAIL_COND_V_MSG(expression.has_execute_failed(), FAILURE, "BTEvaluateExpression: Failed to execute: " + expression.get_error_text()); + + if (!result_var.is_empty()) { + get_blackboard()->set_var(result_var, result); + } + + return SUCCESS; +} + +//**** Godot + +void BTEvaluateExpression::_bind_methods() { + ClassDB::bind_method(D_METHOD("parse"), &BTEvaluateExpression::parse); + ClassDB::bind_method(D_METHOD("set_expression_string", "p_method"), &BTEvaluateExpression::set_expression_string); + ClassDB::bind_method(D_METHOD("get_expression_string"), &BTEvaluateExpression::get_expression_string); + ClassDB::bind_method(D_METHOD("set_node_param", "p_param"), &BTEvaluateExpression::set_node_param); + ClassDB::bind_method(D_METHOD("get_node_param"), &BTEvaluateExpression::get_node_param); + ClassDB::bind_method(D_METHOD("set_input_names", "p_input_names"), &BTEvaluateExpression::set_input_names); + ClassDB::bind_method(D_METHOD("get_input_names"), &BTEvaluateExpression::get_input_names); + ClassDB::bind_method(D_METHOD("set_input_values", "p_input_values"), &BTEvaluateExpression::set_input_values); + ClassDB::bind_method(D_METHOD("get_input_values"), &BTEvaluateExpression::get_input_values); + ClassDB::bind_method(D_METHOD("set_input_include_delta", "p_input_include_delta"), &BTEvaluateExpression::set_input_include_delta); + ClassDB::bind_method(D_METHOD("is_input_delta_included"), &BTEvaluateExpression::is_input_delta_included); + ClassDB::bind_method(D_METHOD("set_result_var", "p_result_var"), &BTEvaluateExpression::set_result_var); + ClassDB::bind_method(D_METHOD("get_result_var"), &BTEvaluateExpression::get_result_var); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "BBNode"), "set_node_param", "get_node_param"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "expression_string"), "set_expression_string", "get_expression_string"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "result_var"), "set_result_var", "get_result_var"); + ADD_GROUP("Inputs", "input_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "input_include_delta"), "set_input_include_delta", "is_input_delta_included"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "input_names", PROPERTY_HINT_ARRAY_TYPE, "String"), "set_input_names", "get_input_names"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "input_values", PROPERTY_HINT_ARRAY_TYPE, RESOURCE_TYPE_HINT("BBVariant")), "set_input_values", "get_input_values"); +} + +BTEvaluateExpression::BTEvaluateExpression() { +} diff --git a/bt/tasks/utility/bt_evaluate_expression.h b/bt/tasks/utility/bt_evaluate_expression.h new file mode 100644 index 0000000..024a8ba --- /dev/null +++ b/bt/tasks/utility/bt_evaluate_expression.h @@ -0,0 +1,77 @@ +/** + * bt_evaluate_expression.h + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * Copyright 2024 Wilson E. Alvarez + * + * 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 BT_EVALUATE_EXPRESSION_H +#define BT_EVALUATE_EXPRESSION_H + +#include "../bt_action.h" + +#ifdef LIMBOAI_MODULE +#include "core/math/expression.h" +#endif + +#ifdef LIMBOAI_GDEXTENSION +#include +#endif + +#include "../../../blackboard/bb_param/bb_node.h" +#include "../../../blackboard/bb_param/bb_variant.h" + +class BTEvaluateExpression : public BTAction { + GDCLASS(BTEvaluateExpression, BTAction); + TASK_CATEGORY(Utility); + +private: + Expression expression; + Error is_parsed = FAILED; + Ref node_param; + String expression_string; + PackedStringArray input_names; + TypedArray input_values; + bool input_include_delta = false; + Array processed_input_values; + String result_var; + +protected: + static void _bind_methods(); + + virtual String _generate_name() override; + virtual void _setup() override; + virtual Status _tick(double p_delta) override; + +public: + Error parse(); + + void set_expression_string(const String &p_expression_string); + String get_expression_string() const { return expression_string; } + + void set_node_param(Ref p_object); + Ref get_node_param() const { return node_param; } + + void set_input_names(const PackedStringArray &p_input_names); + PackedStringArray get_input_names() const { return input_names; } + + void set_input_values(const TypedArray &p_input_values); + TypedArray get_input_values() const { return input_values; } + + void set_input_include_delta(bool p_input_include_delta); + bool is_input_delta_included() const { return input_include_delta; } + + void set_result_var(const String &p_result_var); + String get_result_var() const { return result_var; } + + virtual PackedStringArray get_configuration_warnings() override; + + BTEvaluateExpression(); +}; + +#endif // BT_EVALUATE_EXPRESSION_H diff --git a/config.py b/config.py index 875d0d3..fe81b90f 100644 --- a/config.py +++ b/config.py @@ -68,6 +68,7 @@ def get_doc_classes(): "BTAlwaysSucceed", "BTAwaitAnimation", "BTCallMethod", + "BTEvaluateExpression", "BTCheckAgentProperty", "BTCheckTrigger", "BTCheckVar", diff --git a/doc_classes/BTEvaluateExpression.xml b/doc_classes/BTEvaluateExpression.xml new file mode 100644 index 0000000..36593a9 --- /dev/null +++ b/doc_classes/BTEvaluateExpression.xml @@ -0,0 +1,43 @@ + + + + BT action that evaluates an [Expression] against a specified [Node] or [Object]. + + + BTEvaluateExpression action evaluates an [member expression_string] on the specified [Node] or [Object] instance and returns [code]SUCCESS[/code] when the [Expression] executes successfully. + Returns [code]FAILURE[/code] if the action encounters an issue during the [Expression] parsing or execution. + + + + + + + + Calls [method Expression.parse] considering [member input_include_delta] and [member input_names] and returns its error code. + + + + + + The expression string to be parsed and executed. + [b]Warning:[/b] Call [method parse] after updating [member expression_string] to update the internal [Expression] as it won't be updated automatically. + + + If enabled, the input variable [code]delta[/code] will be added to [member input_names] and [member input_values]. + [b]Warning:[/b] Call [method parse] after toggling [member input_include_delta] to update the internal [Expression] as it won't be updated automatically. + + + List of variable names within [member expression_string] for which the user will provide values for through [member input_values]. + [b]Warning:[/b] Call [method parse] after updating [member input_names] to update the internal [Expression] as it won't be updated automatically. + + + List of values for variables specified in [member input_names]. The values are mapped to the variables by their array index. + + + Specifies the [Node] or [Object] instance containing the method to be called. + + + if non-empty, assign the result of the method call to the blackboard variable specified by this property. + + + diff --git a/register_types.cpp b/register_types.cpp index d74a015..d209c9f 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -90,6 +90,7 @@ #include "bt/tasks/scene/bt_stop_animation.h" #include "bt/tasks/utility/bt_call_method.h" #include "bt/tasks/utility/bt_console_print.h" +#include "bt/tasks/utility/bt_evaluate_expression.h" #include "bt/tasks/utility/bt_fail.h" #include "bt/tasks/utility/bt_random_wait.h" #include "bt/tasks/utility/bt_wait.h" @@ -182,6 +183,7 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(BTCondition); LIMBO_REGISTER_TASK(BTAwaitAnimation); LIMBO_REGISTER_TASK(BTCallMethod); + LIMBO_REGISTER_TASK(BTEvaluateExpression); LIMBO_REGISTER_TASK(BTConsolePrint); LIMBO_REGISTER_TASK(BTFail); LIMBO_REGISTER_TASK(BTPauseAnimation); diff --git a/tests/test_evaluate_expression.h b/tests/test_evaluate_expression.h new file mode 100644 index 0000000..9058911 --- /dev/null +++ b/tests/test_evaluate_expression.h @@ -0,0 +1,216 @@ +/** + * test_evaluate_expression.h + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * Copyright 2024 Wilson E. Alvarez + * + * 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 TEST_EVALUATE_EXPRESSION_H +#define TEST_EVALUATE_EXPRESSION_H + +#include "limbo_test.h" + +#include "modules/limboai/blackboard/bb_param/bb_node.h" +#include "modules/limboai/blackboard/blackboard.h" +#include "modules/limboai/bt/tasks/bt_task.h" +#include "modules/limboai/bt/tasks/utility/bt_evaluate_expression.h" + +#include "core/os/memory.h" +#include "core/variant/array.h" + +namespace TestEvaluateExpression { + +TEST_CASE("[Modules][LimboAI] BTEvaluateExpression") { + Ref ee = memnew(BTEvaluateExpression); + + SUBCASE("When node parameter is null") { + ee->set_node_param(nullptr); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + + SUBCASE("With object on the blackboard") { + Node *dummy = memnew(Node); + Ref bb = memnew(Blackboard); + + Ref node_param = memnew(BBNode); + ee->set_node_param(node_param); + Ref callback_counter = memnew(CallbackCounter); + bb->set_var("object", callback_counter); + node_param->set_value_source(BBParam::BLACKBOARD_VAR); + node_param->set_variable("object"); + ee->set_expression_string("callback()"); + + ee->initialize(dummy, bb); + + SUBCASE("When expression string is empty") { + ee->set_expression_string(""); + CHECK(ee->parse() == ERR_INVALID_PARAMETER); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + SUBCASE("When expression string calls non-existent function") { + ee->set_expression_string("not_found()"); + CHECK(ee->parse() == OK); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + SUBCASE("When expression string accesses a non-existent property") { + ee->set_expression_string("not_found"); + CHECK(ee->parse() == OK); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + SUBCASE("When expression string can't be parsed") { + ee->set_expression_string("assignment = failure"); + CHECK(ee->parse() == ERR_INVALID_PARAMETER); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + } + SUBCASE("When expression is valid") { + ee->set_expression_string("callback()"); + CHECK(ee->parse() == OK); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 1); + } + SUBCASE("With inputs") { + ee->set_expression_string("callback_delta(delta)"); + + SUBCASE("Should fail with 0 inputs") { + ee->set_input_include_delta(false); + ee->set_input_names(PackedStringArray()); + CHECK(ee->parse() == OK); + ee->set_input_values(TypedArray()); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 0); + } + SUBCASE("Should succeed with too many inputs") { + ee->set_input_include_delta(true); + PackedStringArray input_names; + input_names.push_back("point_two"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant(0.2))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 1); + } + SUBCASE("Should fail with a wrong type arg") { + ee->set_input_include_delta(false); + PackedStringArray input_names; + input_names.push_back("delta"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant("wrong data type"))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 1); + } + SUBCASE("Should succeed with delta included") { + ee->set_input_include_delta(true); + ee->set_input_names(PackedStringArray()); + CHECK(ee->parse() == OK); + ee->set_input_values(TypedArray()); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 1); + } + SUBCASE("Should succeed with one float arg") { + ee->set_input_include_delta(false); + PackedStringArray input_names; + input_names.push_back("delta"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant(0.2))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 1); + } + } + + SUBCASE("Should fail with too many method arguments") { + ee->set_expression_string("callback_delta(delta, extra)"); + ee->set_input_include_delta(true); + PackedStringArray input_names; + input_names.push_back("point_two"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant(0.2))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::FAILURE); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 0); + } + + SUBCASE("When toggling input_include_delta") { + ee->set_expression_string("delta + extra"); + ee->set_result_var("sum_result"); + + SUBCASE("Sum should be greater than 1 with input_include_delta set to false") { + ee->set_input_include_delta(false); + PackedStringArray input_names; + input_names.push_back("delta"); + input_names.push_back("extra"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant(0.01666))); + input_values.push_back(memnew(BBVariant(1))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + CHECK(float(ee->get_blackboard()->get_var("sum_result", 0)) > 1); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 0); + } + + SUBCASE("Sum should be greater than 1 with input_include_delta set to true") { + ee->set_input_include_delta(true); + PackedStringArray input_names; + input_names.push_back("extra"); + ee->set_input_names(input_names); + CHECK(ee->parse() == OK); + TypedArray input_values; + input_values.push_back(memnew(BBVariant(1))); + ee->set_input_values(input_values); + ERR_PRINT_OFF; + CHECK(ee->execute(0.01666) == BTTask::SUCCESS); + CHECK(float(ee->get_blackboard()->get_var("sum_result", 0)) > 1); + ERR_PRINT_ON; + CHECK(callback_counter->num_callbacks == 0); + } + } + + memdelete(dummy); + } +} + +} //namespace TestEvaluateExpression + +#endif // TEST_EVALUATE_EXPRESSION_H