From 976ced37315209faa07d2a9602ce057b05ca3731 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 1 May 2026 00:31:06 +0300 Subject: [PATCH] Lua-based behavior tree node --- src/features/editScene/CMakeLists.txt | 24 + src/features/editScene/EditorApp.cpp | 2 + .../editScene/components/BehaviorTree.hpp | 12 +- .../lua-examples/behavior_tree_example.lua | 316 +++++++++++ .../editScene/lua/LuaBehaviorTreeApi.cpp | 348 +++++++++++++ .../editScene/lua/LuaBehaviorTreeApi.hpp | 83 +++ .../editScene/systems/BehaviorTreeSystem.cpp | 31 ++ .../editScene/systems/EditorUISystem.cpp | 23 +- .../editScene/systems/EditorUISystem.hpp | 5 + src/features/editScene/tests/Ogre.h | 12 + .../tests/behavior_tree_lua_test.cpp | 492 ++++++++++++++++++ .../ui/ActionDatabaseSingletonEditor.cpp | 236 +++++++++ .../ui/ActionDatabaseSingletonEditor.hpp | 38 ++ 13 files changed, 1619 insertions(+), 3 deletions(-) create mode 100644 src/features/editScene/lua-examples/behavior_tree_example.lua create mode 100644 src/features/editScene/lua/LuaBehaviorTreeApi.cpp create mode 100644 src/features/editScene/lua/LuaBehaviorTreeApi.hpp create mode 100644 src/features/editScene/tests/behavior_tree_lua_test.cpp create mode 100644 src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp create mode 100644 src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index bff77ec..f1e87bf 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -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") diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 8035ada..c942fbe 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -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(); diff --git a/src/features/editScene/components/BehaviorTree.hpp b/src/features/editScene/components/BehaviorTree.hpp index 020d812..9f047de 100644 --- a/src/features/editScene/components/BehaviorTree.hpp +++ b/src/features/editScene/components/BehaviorTree.hpp @@ -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"; } }; diff --git a/src/features/editScene/lua-examples/behavior_tree_example.lua b/src/features/editScene/lua-examples/behavior_tree_example.lua new file mode 100644 index 0000000..1b69f1b --- /dev/null +++ b/src/features/editScene/lua-examples/behavior_tree_example.lua @@ -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. +-- ============================================================================= diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.cpp b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp new file mode 100644 index 0000000..144ea64 --- /dev/null +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp @@ -0,0 +1,348 @@ +#include "LuaBehaviorTreeApi.hpp" +#include "LuaEntityApi.hpp" +#include "../components/GoapBlackboard.hpp" +#include +#include +#include + +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 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 ¶ms) +{ + 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(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(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 ¶ms) +{ + // Use the global Lua state if none was passed + lua_State *L = static_cast(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 diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.hpp b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp new file mode 100644 index 0000000..8153224 --- /dev/null +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp @@ -0,0 +1,83 @@ +#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP +#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP +#pragma once + +#include + +/** + * @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 diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 0d0f9b7..409a1ee 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -18,6 +18,20 @@ #include #include +/* 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 ¶ms); +} + 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; } diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 5eead5a..35b59a1 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -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()) indicators += " [Anim]"; - // ActionDatabase is now a singleton, not a per-entity component + if (entity.has()) + indicators += " [ActDB]"; if (entity.has()) indicators += " [Debug]"; + if (entity.has()) indicators += " [BT]"; if (entity.has()) @@ -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()) { + auto &db = entity.get_mut(); + m_componentRegistry.render(entity, db); + componentCount++; + } // Render ActionDebug if present + if (entity.has()) { auto &debug = entity.get_mut(); m_componentRegistry.render(entity, debug); diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index eb94a1a..386e5bc 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -7,6 +7,7 @@ #include #include #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 m_nameQuery; diff --git a/src/features/editScene/tests/Ogre.h b/src/features/editScene/tests/Ogre.h index 48d2376..1644ef6 100644 --- a/src/features/editScene/tests/Ogre.h +++ b/src/features/editScene/tests/Ogre.h @@ -17,6 +17,7 @@ #include #include #include +#include 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 diff --git a/src/features/editScene/tests/behavior_tree_lua_test.cpp b/src/features/editScene/tests/behavior_tree_lua_test.cpp new file mode 100644 index 0000000..8fe8691 --- /dev/null +++ b/src/features/editScene/tests/behavior_tree_lua_test.cpp @@ -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 +#include +#include +#include +#include + +// 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 +#include +#include +} + +// 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; +} diff --git a/src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp b/src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp new file mode 100644 index 0000000..5ddfcf2 --- /dev/null +++ b/src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp @@ -0,0 +1,236 @@ +#include "ActionDatabaseSingletonEditor.hpp" +#include "GoapBlackboardEditor.hpp" +#include "InlineBehaviorTreeEditor.hpp" +#include + +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(); + } +} diff --git a/src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp b/src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp new file mode 100644 index 0000000..2a5e320 --- /dev/null +++ b/src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp @@ -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