Lua-based behavior tree node

This commit is contained in:
2026-05-01 00:31:06 +03:00
parent 0fd8deaf53
commit 976ced3731
13 changed files with 1619 additions and 3 deletions

View File

@@ -107,6 +107,7 @@ set(EDITSCENE_SOURCES
ui/InlineBehaviorTreeEditor.cpp
ui/NavMeshEditor.cpp
ui/ActionDatabaseEditor.cpp
ui/ActionDatabaseSingletonEditor.cpp
ui/ActionDebugEditor.cpp
ui/ComponentRegistration.cpp
components/GoapBlackboard.cpp
@@ -152,6 +153,7 @@ set(EDITSCENE_SOURCES
lua/LuaComponentApi.cpp
lua/LuaEventApi.cpp
lua/LuaActionApi.cpp
lua/LuaBehaviorTreeApi.cpp
)
set(EDITSCENE_HEADERS
@@ -283,6 +285,7 @@ set(EDITSCENE_HEADERS
ui/NavMeshEditor.hpp
ui/NavMeshGeometrySourceEditor.hpp
ui/ActionDatabaseEditor.hpp
ui/ActionDatabaseSingletonEditor.hpp
ui/ActionDebugEditor.hpp
components/GoapBlackboard.hpp
components/GoapExpression.hpp
@@ -301,6 +304,7 @@ set(EDITSCENE_HEADERS
lua/LuaComponentApi.hpp
lua/LuaEventApi.hpp
lua/LuaActionApi.hpp
lua/LuaBehaviorTreeApi.hpp
)
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
@@ -412,7 +416,27 @@ target_include_directories(action_db_lua_test PRIVATE
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
# Test: Behavior Tree Lua API
add_executable(behavior_tree_lua_test
tests/behavior_tree_lua_test.cpp
lua/LuaBehaviorTreeApi.cpp
lua/LuaEntityApi.cpp
components/GoapBlackboard.cpp
)
target_link_libraries(behavior_tree_lua_test
lua
flecs::flecs_static
OgreMain
)
target_include_directories(behavior_tree_lua_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/tests
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
)
target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC)
# Copy local resources (materials, etc.)
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")

View File

@@ -88,6 +88,7 @@
#include "lua/LuaComponentApi.hpp"
#include "lua/LuaEventApi.hpp"
#include "lua/LuaActionApi.hpp"
#include "lua/LuaBehaviorTreeApi.hpp"
//=============================================================================
// ImGuiRenderListener Implementation
@@ -506,6 +507,7 @@ void EditorApp::setup()
editScene::registerLuaComponentApi(L);
editScene::registerLuaEventApi(L);
editScene::registerLuaActionApi(L);
editScene::registerLuaBehaviorTreeApi(L);
// Run late setup: load data.lua and initial scripts.
m_lua.lateSetup();

View File

@@ -50,6 +50,15 @@
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
* (for quest rewards, etc.).
* params="itemId,itemName,itemType,count,weight,value"
*
* --- Lua node ---
* "luaTask" - Leaf: calls a registered Lua function.
* name = registered node handler name.
* params = "key=val,key2=val2" passed to the Lua function.
* The Lua function receives (entity_id, params_table)
* and must return "success", "failure", or "running".
* Register handlers via:
* ecs.behavior_tree.register_node("name", function)
*/
struct BehaviorTreeNode {
Ogre::String type = "task";
@@ -95,7 +104,8 @@ struct BehaviorTreeNode {
type == "sendEvent" || type == "hasItem" ||
type == "hasItemByName" || type == "countItem" ||
type == "pickupItem" || type == "dropItem" ||
type == "useItem" || type == "addItemToInventory";
type == "useItem" || type == "addItemToInventory" ||
type == "luaTask";
}
};

View File

@@ -0,0 +1,316 @@
-- =============================================================================
-- Behavior Tree Lua API Examples
-- =============================================================================
-- This file demonstrates how to create custom behavior tree nodes using
-- Lua functions via the ecs.behavior_tree API.
--
-- The API allows you to:
-- 1. Register Lua functions as behavior tree node handlers
-- 2. Create behavior tree nodes that invoke those handlers
-- 3. Return "success", "failure", or "running" to control tree flow
-- 4. Pass parameters from the behavior tree editor to your Lua function
-- =============================================================================
-- =============================================================================
-- Registering Node Handlers
-- =============================================================================
-- Use ecs.behavior_tree.register_node(name, function) to register a Lua
-- function as a behavior tree node handler.
--
-- The function receives two arguments:
-- entity_id - The entity ID executing this behavior tree node
-- params - A table of parameters parsed from the node's params string
--
-- The function must return one of:
-- "success" - Node completed successfully (tree continues)
-- "failure" - Node failed (tree stops with failure)
-- "running" - Node is still running (will be called again next frame)
-- =============================================================================
-- =============================================================================
-- Example 1: Simple greeting node
-- =============================================================================
-- This node just prints a message and succeeds immediately.
ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
local message = params.message or "Hello!"
print("Entity " .. entity_id .. " says: " .. message)
return "success"
end)
-- =============================================================================
-- Example 2: Node that checks a blackboard value
-- =============================================================================
-- This node checks if a blackboard value meets a threshold.
-- It succeeds if the check passes, fails otherwise.
ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params)
local key = params.key
local min_val = tonumber(params.min) or 0
if not key then
print("[BT] check_blackboard_value: no 'key' param provided")
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
print("[BT] Entity " .. entity_id .. " has no GoapBlackboard")
return "failure"
end
local value = bb.values[key]
if value == nil then
print("[BT] Blackboard has no key '" .. key .. "'")
return "failure"
end
if value >= min_val then
print("[BT] Check passed: " .. key .. " = " .. value .. " >= " .. min_val)
return "success"
else
print("[BT] Check failed: " .. key .. " = " .. value .. " < " .. min_val)
return "failure"
end
end)
-- =============================================================================
-- Example 3: Node that runs over multiple frames (running state)
-- =============================================================================
-- This node waits for a specified duration, storing its progress in the
-- blackboard's floatValues. It returns "running" each frame until done.
ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params)
local duration = tonumber(params.duration) or 1.0
local timer_key = params.timer_key or "wait_timer"
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
-- Get elapsed time from blackboard (or start at 0)
local elapsed = bb.floatValues[timer_key] or 0.0
-- Get delta time (requires ecs.get_delta_time to be available)
-- For now, use a fixed step approximation
local dt = 0.016 -- ~60 FPS
elapsed = elapsed + dt
bb.floatValues[timer_key] = elapsed
-- Save updated blackboard
ecs.set_component(entity_id, "GoapBlackboard", bb)
if elapsed >= duration then
-- Done waiting - clean up timer
bb.floatValues[timer_key] = nil
ecs.set_component(entity_id, "GoapBlackboard", bb)
print("[BT] Wait complete after " .. duration .. " seconds")
return "success"
end
-- Still waiting
return "running"
end)
-- =============================================================================
-- Example 4: Node that modifies blackboard values
-- =============================================================================
-- This node adds a value to a blackboard integer value.
ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params)
local key = params.key
local amount = tonumber(params.amount) or 1
if not key then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
bb.values[key] = (bb.values[key] or 0) + amount
ecs.set_component(entity_id, "GoapBlackboard", bb)
print("[BT] Added " .. amount .. " to " .. key .. " (now " .. bb.values[key] .. ")")
return "success"
end)
-- =============================================================================
-- Example 5: Node that sets a blackboard bit
-- =============================================================================
-- This node sets or clears a named bit in the blackboard.
ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params)
local bit_name = params.bit
local value = params.value == "1" or params.value == "true"
if not bit_name then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
bb.bits[bit_name] = value
ecs.set_component(entity_id, "GoapBlackboard", bb)
print("[BT] Set bit '" .. bit_name .. "' to " .. tostring(value))
return "success"
end)
-- =============================================================================
-- Example 6: Node that checks a blackboard bit
-- =============================================================================
-- This node checks if a named bit is set to a specific value.
ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params)
local bit_name = params.bit
local expected = params.value ~= "0" and params.value ~= "false"
if not bit_name then
return "failure"
end
local bb = ecs.get_component(entity_id, "GoapBlackboard")
if not bb then
return "failure"
end
local actual = bb.bits[bit_name] == true
if actual == expected then
return "success"
else
return "failure"
end
end)
-- =============================================================================
-- Example 7: Random chance node
-- =============================================================================
-- This node succeeds with a configurable probability.
ecs.behavior_tree.register_node("random_chance", function(entity_id, params)
local probability = tonumber(params.probability) or 0.5
-- Simple pseudo-random using math.random
local roll = math.random()
if roll < probability then
print("[BT] Random chance succeeded (roll=" .. roll .. " < " .. probability .. ")")
return "success"
else
print("[BT] Random chance failed (roll=" .. roll .. " >= " .. probability .. ")")
return "failure"
end
end)
-- =============================================================================
-- Using the Nodes in Behavior Trees
-- =============================================================================
-- Once registered, you can use ecs.behavior_tree.create_node(name, params)
-- to create node tables for use in action behavior trees.
--
-- The params string uses "key=val,key2=val2" format:
-- - Numbers (int or float) are parsed automatically
-- - Quoted strings preserve string values: msg="hello world"
-- - Unquoted non-numeric values are treated as strings
-- =============================================================================
-- Example: Action that uses Lua behavior tree nodes
ecs.action_db.add_action("lua_demo_greet", 1,
{}, -- no preconditions
{}, -- no effects
{ -- behavior tree
type = "sequence",
children = {
ecs.behavior_tree.create_node("say_hello", "message=Welcome to the game!"),
ecs.behavior_tree.create_node("wait_for_duration", "duration=2.0,timer_key=greet_timer"),
ecs.behavior_tree.create_node("say_hello", "message=How are you today?")
}
}
)
-- Example: Action with conditional logic using Lua nodes
ecs.action_db.add_action("lua_demo_conditional", 2,
{
values = { experience = 10 }
},
{
values = { experience = 20 }
},
{
type = "selector",
children = {
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("check_blackboard_value", "key=experience,min=50"),
ecs.behavior_tree.create_node("say_hello", "message=You are experienced!"),
ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=10")
}
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("say_hello", "message=You are still learning..."),
ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=5")
}
}
}
}
)
-- Example: Action with random outcomes
ecs.action_db.add_action("lua_demo_gamble", 3,
{},
{
values = { gold = 10 }
},
{
type = "sequence",
children = {
ecs.behavior_tree.create_node("random_chance", "probability=0.3"),
ecs.behavior_tree.create_node("say_hello", "message=You won the gamble!"),
ecs.behavior_tree.create_node("add_blackboard_value", "key=gold,amount=10")
}
}
)
-- =============================================================================
-- Managing Registered Nodes
-- =============================================================================
-- List all registered node handlers:
local nodes = ecs.behavior_tree.list_nodes()
print("Registered behavior tree nodes:")
for i, name in ipairs(nodes) do
print(" " .. i .. ". " .. name)
end
-- Unregister a node handler:
-- local removed = ecs.behavior_tree.unregister_node("say_hello")
-- if removed then
-- print("Unregistered node: say_hello")
-- end
-- =============================================================================
-- Parameter Format Reference
-- =============================================================================
--
-- The params string passed to create_node() supports:
--
-- Integer: "count=42" -> params.count = 42 (number)
-- Float: "speed=3.5" -> params.speed = 3.5 (number)
-- String: "msg=hello" -> params.msg = "hello" (string)
-- Quoted: 'msg="hello world"' -> params.msg = "hello world" (string)
-- Multiple: "key=val,count=5" -> params.key = "val", params.count = 5
-- Empty: "" -> params = {} (empty table)
--
-- The params table is passed as the second argument to your registered
-- Lua function handler.
-- =============================================================================

