Merge pull request #43 from Rubonnek/add-bt-evaluate-expression

Add `BTEvaluateExpression`
This commit is contained in:
Serhii Snitsaruk 2024-02-18 19:43:34 +01:00 committed by GitHub
commit 0caa2369c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 498 additions and 0 deletions

View File

@ -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<BBNode> 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<BBVariant> &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<BBVariant> &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() {
}

View File

@ -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 <godot_cpp/classes/expression.hpp>
#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<BBNode> node_param;
String expression_string;
PackedStringArray input_names;
TypedArray<BBVariant> 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<BBNode> p_object);
Ref<BBNode> 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<BBVariant> &p_input_values);
TypedArray<BBVariant> 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

View File

@ -68,6 +68,7 @@ def get_doc_classes():
"BTAlwaysSucceed",
"BTAwaitAnimation",
"BTCallMethod",
"BTEvaluateExpression",
"BTCheckAgentProperty",
"BTCheckTrigger",
"BTCheckVar",

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="BTEvaluateExpression" inherits="BTAction" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../doc/class.xsd">
<brief_description>
BT action that evaluates an [Expression] against a specified [Node] or [Object].
</brief_description>
<description>
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.
</description>
<tutorials>
</tutorials>
<methods>
<method name="parse">
<return type="int" enum="Error" />
<description>
Calls [method Expression.parse] considering [member input_include_delta] and [member input_names] and returns its error code.
</description>
</method>
</methods>
<members>
<member name="expression_string" type="String" setter="set_expression_string" getter="get_expression_string" default="&quot;&quot;">
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.
</member>
<member name="input_include_delta" type="bool" setter="set_input_include_delta" getter="is_input_delta_included" default="false">
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.
</member>
<member name="input_names" type="PackedStringArray" setter="set_input_names" getter="get_input_names" default="PackedStringArray()">
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.
</member>
<member name="input_values" type="BBVariant[]" setter="set_input_values" getter="get_input_values" default="[]">
List of values for variables specified in [member input_names]. The values are mapped to the variables by their array index.
</member>
<member name="node" type="BBNode" setter="set_node_param" getter="get_node_param">
Specifies the [Node] or [Object] instance containing the method to be called.
</member>
<member name="result_var" type="String" setter="set_result_var" getter="get_result_var" default="&quot;&quot;">
if non-empty, assign the result of the method call to the blackboard variable specified by this property.
</member>
</members>
</class>

View File

@ -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);

View File

@ -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<BTEvaluateExpression> 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<Blackboard> bb = memnew(Blackboard);
Ref<BBNode> node_param = memnew(BBNode);
ee->set_node_param(node_param);
Ref<CallbackCounter> 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<BBVariant>());
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<BBVariant> 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<BBVariant> 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<BBVariant>());
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<BBVariant> 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<BBVariant> 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<BBVariant> 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<BBVariant> 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