View File

@@ -0,0 +1,348 @@
#include "LuaBehaviorTreeApi.hpp"
#include "LuaEntityApi.hpp"
#include "../components/GoapBlackboard.hpp"
#include <OgreLogManager.h>
#include <unordered_map>
#include <string>
namespace editScene
{
// ---------------------------------------------------------------------------
// Global registry of Lua node handlers
// ---------------------------------------------------------------------------
// Maps node name -> Lua registry reference for the callback function.
// The callback is stored as a Lua reference in the registry so it persists
// across Lua state resets and can be called from C++.
static std::unordered_map<std::string, int> g_luaNodeHandlers;
// Global Lua state pointer, set during registerLuaBehaviorTreeApi().
// Used by callLuaBehaviorTreeNode() which is invoked from the C++
// behavior tree system without direct access to the Lua state.
static lua_State *g_luaState = nullptr;
// ---------------------------------------------------------------------------
// Helper: push a GoapBlackboard as a Lua table (reused from LuaActionApi)
// ---------------------------------------------------------------------------
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
{
lua_newtable(L); // blackboard table
// Bits
lua_newtable(L); // bits table
for (int i = 0; i < 64; i++) {
if (bb.hasBit(i)) {
const char *name = GoapBlackboard::getBitName(i);
const char *key = name ? name : "";
lua_pushboolean(L, bb.getBit(i));
lua_setfield(L, -2, key);
}
}
lua_setfield(L, -2, "bits");
// Integer values
lua_newtable(L);
for (const auto &kv : bb.values) {
lua_pushinteger(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "values");
// Float values
lua_newtable(L);
for (const auto &kv : bb.floatValues) {
lua_pushnumber(L, kv.second);
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "floatValues");
// String values
lua_newtable(L);
for (const auto &kv : bb.stringValues) {
lua_pushstring(L, kv.second.c_str());
lua_setfield(L, -2, kv.first.c_str());
}
lua_setfield(L, -2, "stringValues");
}
// ---------------------------------------------------------------------------
// Helper: parse "key=val,key2=val2" params into a Lua table
// ---------------------------------------------------------------------------
static void pushParamsTable(lua_State *L, const std::string &params)
{
lua_newtable(L); // params table
if (params.empty())
return;
const char *s = params.c_str();
while (*s) {
// Skip whitespace
while (*s == ' ' || *s == '\t')
s++;
if (!*s)
break;
// Find key
const char *keyStart = s;
while (*s && *s != '=' && *s != ',')
s++;
std::string key(keyStart, static_cast<size_t>(s - keyStart));
// Trim trailing spaces from key
while (!key.empty() &&
(key.back() == ' ' || key.back() == '\t'))
key.pop_back();
if (*s != '=') {
while (*s && *s != ',')
s++;
if (*s == ',')
s++;
continue;
}
s++; // skip '='
// Skip whitespace before value
while (*s == ' ' || *s == '\t')
s++;
// Find value end (next comma or end)
const char *valStart = s;
bool inQuotes = false;
while (*s && (*s != ',' || inQuotes)) {
if (*s == '"')
inQuotes = !inQuotes;
s++;
}
std::string val(valStart, static_cast<size_t>(s - valStart));
// Trim trailing spaces from value
while (!val.empty() &&
(val.back() == ' ' || val.back() == '\t'))
val.pop_back();
// Strip quotes if present
if (val.size() >= 2 && val.front() == '"' &&
val.back() == '"') {
val = val.substr(1, val.size() - 2);
lua_pushstring(L, val.c_str());
lua_setfield(L, -2, key.c_str());
} else {
// Try numeric
char *end = nullptr;
long iVal = strtol(val.c_str(), &end, 10);
if (end != val.c_str() && *end == '\0') {
lua_pushinteger(L, (int)iVal);
lua_setfield(L, -2, key.c_str());
} else {
// Try float
end = nullptr;
float fVal = strtof(val.c_str(), &end);
if (end != val.c_str() && *end == '\0') {
lua_pushnumber(L, fVal);
lua_setfield(L, -2, key.c_str());
} else {
// Fallback to string
lua_pushstring(L, val.c_str());
lua_setfield(L, -2, key.c_str());
}
}
}
if (*s == ',')
s++;
}
}
// ---------------------------------------------------------------------------
// Public API: Call a registered Lua node handler
// ---------------------------------------------------------------------------
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
flecs::entity entity, const std::string &params)
{
// Use the global Lua state if none was passed
lua_State *L = static_cast<lua_State *>(L_);
if (!L)
L = g_luaState;
if (!L)
return -1;
auto it = g_luaNodeHandlers.find(nodeName);
if (it == g_luaNodeHandlers.end())
return -1;
int ref = it->second;
// Push the callback function
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
// Push entity ID as first argument
lua_pushinteger(L, luaEntityToId(entity));
// Push params table as second argument
pushParamsTable(L, params);
// Call the function
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Error calling node '" << nodeName
<< "': " << lua_tostring(L, -1);
lua_pop(L, 1);
return -1;
}
// Read the result
if (!lua_isstring(L, -1)) {
lua_pop(L, 1);
return -1;
}
const char *result = lua_tostring(L, -1);
lua_pop(L, 1);
if (strcmp(result, "success") == 0)
return 0;
if (strcmp(result, "failure") == 0)
return 1;
if (strcmp(result, "running") == 0)
return 2;
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Node '" << nodeName
<< "' returned invalid result: " << result;
return -1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.register_node(name, function)
// ---------------------------------------------------------------------------
static int luaRegisterNode(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
if (!lua_isfunction(L, 2))
luaL_error(L, "Expected function as second argument");
// Remove any existing handler with the same name
auto it = g_luaNodeHandlers.find(name);
if (it != g_luaNodeHandlers.end()) {
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
}
// Store the function reference
lua_pushvalue(L, 2);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
g_luaNodeHandlers[name] = ref;
Ogre::LogManager::getSingleton().stream()
<< "[Lua BT] Registered node handler: " << name;
return 0;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.unregister_node(name) -> bool
// ---------------------------------------------------------------------------
static int luaUnregisterNode(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
auto it = g_luaNodeHandlers.find(name);
if (it == g_luaNodeHandlers.end()) {
lua_pushboolean(L, false);
return 1;
}
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
g_luaNodeHandlers.erase(it);
lua_pushboolean(L, true);
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.list_nodes() -> table of names
// ---------------------------------------------------------------------------
static int luaListNodeHandlers(lua_State *L)
{
lua_newtable(L);
int idx = 1;
for (const auto &kv : g_luaNodeHandlers) {
lua_pushstring(L, kv.first.c_str());
lua_rawseti(L, -2, idx++);
}
return 1;
}
// ---------------------------------------------------------------------------
// Lua: ecs.behavior_tree.create_node(name, params) -> table
// ---------------------------------------------------------------------------
static int luaCreateNode(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
const char *params = luaL_optstring(L, 2, "");
lua_newtable(L);
lua_pushstring(L, "luaTask");
lua_setfield(L, -2, "type");
lua_pushstring(L, name);
lua_setfield(L, -2, "name");
if (params && params[0]) {
lua_pushstring(L, params);
lua_setfield(L, -2, "params");
}
return 1;
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
void registerLuaBehaviorTreeApi(lua_State *L)
{
// Store the Lua state globally so callLuaBehaviorTreeNode can use it
g_luaState = L;
// Get or create the "ecs" global table
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
// Create the behavior_tree sub-table
lua_newtable(L);
lua_pushcfunction(L, luaRegisterNode);
lua_setfield(L, -2, "register_node");
lua_pushcfunction(L, luaUnregisterNode);
lua_setfield(L, -2, "unregister_node");
lua_pushcfunction(L, luaListNodeHandlers);
lua_setfield(L, -2, "list_nodes");
lua_pushcfunction(L, luaCreateNode);
lua_setfield(L, -2, "create_node");
// Set behavior_tree as a field of ecs
lua_setfield(L, -2, "behavior_tree");
// Ensure ecs is global
lua_setglobal(L, "ecs");
}
} // namespace editScene

View File

@@ -0,0 +1,83 @@
#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
#pragma once
#include <lua.hpp>
/**
* @file LuaBehaviorTreeApi.hpp
* @brief Lua API for creating behavior tree nodes that run Lua functions.
*
* Provides Lua bindings to register named Lua functions as behavior tree
* node handlers, and to create behavior tree nodes that invoke those
* functions during tree evaluation.
*
* Exposed Lua globals (under the "ecs" table):
* ecs.behavior_tree.register_node(name, function) -> nil
* Register a Lua function as a behavior tree node handler.
* The function receives (entity_id, params_table) and must return
* "success", "failure", or "running".
*
* ecs.behavior_tree.unregister_node(name) -> bool
* Remove a previously registered node handler.
*
* ecs.behavior_tree.list_nodes() -> table of registered node names
* Returns an array of all registered node handler names.
*
* ecs.behavior_tree.create_node(name, params) -> table
* Creates a behavior tree node table suitable for use in
* ecs.action_db.add_action() behavior trees.
* Returns { type = "luaTask", name = name, params = params }
*
* Example:
* -- Register a node handler
* ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
* print("Hello from entity " .. entity_id .. "! " .. (params.message or ""))
* return "success"
* end)
*
* -- Use it in an action's behavior tree
* ecs.action_db.add_action("greet", 1, {}, {}, {
* type = "sequence",
* children = {
* ecs.behavior_tree.create_node("say_hello", "message=Hello World")
* }
* })
*
* -- A node that runs for a while:
* ecs.behavior_tree.register_node("wait_random", function(entity_id, params)
* -- Use the blackboard to store state across frames
* local bb = ecs.get_component(entity_id, "GoapBlackboard")
* if not bb then return "failure" end
*
* local key = "wait_timer_" .. params.timer_key
* local dt = ecs.get_delta_time()
* bb.floatValues[key] = (bb.floatValues[key] or 0) + dt
*
* local duration = tonumber(params.duration) or 1.0
* if bb.floatValues[key] >= duration then
* bb.floatValues[key] = nil
* ecs.set_component(entity_id, "GoapBlackboard", bb)
* return "success"
* end
* ecs.set_component(entity_id, "GoapBlackboard", bb)
* return "running"
* end)
*/
namespace editScene
{
/**
* @brief Register all behavior tree Lua API functions into the "ecs" table.
*
* Adds a behavior_tree sub-table to the "ecs" global table with functions
* for registering Lua node handlers and creating behavior tree nodes.
*
* @param L The Lua state.
*/
void registerLuaBehaviorTreeApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP

View File

@@ -18,6 +18,20 @@
#include <cstdlib>
#include <cstring>
/* Forward declaration for Lua behavior tree node support.
* The actual implementation is in lua/LuaBehaviorTreeApi.cpp.
* Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error.
* We use a function pointer approach to avoid a hard dependency on
* the Lua headers in this translation unit.
*
* The lua_State pointer is passed as void* to avoid needing the Lua
* headers in this translation unit. */
namespace editScene
{
int callLuaBehaviorTreeNode(void *L, const std::string &nodeName,
flecs::entity entity, const std::string &params);
}
static float g_epsilon = 0.0001f;
static bool parseValueString(const Ogre::String &str, int &outInt,
@@ -973,6 +987,23 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
return Status::success;
}
/* --- Lua behavior tree node --- */
if (node.type == "luaTask") {
/* Call the Lua function via the forward-declared API.
* Returns: 0 = success, 1 = failure, 2 = running, -1 = error/not found */
int result = editScene::callLuaBehaviorTreeNode(
nullptr, node.name, e, node.params);
if (result == 0) return Status::success;
if (result == 1)
return Status::failure;
if (result == 2)
return Status::running;
/* -1 or other: log error and return failure */
std::cout << "[BT] luaTask: node '" << node.name
<< "' not registered or error" << std::endl;
return Status::failure;
}
return Status::success;
}

View File

@@ -326,6 +326,12 @@ void EditorUISystem::update(float deltaTime)
renderPrefabBrowser();
renderCursorPanel();
// Render Action Database singleton editor window
if (m_showActionDatabaseSingleton) {
m_actionDatabaseSingletonEditor.render(
&m_showActionDatabaseSingleton);
}
// Render FPS overlay
renderFPSOverlay(deltaTime);
}
@@ -409,6 +415,11 @@ void EditorUISystem::renderHierarchyWindow()
if (ImGui::MenuItem("3D Cursor")) {
m_showCursorPanel = true;
}
ImGui::Separator();
if (ImGui::MenuItem(
"Action Database (Singleton)")) {
m_showActionDatabaseSingleton = true;
}
ImGui::EndMenu();
}
@@ -627,9 +638,11 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
indicators += " [Mat]";
if (entity.has<AnimationTreeComponent>())
indicators += " [Anim]";
// ActionDatabase is now a singleton, not a per-entity component
if (entity.has<ActionDatabaseComponent>())
indicators += " [ActDB]";
if (entity.has<ActionDebug>())
indicators += " [Debug]";
if (entity.has<BehaviorTreeComponent>())
indicators += " [BT]";
if (entity.has<GoapBlackboard>())
@@ -1019,9 +1032,15 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// ActionDatabase is now a singleton, not a per-entity component
// Render ActionDatabaseComponent if present
if (entity.has<ActionDatabaseComponent>()) {
auto &db = entity.get_mut<ActionDatabaseComponent>();
m_componentRegistry.render<ActionDatabaseComponent>(entity, db);
componentCount++;
}
// Render ActionDebug if present
if (entity.has<ActionDebug>()) {
auto &debug = entity.get_mut<ActionDebug>();
m_componentRegistry.render<ActionDebug>(entity, debug);

View File

@@ -7,6 +7,7 @@
#include <string>
#include <vector>
#include "../ui/ComponentRegistry.hpp"
#include "../ui/ActionDatabaseSingletonEditor.hpp"
#include "../components/EntityName.hpp"
#include "../gizmo/Gizmo.hpp"
#include "../gizmo/Cursor3D.hpp"
@@ -297,6 +298,10 @@ private:
// Camera reference for cursor placement/rotation
EditorCamera *m_editorCamera = nullptr;
// Action Database singleton editor state
bool m_showActionDatabaseSingleton = false;
ActionDatabaseSingletonEditor m_actionDatabaseSingletonEditor;
// Queries
flecs::query<EntityNameComponent> m_nameQuery;

View File

@@ -17,6 +17,7 @@
#include <cstdint>
#include <cstdio>
#include <sstream>
#include <cassert>
namespace Ogre
{
@@ -119,6 +120,17 @@ public:
}
};
// OgreAssert macro (used by LuaEntityApi.hpp)
#ifndef OgreAssert
#define OgreAssert(expr, msg) \
do { \
if (!(expr)) { \
fprintf(stderr, "OgreAssert failed: %s\n", msg); \
assert(expr); \
} \
} while (0)
#endif
} // namespace Ogre
#endif // OGRE_STUB_H

View File

@@ -0,0 +1,492 @@
/**
* @file behavior_tree_lua_test.cpp
* @brief Standalone test for Behavior Tree Lua API.
*
* This test creates a Lua state, registers the behavior tree Lua API,
* then runs Lua scripts that register node handlers, create nodes,
* list/unregister handlers, and verify the results.
*
* Build with:
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
* behavior_tree_lua_test.cpp \
* ../lua/LuaBehaviorTreeApi.cpp \
* ../../lua/lua-5.4.8/src/liblua.a \
* -o behavior_tree_lua_test -lm
*
* Or via CMake (see CMakeLists.txt in this directory).
*/
#include <cstdio>
#include <cstring>
#include <cassert>
#include <string>
#include <vector>
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
// Must be included before any component headers that use Ogre types.
#include "ogre_stub.h"
// Include Lua
extern "C" {
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
}
// Include the components we need
#include "../components/BehaviorTree.hpp"
// Forward declare the registration function
namespace editScene
{
void registerLuaBehaviorTreeApi(lua_State *L);
}
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
static int testCount = 0;
static int passCount = 0;
#define TEST(name) \
do { \
testCount++; \
printf(" TEST %d: %s ... ", testCount, name); \
} while (0)
#define PASS() \
do { \
passCount++; \
printf("PASS\n"); \
} while (0)
#define FAIL(msg) \
do { \
printf("FAIL: %s\n", msg); \
return 1; \
} while (0)
// ---------------------------------------------------------------------------
// Helper: run a Lua string and check for errors
// ---------------------------------------------------------------------------
static bool runLua(lua_State *L, const char *code)
{
if (luaL_dostring(L, code) != LUA_OK) {
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return false;
}
return true;
}
// ---------------------------------------------------------------------------
// Helper: get a global integer from Lua
// ---------------------------------------------------------------------------
static int getGlobalInt(lua_State *L, const char *name)
{
lua_getglobal(L, name);
int val = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
return val;
}
// ---------------------------------------------------------------------------
// Helper: get a global string from Lua
// ---------------------------------------------------------------------------
static std::string getGlobalString(lua_State *L, const char *name)
{
lua_getglobal(L, name);
const char *s = lua_tostring(L, -1);
std::string val = s ? s : "";
lua_pop(L, 1);
return val;
}
// ---------------------------------------------------------------------------
// Test 1: Register a node handler and verify it's stored
// ---------------------------------------------------------------------------
static int testRegisterNode(lua_State *L)
{
TEST("register a node handler");
bool ok = runLua(L, "ecs.behavior_tree.register_node('test_node', "
" function(entity_id, params) "
" return 'success' "
" end"
")");
if (!ok)
FAIL("failed to run Lua");
// Verify via list_nodes
ok = runLua(L, "local nodes = ecs.behavior_tree.list_nodes()\n"
"found = false\n"
"for i, name in ipairs(nodes) do\n"
" if name == 'test_node' then found = true end\n"
"end\n"
"test_result = found\n");
if (!ok)
FAIL("failed to list nodes");
lua_getglobal(L, "test_result");
if (!lua_toboolean(L, -1))
FAIL("test_node not found in list");
lua_pop(L, 1);
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 2: Register multiple nodes and list them
// ---------------------------------------------------------------------------
static int testListNodes(lua_State *L)
{
TEST("list multiple registered nodes");
bool ok = runLua(L, "ecs.behavior_tree.register_node('node_a', "
" function(e, p) return 'success' end)\n"
"ecs.behavior_tree.register_node('node_b', "
" function(e, p) return 'failure' end)\n"
"ecs.behavior_tree.register_node('node_c', "
" function(e, p) return 'running' end)\n"
"local nodes = ecs.behavior_tree.list_nodes()\n"
"count = #nodes\n");
if (!ok)
FAIL("failed to register/list nodes");
int count = getGlobalInt(L, "count");
if (count < 3)
FAIL("expected at least 3 nodes");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 3: Unregister a node handler
// ---------------------------------------------------------------------------
static int testUnregisterNode(lua_State *L)
{
TEST("unregister a node handler");
bool ok = runLua(
L,
"local removed = ecs.behavior_tree.unregister_node('node_a')\n"
"unreg_result = removed\n"
"local nodes = ecs.behavior_tree.list_nodes()\n"
"found_a = false\n"
"for i, name in ipairs(nodes) do\n"
" if name == 'node_a' then found_a = true end\n"
"end\n");
if (!ok)
FAIL("failed to unregister node");
lua_getglobal(L, "unreg_result");
if (!lua_toboolean(L, -1))
FAIL("unregister_node returned false");
lua_pop(L, 1);
lua_getglobal(L, "found_a");
if (lua_toboolean(L, -1))
FAIL("node_a still found after unregister");
lua_pop(L, 1);
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 4: Unregister non-existent node returns false
// ---------------------------------------------------------------------------
static int testUnregisterNonExistent(lua_State *L)
{
TEST("unregister non-existent node returns false");
bool ok = runLua(
L,
"local removed = ecs.behavior_tree.unregister_node('nonexistent')\n"
"unreg_none = removed\n");
if (!ok)
FAIL("failed to run Lua");
lua_getglobal(L, "unreg_none");
if (lua_toboolean(L, -1))
FAIL("unregister of non-existent should return false");
lua_pop(L, 1);
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 5: Create a behavior tree node via create_node
// ---------------------------------------------------------------------------
static int testCreateNode(lua_State *L)
{
TEST("create_node returns correct node table");
bool ok = runLua(
L, "local node = ecs.behavior_tree.create_node('test_node', "
" 'key=val,count=42')\n"
"node_type = node.type\n"
"node_name = node.name\n"
"node_params = node.params\n");
if (!ok)
FAIL("failed to create node");
std::string type = getGlobalString(L, "node_type");
std::string name = getGlobalString(L, "node_name");
std::string params = getGlobalString(L, "node_params");
if (type != "luaTask")
FAIL("expected type 'luaTask', got '" + type + "'");
if (name != "test_node")
FAIL("expected name 'test_node', got '" + name + "'");
if (params != "key=val,count=42")
FAIL("expected params 'key=val,count=42', got '" + params +
"'");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 6: Create node without params (optional)
// ---------------------------------------------------------------------------
static int testCreateNodeNoParams(lua_State *L)
{
TEST("create_node without params");
bool ok = runLua(
L, "local node = ecs.behavior_tree.create_node('simple_node')\n"
"no_params_type = node.type\n"
"no_params_name = node.name\n"
"no_params_has_params = (node.params ~= nil)\n");
if (!ok)
FAIL("failed to create node without params");
std::string type = getGlobalString(L, "no_params_type");
std::string name = getGlobalString(L, "no_params_name");
if (type != "luaTask")
FAIL("expected type 'luaTask'");
if (name != "simple_node")
FAIL("expected name 'simple_node'");
// params should be nil or empty when not provided
lua_getglobal(L, "no_params_has_params");
if (lua_toboolean(L, -1)) {
// It's okay if params is set to empty string
lua_getglobal(L, "node_params");
const char *p = lua_tostring(L, -1);
if (p && p[0] != '\0') {
FAIL("expected no params, got '" + std::string(p) +
"'");
}
lua_pop(L, 1);
}
lua_pop(L, 1);
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 7: Create node and use in a behavior tree structure
// ---------------------------------------------------------------------------
static int testCreateNodeInTree(lua_State *L)
{
TEST("create_node used in behavior tree structure");
bool ok = runLua(
L, "local tree = {\n"
" type = 'sequence',\n"
" children = {\n"
" ecs.behavior_tree.create_node('say_hello', "
" 'message=Hello'),\n"
" ecs.behavior_tree.create_node('wait_for_duration', "
" 'duration=2.0,timer_key=test'),\n"
" ecs.behavior_tree.create_node('say_hello', "
" 'message=Done')\n"
" }\n"
"}\n"
"tree_type = tree.type\n"
"child_count = #tree.children\n"
"child0_type = tree.children[1].type\n"
"child0_name = tree.children[1].name\n"
"child0_params = tree.children[1].params\n"
"child1_type = tree.children[2].type\n"
"child1_name = tree.children[2].name\n"
"child1_params = tree.children[2].params\n");
if (!ok)
FAIL("failed to create tree with Lua nodes");
std::string treeType = getGlobalString(L, "tree_type");
int childCount = getGlobalInt(L, "child_count");
if (treeType != "sequence")
FAIL("expected tree type 'sequence'");
if (childCount != 3)
FAIL("expected 3 children");
std::string c0type = getGlobalString(L, "child0_type");
std::string c0name = getGlobalString(L, "child0_name");
std::string c0params = getGlobalString(L, "child0_params");
if (c0type != "luaTask")
FAIL("child 0: expected type 'luaTask'");
if (c0name != "say_hello")
FAIL("child 0: expected name 'say_hello'");
if (c0params != "message=Hello")
FAIL("child 0: expected params 'message=Hello'");
std::string c1type = getGlobalString(L, "child1_type");
std::string c1name = getGlobalString(L, "child1_name");
std::string c1params = getGlobalString(L, "child1_params");
if (c1type != "luaTask")
FAIL("child 1: expected type 'luaTask'");
if (c1name != "wait_for_duration")
FAIL("child 1: expected name 'wait_for_duration'");
if (c1params != "duration=2.0,timer_key=test")
FAIL("child 1: unexpected params");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 8: Re-register a node (replace existing handler)
// ---------------------------------------------------------------------------
static int testReregisterNode(lua_State *L)
{
TEST("re-register a node handler (replace)");
bool ok = runLua(
L,
"ecs.behavior_tree.register_node('replaceable', "
" function(e, p) return 'failure' end)\n"
"ecs.behavior_tree.register_node('replaceable', "
" function(e, p) return 'success' end)\n"
"local nodes = ecs.behavior_tree.list_nodes()\n"
"found_count = 0\n"
"for i, name in ipairs(nodes) do\n"
" if name == 'replaceable' then found_count = found_count + 1 end\n"
"end\n");
if (!ok)
FAIL("failed to re-register node");
int count = getGlobalInt(L, "found_count");
if (count != 1)
FAIL("expected exactly 1 'replaceable' after re-register, got " +
std::to_string(count));
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 9: Empty list when no nodes registered
// ---------------------------------------------------------------------------
static int testEmptyList(lua_State *L)
{
TEST("list_nodes returns empty when no nodes");
// Unregister all nodes we created
bool ok = runLua(L, "ecs.behavior_tree.unregister_node('test_node')\n"
"ecs.behavior_tree.unregister_node('node_b')\n"
"ecs.behavior_tree.unregister_node('node_c')\n"
"ecs.behavior_tree.unregister_node('replaceable')\n"
"local nodes = ecs.behavior_tree.list_nodes()\n"
"empty_count = #nodes\n");
if (!ok)
FAIL("failed to unregister all nodes");
int count = getGlobalInt(L, "empty_count");
if (count != 0)
FAIL("expected empty list, got " + std::to_string(count) +
" nodes");
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Test 10: Register node with invalid arguments
// ---------------------------------------------------------------------------
static int testRegisterInvalidArgs(lua_State *L)
{
TEST("register_node with invalid args (no function)");
// This should error - we just verify it doesn't crash
bool ok = runLua(L, "local ok, err = pcall(function()\n"
" ecs.behavior_tree.register_node('bad_node')\n"
"end)\n"
"pcall_ok = ok\n");
if (!ok)
FAIL("failed to run pcall");
lua_getglobal(L, "pcall_ok");
if (lua_toboolean(L, -1))
FAIL("expected register_node without function to error");
lua_pop(L, 1);
PASS();
return 0;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main()
{
printf("Behavior Tree Lua API Tests\n");
printf("============================\n\n");
// Create Lua state
lua_State *L = luaL_newstate();
if (!L) {
fprintf(stderr, "Failed to create Lua state\n");
return 1;
}
luaL_openlibs(L);
// Register the behavior tree API
editScene::registerLuaBehaviorTreeApi(L);
// Run tests
int failures = 0;
failures += testRegisterNode(L);
failures += testListNodes(L);
failures += testUnregisterNode(L);
failures += testUnregisterNonExistent(L);
failures += testCreateNode(L);
failures += testCreateNodeNoParams(L);
failures += testCreateNodeInTree(L);
failures += testReregisterNode(L);
failures += testEmptyList(L);
failures += testRegisterInvalidArgs(L);
// Cleanup
lua_close(L);
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
failures);
return failures > 0 ? 1 : 0;
}

View File

@@ -0,0 +1,236 @@
#include "ActionDatabaseSingletonEditor.hpp"
#include "GoapBlackboardEditor.hpp"
#include "InlineBehaviorTreeEditor.hpp"
#include <imgui.h>
void ActionDatabaseSingletonEditor::render(bool *open)
{
if (!ImGui::Begin("Action Database (Singleton)", open)) {
ImGui::End();
return;
}
if (ImGui::CollapsingHeader("Bit Names",
ImGuiTreeNodeFlags_DefaultOpen))
renderBitNames();
if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen))
renderActions();
if (ImGui::CollapsingHeader("Goals", ImGuiTreeNodeFlags_DefaultOpen))
renderGoals();
ImGui::End();
}
void ActionDatabaseSingletonEditor::renderBitNames()
{
bool changed = false;
ImGui::Indent();
ImGui::Text("Name bits for use in preconditions/effects:");
ImGui::TextDisabled("(empty slots show as numbers in blackboards)");
for (int i = 0; i < 64; i++) {
const char *name = GoapBlackboard::getBitName(i);
char buf[64];
snprintf(buf, sizeof(buf), "%s", name ? name : "");
ImGui::PushID(i);
char label[16];
snprintf(label, sizeof(label), "%2d", i);
ImGui::SetNextItemWidth(-FLT_MIN);
if (ImGui::InputText(label, buf, sizeof(buf))) {
GoapBlackboard::setBitName(i, buf);
changed = true;
}
ImGui::PopID();
}
ImGui::Unindent();
(void)changed;
}
void ActionDatabaseSingletonEditor::renderActions()
{
ActionDatabase &db = ActionDatabase::getSingleton();
ImGui::Indent();
for (size_t i = 0; i < db.actions.size(); i++) {
ImGui::PushID((int)i);
bool isSelected = (m_selectedAction == (int)i);
char label[256];
snprintf(label, sizeof(label), "%s (cost: %d)",
db.actions[i].name.c_str(), db.actions[i].cost);
if (ImGui::Selectable(label, isSelected))
m_selectedAction = isSelected ? -1 : (int)i;
ImGui::SameLine();
if (ImGui::SmallButton("X")) {
db.actions.erase(db.actions.begin() + i);
if (m_selectedAction == (int)i)
m_selectedAction = -1;
else if (m_selectedAction > (int)i)
m_selectedAction--;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::Button("Add Action")) {
char newName[64];
snprintf(newName, sizeof(newName), "Action_%zu",
db.actions.size());
db.actions.emplace_back(newName);
m_selectedAction = (int)db.actions.size() - 1;
}
if (m_selectedAction >= 0 &&
m_selectedAction < (int)db.actions.size()) {
renderActionEditor(db.actions[m_selectedAction]);
}
ImGui::Unindent();
}
void ActionDatabaseSingletonEditor::renderGoals()
{
ActionDatabase &db = ActionDatabase::getSingleton();
ImGui::Indent();
for (size_t i = 0; i < db.goals.size(); i++) {
ImGui::PushID((int)i + 10000);
bool isSelected = (m_selectedGoal == (int)i);
char label[256];
snprintf(label, sizeof(label), "%s (priority: %d)",
db.goals[i].name.c_str(), db.goals[i].priority);
if (ImGui::Selectable(label, isSelected))
m_selectedGoal = isSelected ? -1 : (int)i;
ImGui::SameLine();
if (ImGui::SmallButton("X")) {
db.goals.erase(db.goals.begin() + i);
if (m_selectedGoal == (int)i)
m_selectedGoal = -1;
else if (m_selectedGoal > (int)i)
m_selectedGoal--;
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (ImGui::Button("Add Goal")) {
char newName[64];
snprintf(newName, sizeof(newName), "Goal_%zu", db.goals.size());
db.goals.emplace_back(newName);
m_selectedGoal = (int)db.goals.size() - 1;
}
if (m_selectedGoal >= 0 && m_selectedGoal < (int)db.goals.size()) {
renderGoalEditor(db.goals[m_selectedGoal]);
}
ImGui::Unindent();
}
void ActionDatabaseSingletonEditor::renderActionEditor(GoapAction &action)
{
ImGui::Separator();
ImGui::Text("Action: %s", action.name.c_str());
char nameBuf[256];
snprintf(nameBuf, sizeof(nameBuf), "%s", action.name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
action.name = nameBuf;
if (ImGui::InputInt("Cost", &action.cost))
action.cost = action.cost < 0 ? 0 : action.cost;
char btNameBuf[256];
snprintf(btNameBuf, sizeof(btNameBuf), "%s",
action.behaviorTreeName.c_str());
if (ImGui::InputText("Behavior Tree Name", btNameBuf,
sizeof(btNameBuf)))
action.behaviorTreeName = btNameBuf;
if (ImGui::TreeNode("Behavior Tree")) {
if (InlineBehaviorTreeEditor::render(action.behaviorTree))
/* modified */;
ImGui::TreePop();
}
if (ImGui::TreeNode("Preconditions")) {
GoapBlackboardEditor::render(action.preconditions, "precond");
ImGui::TreePop();
}
// Precondition mask editor
ImGui::Separator();
ImGui::Text("Precondition Mask (bits to check)");
for (int i = 0; i < 64; i += 8) {
for (int j = 0; j < 8 && (i + j) < 64; j++) {
int bit = i + j;
bool enabled = (action.preconditionMask >> bit) & 1ULL;
char label[8];
snprintf(label, sizeof(label), "%d", bit);
if (j > 0)
ImGui::SameLine();
if (ImGui::Checkbox(label, &enabled)) {
if (enabled)
action.preconditionMask |=
(1ULL << bit);
else
action.preconditionMask &=
~(1ULL << bit);
}
}
}
ImGui::Text("Mask: 0x%016llX",
(unsigned long long)action.preconditionMask);
ImGui::SameLine();
if (ImGui::SmallButton("Set All##mask")) {
action.preconditionMask = ~0ULL;
}
ImGui::SameLine();
if (ImGui::SmallButton("Clear All##mask")) {
action.preconditionMask = 0ULL;
}
if (ImGui::TreeNode("Effects")) {
GoapBlackboardEditor::render(action.effects, "effects");
ImGui::TreePop();
}
}
void ActionDatabaseSingletonEditor::renderGoalEditor(GoapGoal &goal)
{
ImGui::Separator();
ImGui::Text("Goal: %s", goal.name.c_str());
char nameBuf[256];
snprintf(nameBuf, sizeof(nameBuf), "%s", goal.name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
goal.name = nameBuf;
if (ImGui::InputInt("Priority", &goal.priority))
goal.priority = goal.priority < 0 ? 0 : goal.priority;
char condBuf[512];
snprintf(condBuf, sizeof(condBuf), "%s", goal.condition.c_str());
if (ImGui::InputText("Condition", condBuf, sizeof(condBuf)))
goal.condition = condBuf;
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Optional expression: health > 20 && hunger < 50");
}
if (ImGui::TreeNode("Target Blackboard")) {
GoapBlackboardEditor::render(goal.target, "target");
ImGui::TreePop();
}
}

View File

@@ -0,0 +1,38 @@
#ifndef EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
#define EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
#pragma once
#include "../components/ActionDatabase.hpp"
/**
* Editor for the ActionDatabase singleton.
*
* Unlike ActionDatabaseEditor (which edits the ActionDatabaseComponent on a
* scene entity), this editor works directly on the global ActionDatabase
* singleton. It is opened from the editor menu and renders in its own
* ImGui window.
*/
class ActionDatabaseSingletonEditor {
public:
ActionDatabaseSingletonEditor() = default;
/**
* Render the singleton editor window.
* Call this inside an ImGui frame.
*
* @param open Pointer to bool controlling window visibility.
*/
void render(bool *open);
private:
void renderBitNames();
void renderActions();
void renderGoals();
void renderActionEditor(GoapAction &action);
void renderGoalEditor(GoapGoal &goal);
int m_selectedAction = -1;
int m_selectedGoal = -1;
};
#endif // EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP