From 0ed83966dac7619e94462095e199eee6890b641d Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Thu, 30 Apr 2026 10:03:56 +0300 Subject: [PATCH] Lua action APIs --- src/features/editScene/CMakeLists.txt | 27 + src/features/editScene/EditorApp.cpp | 4 +- .../editScene/components/ActionDatabase.cpp | 85 +- .../editScene/components/ActionDatabase.hpp | 45 +- .../components/ActionDatabaseModule.cpp | 15 +- .../lua-examples/action_db_example.lua | 418 ++++++++ src/features/editScene/lua/LuaActionApi.cpp | 576 +++++++++++ src/features/editScene/lua/LuaActionApi.hpp | 101 ++ .../editScene/lua/LuaComponentApi.cpp | 10 +- .../editScene/systems/ActuatorSystem.cpp | 9 +- .../editScene/systems/BehaviorTreeSystem.cpp | 7 +- .../editScene/systems/EditorUISystem.cpp | 10 +- .../editScene/systems/EventHandlerSystem.cpp | 9 +- .../editScene/systems/GoapPlannerSystem.cpp | 9 +- .../editScene/systems/GoapRunnerSystem.cpp | 19 +- src/features/editScene/systems/ItemSystem.cpp | 9 +- .../editScene/systems/SceneSerializer.cpp | 40 +- .../editScene/systems/SceneSerializer.hpp | 5 +- .../editScene/systems/SmartObjectSystem.cpp | 18 +- src/features/editScene/tests/Ogre.h | 91 ++ src/features/editScene/tests/OgreLogManager.h | 14 + .../editScene/tests/action_db_lua_test.cpp | 895 ++++++++++++++++++ src/features/editScene/tests/ogre_stub.h | 77 ++ .../editScene/ui/ActionDatabaseEditor.cpp | 35 +- .../editScene/ui/ActionDatabaseEditor.hpp | 16 +- .../editScene/ui/ActionDebugEditor.cpp | 10 +- src/features/editScene/ui/ActuatorEditor.cpp | 10 +- .../editScene/ui/EventHandlerEditor.cpp | 10 +- .../editScene/ui/GoapPlannerEditor.cpp | 32 +- src/features/editScene/ui/ItemEditor.cpp | 10 +- .../editScene/ui/SmartObjectEditor.cpp | 10 +- 31 files changed, 2417 insertions(+), 209 deletions(-) create mode 100644 src/features/editScene/lua-examples/action_db_example.lua create mode 100644 src/features/editScene/lua/LuaActionApi.cpp create mode 100644 src/features/editScene/lua/LuaActionApi.hpp create mode 100644 src/features/editScene/tests/Ogre.h create mode 100644 src/features/editScene/tests/OgreLogManager.h create mode 100644 src/features/editScene/tests/action_db_lua_test.cpp create mode 100644 src/features/editScene/tests/ogre_stub.h diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 231c16f..0b26c33 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -151,6 +151,7 @@ set(EDITSCENE_SOURCES lua/LuaEntityApi.cpp lua/LuaComponentApi.cpp lua/LuaEventApi.cpp + lua/LuaActionApi.cpp ) set(EDITSCENE_HEADERS @@ -299,6 +300,7 @@ set(EDITSCENE_HEADERS lua/LuaEntityApi.hpp lua/LuaComponentApi.hpp lua/LuaEventApi.hpp + lua/LuaActionApi.hpp ) add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS}) @@ -334,6 +336,31 @@ target_include_directories(editSceneEditor PRIVATE ${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0 ) +# --------------------------------------------------------------------------- +# Test: ActionDatabase Lua API +# --------------------------------------------------------------------------- +# Standalone test that verifies the ActionDatabase singleton and Lua API +# work correctly. Does not require OGRE or Flecs - only Lua and the +# core component types. +add_executable(action_db_lua_test + tests/action_db_lua_test.cpp + components/ActionDatabase.cpp + components/GoapBlackboard.cpp + components/GoapGoal.cpp + components/GoapExpression.cpp + lua/LuaActionApi.cpp +) + +target_link_libraries(action_db_lua_test + lua +) + +target_include_directories(action_db_lua_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/tests + ${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src +) + # Copy local resources (materials, etc.) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources") file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources" diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 74e7202..30fd3d1 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -87,6 +87,7 @@ #include "lua/LuaEntityApi.hpp" #include "lua/LuaComponentApi.hpp" #include "lua/LuaEventApi.hpp" +#include "lua/LuaActionApi.hpp" //============================================================================= // ImGuiRenderListener Implementation @@ -504,6 +505,7 @@ void EditorApp::setup() editScene::registerLuaEntityApi(L); editScene::registerLuaComponentApi(L); editScene::registerLuaEventApi(L); + editScene::registerLuaActionApi(L); // Run late setup: load data.lua and initial scripts. m_lua.lateSetup(); @@ -669,7 +671,7 @@ void EditorApp::setupECS() m_world.component(); // Register AI/GOAP components - m_world.component(); + // ActionDatabase is now a singleton, registered in ActionDatabaseModule m_world.component(); m_world.component(); m_world.component(); diff --git a/src/features/editScene/components/ActionDatabase.cpp b/src/features/editScene/components/ActionDatabase.cpp index a837e7f..de98b6c 100644 --- a/src/features/editScene/components/ActionDatabase.cpp +++ b/src/features/editScene/components/ActionDatabase.cpp @@ -1,5 +1,24 @@ #include "ActionDatabase.hpp" +// --------------------------------------------------------------------------- +// Singleton +// --------------------------------------------------------------------------- + +ActionDatabase &ActionDatabase::getSingleton() +{ + static ActionDatabase instance; + return instance; +} + +ActionDatabase *ActionDatabase::getSingletonPtr() +{ + return &getSingleton(); +} + +// --------------------------------------------------------------------------- +// Find methods +// --------------------------------------------------------------------------- + const GoapAction *ActionDatabase::findAction(const Ogre::String &name) const { for (const auto &action : actions) { @@ -36,6 +55,36 @@ GoapGoal *ActionDatabase::findGoal(const Ogre::String &name) return nullptr; } +// --------------------------------------------------------------------------- +// Add or replace +// --------------------------------------------------------------------------- + +void ActionDatabase::addOrReplaceAction(const GoapAction &action) +{ + for (auto &a : actions) { + if (a.name == action.name) { + a = action; + return; + } + } + actions.push_back(action); +} + +void ActionDatabase::addOrReplaceGoal(const GoapGoal &goal) +{ + for (auto &g : goals) { + if (g.name == goal.name) { + g = goal; + return; + } + } + goals.push_back(goal); +} + +// --------------------------------------------------------------------------- +// Remove methods +// --------------------------------------------------------------------------- + bool ActionDatabase::removeAction(const Ogre::String &name) { for (auto it = actions.begin(); it != actions.end(); ++it) { @@ -58,8 +107,12 @@ bool ActionDatabase::removeGoal(const Ogre::String &name) return false; } -const GoapGoal *ActionDatabase::selectBestGoal( - const GoapBlackboard &blackboard) const +// --------------------------------------------------------------------------- +// Selection / validation +// --------------------------------------------------------------------------- + +const GoapGoal * +ActionDatabase::selectBestGoal(const GoapBlackboard &blackboard) const { const GoapGoal *best = nullptr; int bestPriority = -1; @@ -75,8 +128,8 @@ const GoapGoal *ActionDatabase::selectBestGoal( return best; } -std::vector ActionDatabase::getValidActions( - const GoapBlackboard &blackboard) const +std::vector +ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const { std::vector result; for (const auto &action : actions) { @@ -85,3 +138,27 @@ std::vector ActionDatabase::getValidActions( } return result; } + +// --------------------------------------------------------------------------- +// Clear +// --------------------------------------------------------------------------- + +void ActionDatabase::clear() +{ + actions.clear(); + goals.clear(); +} + +// --------------------------------------------------------------------------- +// ActionDatabaseComponent +// --------------------------------------------------------------------------- + +void ActionDatabaseComponent::syncToSingleton() const +{ + auto &db = ActionDatabase::getSingleton(); + db.clear(); + for (const auto &action : actions) + db.addOrReplaceAction(action); + for (const auto &goal : goals) + db.addOrReplaceGoal(goal); +} diff --git a/src/features/editScene/components/ActionDatabase.hpp b/src/features/editScene/components/ActionDatabase.hpp index 5c25fa7..7abf569 100644 --- a/src/features/editScene/components/ActionDatabase.hpp +++ b/src/features/editScene/components/ActionDatabase.hpp @@ -5,14 +5,22 @@ #include "GoapAction.hpp" #include "GoapGoal.hpp" #include +#include /** - * Global action database component. + * Global action database singleton. * * Holds the master list of GOAP actions and goals that characters can use. - * Typically attached to a single "game manager" entity. + * This is a singleton accessible from anywhere in the codebase. + * The ActionDatabaseComponent on a scene entity stores the actions/goals + * and syncs them to the singleton on scene load. */ -struct ActionDatabase { +class ActionDatabase { +public: + /** Get the singleton instance */ + static ActionDatabase &getSingleton(); + static ActionDatabase *getSingletonPtr(); + std::vector actions; std::vector goals; @@ -24,6 +32,12 @@ struct ActionDatabase { const GoapGoal *findGoal(const Ogre::String &name) const; GoapGoal *findGoal(const Ogre::String &name); + // Add or replace an action by name + void addOrReplaceAction(const GoapAction &action); + + // Add or replace a goal by name + void addOrReplaceGoal(const GoapGoal &goal); + // Remove an action by name bool removeAction(const Ogre::String &name); @@ -35,8 +49,29 @@ struct ActionDatabase { const GoapGoal *selectBestGoal(const GoapBlackboard &blackboard) const; // Build a list of actions that can run from a given blackboard state - std::vector getValidActions( - const GoapBlackboard &blackboard) const; + std::vector + getValidActions(const GoapBlackboard &blackboard) const; + + // Clear all actions and goals + void clear(); + +private: + ActionDatabase() = default; + ~ActionDatabase() = default; + ActionDatabase(const ActionDatabase &) = delete; + ActionDatabase &operator=(const ActionDatabase &) = delete; +}; + +/** + * Flecs component that stores action database data on a scene entity. + * When set on an entity, it syncs its contents to the ActionDatabase singleton. + */ +struct ActionDatabaseComponent { + std::vector actions; + std::vector goals; + + /** Sync this component's data to the ActionDatabase singleton */ + void syncToSingleton() const; }; #endif // EDITSCENE_ACTION_DATABASE_HPP diff --git a/src/features/editScene/components/ActionDatabaseModule.cpp b/src/features/editScene/components/ActionDatabaseModule.cpp index 4616ef4..6cdf7f9 100644 --- a/src/features/editScene/components/ActionDatabaseModule.cpp +++ b/src/features/editScene/components/ActionDatabaseModule.cpp @@ -8,21 +8,21 @@ #include "../ui/ActionDatabaseEditor.hpp" #include "../ui/BehaviorTreeEditor.hpp" -REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabase, +REGISTER_COMPONENT_GROUP("Action Database", "AI", ActionDatabaseComponent, ActionDatabaseEditor) { - registry.registerComponent( + registry.registerComponent( "Action Database", "AI", std::make_unique(), // Adder [](flecs::entity e) { - if (!e.has()) - e.set({}); + if (!e.has()) + e.set({}); }, // Remover [](flecs::entity e) { - if (e.has()) - e.remove(); + if (e.has()) + e.remove(); }); } @@ -30,8 +30,7 @@ REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent, BehaviorTreeEditor) { registry.registerComponent( - "Behavior Tree", "AI", - std::make_unique(), + "Behavior Tree", "AI", std::make_unique(), // Adder [](flecs::entity e) { if (!e.has()) { diff --git a/src/features/editScene/lua-examples/action_db_example.lua b/src/features/editScene/lua-examples/action_db_example.lua new file mode 100644 index 0000000..f6c46ff --- /dev/null +++ b/src/features/editScene/lua-examples/action_db_example.lua @@ -0,0 +1,418 @@ +-- ============================================================================= +-- Action Database Lua API Examples +-- ============================================================================= +-- This file demonstrates how to create, query, and manage GOAP actions and +-- goals from Lua using the ecs.action_db API. +-- +-- The ActionDatabase is a global singleton. Actions and goals defined here +-- are immediately available to all characters in the scene. +-- ============================================================================= + +-- ============================================================================= +-- Defining Bit Names +-- ============================================================================= +-- Before using bits in preconditions/effects, you should define meaningful +-- names for the 64 available bit slots. This makes your actions readable. +-- +-- Bit names are global across the entire game session. They map +-- human-readable names (like "has_axe", "is_hungry") to bit indices +-- (0-63) used in GoapBlackboard preconditions and effects. +-- +-- You can define bits explicitly at startup: +-- ============================================================================= + +-- Explicitly assign bit names to specific indices: +ecs.action_db.set_bit_name(0, "has_axe") +ecs.action_db.set_bit_name(1, "has_wood") +ecs.action_db.set_bit_name(2, "is_hungry") +ecs.action_db.set_bit_name(3, "near_tree") +ecs.action_db.set_bit_name(4, "near_well") +ecs.action_db.set_bit_name(5, "has_bucket") +ecs.action_db.set_bit_name(6, "has_food") +ecs.action_db.set_bit_name(7, "near_fire") +ecs.action_db.set_bit_name(8, "has_cooked_food") +ecs.action_db.set_bit_name(9, "is_awake") +ecs.action_db.set_bit_name(10, "at_market") +ecs.action_db.set_bit_name(11, "at_home") +ecs.action_db.set_bit_name(12, "near_chair") +ecs.action_db.set_bit_name(13, "is_sitting") +ecs.action_db.set_bit_name(14, "near_forest") +ecs.action_db.set_bit_name(15, "is_strong") +ecs.action_db.set_bit_name(16, "has_strength") + +-- Or use auto_assign_bit() to let the system pick the index: +local idx = ecs.action_db.auto_assign_bit("has_water") +print("'has_water' assigned to bit " .. idx) + +-- Look up a bit by name: +local bit_idx = ecs.action_db.find_bit_by_name("has_axe") +print("'has_axe' is at bit " .. bit_idx) + +-- Get the name for a bit index: +local name = ecs.action_db.get_bit_name(0) +print("Bit 0 is named '" .. name .. "'") + +-- List all currently assigned bit names: +local bits = ecs.action_db.list_bit_names() +print("Assigned bit names:") +for _, b in ipairs(bits) do + print(" Bit " .. b.index .. ": " .. b.name) +end + +-- NOTE: If you use a bit name in an action's preconditions/effects +-- that hasn't been explicitly assigned, it will be auto-assigned +-- to the first free slot automatically. So you don't HAVE to +-- pre-define them, but it's good practice for clarity. + +-- ============================================================================= +-- Creating Actions +-- ============================================================================= + +-- Simple action with just a name and cost: +ecs.action_db.add_action("idle", 1) + +-- Action with preconditions (what must be true before the action can run): +ecs.action_db.add_action("chop_wood", 2, + { + bits = { has_axe = true }, + values = { stamina = 10 } + }, + { -- effects (what becomes true after the action runs) + bits = { has_wood = true }, + values = { stamina = -5 } + } +) + +-- Action with only preconditions, no effects: +ecs.action_db.add_action("fetch_water", 3, + { + bits = { near_well = true, has_bucket = true }, + values = { thirst = 50 } + } +) + +-- Action with only effects, no preconditions: +ecs.action_db.add_action("rest", 1, + {}, + { + values = { stamina = 100, energy = 100 } + } +) + +-- Action with float and string values in blackboard: +ecs.action_db.add_action("cook_food", 4, + { + bits = { has_food = true, near_fire = true }, + values = { cooking_skill = 3 }, + floatValues = { hunger = 50.0 } + }, + { + bits = { has_cooked_food = true }, + values = { hunger = -30 }, + stringValues = { last_action = "cooking" } + } +) + +-- ============================================================================= +-- Creating Actions WITH Behavior Trees +-- ============================================================================= + +-- Action with a simple leaf behavior tree (plays an animation): +ecs.action_db.add_action("wave", 1, + {}, -- no preconditions + {}, -- no effects + { -- behavior tree (arg 5) + type = "sequence", + children = { + { type = "setAnimationState", name = "SM/Wave" }, + { type = "delay", params = "2.0" }, + { type = "setAnimationState", name = "SM/Idle" } + } + } +) + +-- Action with a selector behavior tree (try to chop, fall back to idle): +ecs.action_db.add_action("chop_tree", 3, + { + bits = { near_tree = true }, + values = { stamina = 15 } + }, + { + bits = { has_wood = true }, + values = { stamina = -8, wood_count = 1 } + }, + { + type = "selector", + children = { + { + type = "sequence", + children = { + { type = "checkBit", name = "has_axe", params = "1" }, + { type = "setAnimationState", name = "SM/Chop" }, + { type = "delay", params = "3.0" }, + { type = "setAnimationState", name = "SM/Idle" }, + { type = "setBit", name = "has_wood", params = "1" } + } + }, + { + type = "sequence", + children = { + { type = "debugPrint", name = "No axe! Picking up stick..." }, + { type = "setAnimationState", name = "SM/Pickup" }, + { type = "delay", params = "1.0" }, + { type = "setBit", name = "has_wood", params = "1" } + } + } + } + } +) + +-- Action with blackboard checks and value manipulation: +ecs.action_db.add_action("travel_to_market", 5, + { + bits = { is_awake = true }, + values = { energy = 20 } + }, + { + bits = { at_market = true }, + values = { energy = -15 } + }, + { + type = "sequence", + children = { + { type = "checkValue", name = "energy", params = ">= 20" }, + { type = "setAnimationState", name = "SM/Walk" }, + { type = "delay", params = "5.0" }, + { type = "setAnimationState", name = "SM/Idle" }, + { type = "setBit", name = "at_market", params = "1" }, + { type = "setBit", name = "at_home", params = "0" } + } + } +) + +-- Action with inventory operations: +ecs.action_db.add_action("gather_wood", 2, + { + bits = { near_forest = true } + }, + { + values = { wood_count = 3 } + }, + { + type = "sequence", + children = { + { type = "setAnimationState", name = "SM/Gather" }, + { type = "delay", params = "2.0" }, + { type = "addItemToInventory", params = "wood,Firewood,material,3,1.0,0" }, + { type = "setAnimationState", name = "SM/Idle" } + } + } +) + +-- Action that teleports character to a smart object child: +ecs.action_db.add_action("sit_on_chair", 1, + { + bits = { near_chair = true } + }, + { + bits = { is_sitting = true } + }, + { + type = "sequence", + children = { + { type = "teleportToChild", name = "SitTarget" }, + { type = "disablePhysics" }, + { type = "setAnimationState", name = "SM/Sit" }, + { type = "delay", params = "5.0" }, + { type = "setAnimationState", name = "SM/Stand" }, + { type = "enablePhysics" } + } + } +) + +-- ============================================================================= +-- Creating Goals +-- ============================================================================= + +-- Simple goal with just a name and priority: +ecs.action_db.add_goal("survive", 100) + +-- Goal with a target blackboard state: +ecs.action_db.add_goal("gather_resources", 50, + { + bits = { has_wood = true, has_water = true }, + values = { wood_count = 5, water_count = 3 } + } +) + +-- Goal with a condition expression (evaluated against character's blackboard): +ecs.action_db.add_goal("stay_healthy", 80, + { + values = { health = 100, stamina = 80 } + }, + "health < 50 || stamina < 30" -- only valid when character needs healing +) + +-- Goal with full specification: +ecs.action_db.add_goal("become_strong", 30, + { + bits = { is_strong = true }, + values = { strength = 100 } + }, + "strength < 100" -- only valid if not already strong +) + +-- ============================================================================= +-- Querying Actions and Goals +-- ============================================================================= + +-- Find an action by name: +local action = ecs.action_db.find_action("chop_wood") +if action then + print("Found action: " .. action.name .. " (cost: " .. action.cost .. ")") + -- action.preconditions and action.effects are tables with: + -- .bits - table of boolean flags + -- .values - table of integer values + -- .floatValues - table of float values + -- .stringValues - table of string values + -- action.behaviorTree is a table with: + -- .type - node type string + -- .name - optional name + -- .params - optional params + -- .children - optional array of child nodes +end + +-- Find a goal by name: +local goal = ecs.action_db.find_goal("gather_resources") +if goal then + print("Found goal: " .. goal.name .. " (priority: " .. goal.priority .. ")") + -- goal.target is a blackboard table + -- goal.condition is the condition string +end + +-- ============================================================================= +-- Listing All Actions and Goals +-- ============================================================================= + +-- List all action names: +local actions = ecs.action_db.list_actions() +print("Available actions:") +for i, name in ipairs(actions) do + print(" " .. i .. ". " .. name) +end + +-- List all goal names: +local goals = ecs.action_db.list_goals() +print("Available goals:") +for i, name in ipairs(goals) do + print(" " .. i .. ". " .. name) +end + +-- ============================================================================= +-- Removing Actions and Goals +-- ============================================================================= + +-- Remove an action by name: +local removed = ecs.action_db.remove_action("idle") +if removed then + print("Removed action: idle") +end + +-- Remove a goal by name: +ecs.action_db.remove_goal("become_strong") + +-- ============================================================================= +-- Replacing Actions (same name = replace) +-- ============================================================================= + +-- If you add an action with the same name as an existing one, it replaces it: +ecs.action_db.add_action("chop_wood", 5, + { + bits = { has_axe = true, has_strength = true }, + values = { stamina = 20 } + }, + { + bits = { has_wood = true }, + values = { stamina = -10, wood_count = 2 } + } +) +-- The old "chop_wood" action is replaced with this new definition. + +-- ============================================================================= +-- Clearing Everything +-- ============================================================================= + +-- Remove all actions and goals: +-- ecs.action_db.clear() + +-- ============================================================================= +-- Blackboard Table Format Reference +-- ============================================================================= +-- +-- The blackboard table passed to add_action/add_goal has this structure: +-- +-- { +-- bits = { +-- has_axe = true, -- boolean flags (use named bits) +-- has_wood = false, +-- is_hungry = true +-- }, +-- values = { -- integer values +-- health = 100, +-- stamina = 50, +-- wood_count = 0 +-- }, +-- floatValues = { -- float values +-- hunger = 75.5, +-- speed = 1.2 +-- }, +-- stringValues = { -- string values +-- last_action = "idle", +-- current_state = "exploring" +-- } +-- } +-- +-- All sub-tables are optional. An empty table or nil means no constraints. +-- ============================================================================= + +-- ============================================================================= +-- Behavior Tree Table Format Reference +-- ============================================================================= +-- +-- The behaviorTree table (arg 5 of add_action) has this structure: +-- +-- { +-- type = "sequence", -- node type (required) +-- name = "optional_name", -- depends on type (task name, anim state, etc.) +-- params = "optional_params", -- extra parameters (delay seconds, bit index, etc.) +-- children = { -- array of child nodes (for sequence/selector/invert) +-- { type = "task", name = "myAction" }, +-- { type = "setAnimationState", name = "SM/Walk" }, +-- { type = "delay", params = "2.0" }, +-- { type = "checkBit", name = "has_axe", params = "1" } +-- } +-- } +-- +-- Common node types: +-- "sequence" - Execute children in order until one fails +-- "selector" - Execute children in order until one succeeds +-- "invert" - Invert the result of a single child +-- "task" - Leaf: references a named task +-- "check" - Leaf: references a named condition +-- "debugPrint" - Leaf: prints 'name' to console +-- "setAnimationState"- Leaf: sets animation state (name="SM/State") +-- "isAnimationEnded" - Leaf: true if animation ended +-- "setBit" - Leaf: sets blackboard bit (name=bit, params=0/1) +-- "checkBit" - Leaf: true if blackboard bit is set +-- "setValue" - Leaf: sets blackboard value (name=key, params=val) +-- "checkValue" - Leaf: blackboard comparison (name=key, params="op val") +-- "delay" - Leaf: waits N seconds (params=seconds as float) +-- "teleportToChild" - Leaf: teleports to named child of Smart Object +-- "disablePhysics" - Leaf: removes character from physics +-- "enablePhysics" - Leaf: re-adds character to physics +-- "hasItem" - Leaf check: true if inventory has itemId +-- "pickupItem" - Leaf: picks up nearest item +-- "dropItem" - Leaf: drops item from inventory +-- "useItem" - Leaf: uses item from inventory +-- "addItemToInventory"- Leaf: adds item directly to inventory +-- ============================================================================= diff --git a/src/features/editScene/lua/LuaActionApi.cpp b/src/features/editScene/lua/LuaActionApi.cpp new file mode 100644 index 0000000..a414543 --- /dev/null +++ b/src/features/editScene/lua/LuaActionApi.cpp @@ -0,0 +1,576 @@ +#include "LuaActionApi.hpp" +#include "../components/ActionDatabase.hpp" +#include "../components/GoapBlackboard.hpp" +#include "../components/BehaviorTree.hpp" +#include +#include + +namespace editScene +{ + +// --------------------------------------------------------------------------- +// Helper: push a BehaviorTreeNode as a Lua table +// --------------------------------------------------------------------------- + +static void pushBehaviorTree(lua_State *L, const BehaviorTreeNode &node) +{ + lua_newtable(L); + + lua_pushstring(L, node.type.c_str()); + lua_setfield(L, -2, "type"); + + if (!node.name.empty()) { + lua_pushstring(L, node.name.c_str()); + lua_setfield(L, -2, "name"); + } + + if (!node.params.empty()) { + lua_pushstring(L, node.params.c_str()); + lua_setfield(L, -2, "params"); + } + + if (!node.children.empty()) { + lua_newtable(L); + for (size_t i = 0; i < node.children.size(); i++) { + pushBehaviorTree(L, node.children[i]); + lua_rawseti(L, -2, (int)i + 1); + } + lua_setfield(L, -2, "children"); + } +} + +// --------------------------------------------------------------------------- +// Helper: read a BehaviorTreeNode from a Lua table at given index +// --------------------------------------------------------------------------- + +static BehaviorTreeNode readBehaviorTree(lua_State *L, int idx) +{ + BehaviorTreeNode node; + + if (!lua_istable(L, idx)) + return node; + + // type (required) + lua_getfield(L, idx, "type"); + if (lua_isstring(L, -1)) + node.type = lua_tostring(L, -1); + lua_pop(L, 1); + + // name (optional) + lua_getfield(L, idx, "name"); + if (lua_isstring(L, -1)) + node.name = lua_tostring(L, -1); + lua_pop(L, 1); + + // params (optional) + lua_getfield(L, idx, "params"); + if (lua_isstring(L, -1)) + node.params = lua_tostring(L, -1); + lua_pop(L, 1); + + // children (optional array) + lua_getfield(L, idx, "children"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + // key is numeric index, value is child table + if (lua_istable(L, -1)) { + node.children.push_back( + readBehaviorTree(L, lua_gettop(L))); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + return node; +} + +// --------------------------------------------------------------------------- +// Helper: push a GoapBlackboard as a Lua table +// --------------------------------------------------------------------------- + +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: read a GoapBlackboard from a Lua table (optional, at given index) +// --------------------------------------------------------------------------- + +static GoapBlackboard readBlackboard(lua_State *L, int idx) +{ + GoapBlackboard bb; + + if (!lua_istable(L, idx)) + return bb; + + // Read bits + lua_getfield(L, idx, "bits"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + // key is the bit name (string), value is boolean + if (lua_isstring(L, -2) && lua_isboolean(L, -1)) { + const char *name = lua_tostring(L, -2); + bool val = lua_toboolean(L, -1) != 0; + // Find bit index by name (auto-registers if new) + int idx2 = GoapBlackboard::findBitByName(name); + if (idx2 < 0) { + // Find first free slot + for (int i = 0; i < 64; i++) { + if (GoapBlackboard::getBitName( + i) == nullptr) { + GoapBlackboard::setBitName( + i, name); + idx2 = i; + break; + } + } + } + if (idx2 >= 0) + bb.setBit(idx2, val); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Read integer values + lua_getfield(L, idx, "values"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2) && lua_isinteger(L, -1)) + bb.values[lua_tostring(L, -2)] = + (int)lua_tointeger(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Read float values + lua_getfield(L, idx, "floatValues"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2) && lua_isnumber(L, -1)) + bb.floatValues[lua_tostring(L, -2)] = + (float)lua_tonumber(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Read string values + lua_getfield(L, idx, "stringValues"); + if (lua_istable(L, -1)) { + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_isstring(L, -2) && lua_isstring(L, -1)) + bb.stringValues[lua_tostring(L, -2)] = + lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + return bb; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree) +// --------------------------------------------------------------------------- + +static int luaAddAction(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + int cost = (int)luaL_optinteger(L, 2, 1); + + GoapAction action(name, cost); + + // Optional preconditions table (arg 3) + if (lua_gettop(L) >= 3 && lua_istable(L, 3)) + action.preconditions = readBlackboard(L, 3); + + // Optional effects table (arg 4) + if (lua_gettop(L) >= 4 && lua_istable(L, 4)) + action.effects = readBlackboard(L, 4); + + // Optional behavior tree table (arg 5) + if (lua_gettop(L) >= 5 && lua_istable(L, 5)) + action.behaviorTree = readBehaviorTree(L, 5); + + ActionDatabase::getSingleton().addOrReplaceAction(action); + + Ogre::LogManager::getSingleton().stream() + << "[Lua] Added action: " << name; + return 0; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.add_goal(name, priority, target, condition) +// --------------------------------------------------------------------------- + +static int luaAddGoal(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + int priority = (int)luaL_optinteger(L, 2, 1); + + GoapGoal goal(name, priority); + + // Optional target blackboard (arg 3) + if (lua_gettop(L) >= 3 && lua_istable(L, 3)) + goal.target = readBlackboard(L, 3); + + // Optional condition string (arg 4) + if (lua_gettop(L) >= 4 && lua_isstring(L, 4)) + goal.condition = lua_tostring(L, 4); + + ActionDatabase::getSingleton().addOrReplaceGoal(goal); + + Ogre::LogManager::getSingleton().stream() + << "[Lua] Added goal: " << name; + return 0; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.remove_action(name) -> bool +// --------------------------------------------------------------------------- + +static int luaRemoveAction(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + bool removed = ActionDatabase::getSingleton().removeAction(name); + lua_pushboolean(L, removed); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.remove_goal(name) -> bool +// --------------------------------------------------------------------------- + +static int luaRemoveGoal(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + bool removed = ActionDatabase::getSingleton().removeGoal(name); + lua_pushboolean(L, removed); + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.find_action(name) -> table or nil +// --------------------------------------------------------------------------- + +static int luaFindAction(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + const GoapAction *action = + ActionDatabase::getSingleton().findAction(name); + + if (!action) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + lua_pushstring(L, action->name.c_str()); + lua_setfield(L, -2, "name"); + lua_pushinteger(L, action->cost); + lua_setfield(L, -2, "cost"); + + pushBlackboard(L, action->preconditions); + lua_setfield(L, -2, "preconditions"); + + pushBlackboard(L, action->effects); + lua_setfield(L, -2, "effects"); + + // Behavior tree + pushBehaviorTree(L, action->behaviorTree); + lua_setfield(L, -2, "behaviorTree"); + + // Behavior tree name (optional reference) + if (!action->behaviorTreeName.empty()) { + lua_pushstring(L, action->behaviorTreeName.c_str()); + lua_setfield(L, -2, "behaviorTreeName"); + } + + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.find_goal(name) -> table or nil +// --------------------------------------------------------------------------- + +static int luaFindGoal(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + const GoapGoal *goal = ActionDatabase::getSingleton().findGoal(name); + + if (!goal) { + lua_pushnil(L); + return 1; + } + + lua_newtable(L); + lua_pushstring(L, goal->name.c_str()); + lua_setfield(L, -2, "name"); + lua_pushinteger(L, goal->priority); + lua_setfield(L, -2, "priority"); + + pushBlackboard(L, goal->target); + lua_setfield(L, -2, "target"); + + lua_pushstring(L, goal->condition.c_str()); + lua_setfield(L, -2, "condition"); + + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.list_actions() -> table of action names +// --------------------------------------------------------------------------- + +static int luaListActions(lua_State *L) +{ + const auto &db = ActionDatabase::getSingleton(); + lua_newtable(L); + int idx = 1; + for (const auto &action : db.actions) { + lua_pushstring(L, action.name.c_str()); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.list_goals() -> table of goal names +// --------------------------------------------------------------------------- + +static int luaListGoals(lua_State *L) +{ + const auto &db = ActionDatabase::getSingleton(); + lua_newtable(L); + int idx = 1; + for (const auto &goal : db.goals) { + lua_pushstring(L, goal.name.c_str()); + lua_rawseti(L, -2, idx++); + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.clear() -> nil +// --------------------------------------------------------------------------- + +static int luaClear(lua_State *L) +{ + (void)L; + ActionDatabase::getSingleton().clear(); + return 0; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.set_bit_name(index, name) -> nil +// --------------------------------------------------------------------------- + +static int luaSetBitName(lua_State *L) +{ + int index = (int)luaL_checkinteger(L, 1); + const char *name = luaL_checkstring(L, 2); + + if (index < 0 || index >= 64) + luaL_error(L, "bit index must be 0-63, got %d", index); + + GoapBlackboard::setBitName(index, name); + return 0; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.find_bit_by_name(name) -> index or nil +// --------------------------------------------------------------------------- + +static int luaFindBitByName(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + int index = GoapBlackboard::findBitByName(name); + if (index < 0) { + lua_pushnil(L); + } else { + lua_pushinteger(L, index); + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.get_bit_name(index) -> name or nil +// --------------------------------------------------------------------------- + +static int luaGetBitName(lua_State *L) +{ + int index = (int)luaL_checkinteger(L, 1); + if (index < 0 || index >= 64) { + lua_pushnil(L); + return 1; + } + const char *name = GoapBlackboard::getBitName(index); + if (name) { + lua_pushstring(L, name); + } else { + lua_pushnil(L); + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.list_bit_names() -> table of { index, name } +// --------------------------------------------------------------------------- + +static int luaListBitNames(lua_State *L) +{ + lua_newtable(L); + int idx = 1; + for (int i = 0; i < 64; i++) { + const char *name = GoapBlackboard::getBitName(i); + if (name) { + lua_newtable(L); + lua_pushinteger(L, i); + lua_setfield(L, -2, "index"); + lua_pushstring(L, name); + lua_setfield(L, -2, "name"); + lua_rawseti(L, -2, idx++); + } + } + return 1; +} + +// --------------------------------------------------------------------------- +// Lua: ecs.action_db.auto_assign_bit(name) -> index +// --------------------------------------------------------------------------- +// Finds a bit by name, or auto-assigns the first free slot if not found. +// Returns the bit index, or -1 if all 64 slots are full. + +static int luaAutoAssignBit(lua_State *L) +{ + const char *name = luaL_checkstring(L, 1); + int index = GoapBlackboard::findBitByName(name); + if (index >= 0) { + lua_pushinteger(L, index); + return 1; + } + // Find first free slot + for (int i = 0; i < 64; i++) { + if (GoapBlackboard::getBitName(i) == nullptr) { + GoapBlackboard::setBitName(i, name); + lua_pushinteger(L, i); + return 1; + } + } + lua_pushinteger(L, -1); + return 1; +} + +// --------------------------------------------------------------------------- +// Registration +// --------------------------------------------------------------------------- + +void registerLuaActionApi(lua_State *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 action_db sub-table + lua_newtable(L); + + lua_pushcfunction(L, luaAddAction); + lua_setfield(L, -2, "add_action"); + + lua_pushcfunction(L, luaAddGoal); + lua_setfield(L, -2, "add_goal"); + + lua_pushcfunction(L, luaRemoveAction); + lua_setfield(L, -2, "remove_action"); + + lua_pushcfunction(L, luaRemoveGoal); + lua_setfield(L, -2, "remove_goal"); + + lua_pushcfunction(L, luaFindAction); + lua_setfield(L, -2, "find_action"); + + lua_pushcfunction(L, luaFindGoal); + lua_setfield(L, -2, "find_goal"); + + lua_pushcfunction(L, luaListActions); + lua_setfield(L, -2, "list_actions"); + + lua_pushcfunction(L, luaListGoals); + lua_setfield(L, -2, "list_goals"); + + lua_pushcfunction(L, luaClear); + lua_setfield(L, -2, "clear"); + + // Bit name management + lua_pushcfunction(L, luaSetBitName); + lua_setfield(L, -2, "set_bit_name"); + + lua_pushcfunction(L, luaFindBitByName); + lua_setfield(L, -2, "find_bit_by_name"); + + lua_pushcfunction(L, luaGetBitName); + lua_setfield(L, -2, "get_bit_name"); + + lua_pushcfunction(L, luaListBitNames); + lua_setfield(L, -2, "list_bit_names"); + + lua_pushcfunction(L, luaAutoAssignBit); + lua_setfield(L, -2, "auto_assign_bit"); + + // Set action_db as a field of ecs + lua_setfield(L, -2, "action_db"); + + // Ensure ecs is global + lua_setglobal(L, "ecs"); +} + +} // namespace editScene diff --git a/src/features/editScene/lua/LuaActionApi.hpp b/src/features/editScene/lua/LuaActionApi.hpp new file mode 100644 index 0000000..d17b9a4 --- /dev/null +++ b/src/features/editScene/lua/LuaActionApi.hpp @@ -0,0 +1,101 @@ +#ifndef EDITSCENE_LUA_ACTION_API_HPP +#define EDITSCENE_LUA_ACTION_API_HPP +#pragma once + +#include + +/** + * @file LuaActionApi.hpp + * @brief Lua API for the ActionDatabase singleton. + * + * Provides Lua functions to create, query, and manage GOAP actions + * and goals in the global ActionDatabase singleton. + * + * Exposed Lua globals (under the "ecs" table): + * ecs.action_db.add_action(name, cost, preconds, effects, behaviorTree) -> nil + * ecs.action_db.add_goal(name, priority, target, condition) -> nil + * ecs.action_db.remove_action(name) -> bool + * ecs.action_db.remove_goal(name) -> bool + * ecs.action_db.find_action(name) -> table or nil + * ecs.action_db.find_goal(name) -> table or nil + * ecs.action_db.list_actions() -> table of action names + * ecs.action_db.list_goals() -> table of goal names + * ecs.action_db.clear() -> nil + * + * Bit Name Management: + * ecs.action_db.set_bit_name(index, name) -> nil + * Assign a human-readable name to a bit slot (0-63). + * Example: ecs.action_db.set_bit_name(0, "has_axe") + * + * ecs.action_db.find_bit_by_name(name) -> index or nil + * Look up which bit index a name is assigned to. + * Returns nil if the name hasn't been assigned yet. + * + * ecs.action_db.get_bit_name(index) -> name or nil + * Get the name assigned to a bit slot, or nil if unassigned. + * + * ecs.action_db.list_bit_names() -> table of { index, name } + * Returns an array of all assigned bit names with their indices. + * + * ecs.action_db.auto_assign_bit(name) -> index + * Find a bit by name, or auto-assign the first free slot. + * Returns the bit index, or -1 if all 64 slots are full. + * This is the same logic used internally by readBlackboard() + * when it encounters an unknown bit name in preconditions/effects. + * + * Bit Name Convention: + * Bit names are global across the entire game session. They map + * human-readable names (like "has_axe", "is_hungry") to bit indices + * (0-63) used in GoapBlackboard preconditions and effects. + * + * You can define bits explicitly at startup: + * ecs.action_db.set_bit_name(0, "has_axe") + * ecs.action_db.set_bit_name(1, "has_wood") + * ecs.action_db.set_bit_name(2, "is_hungry") + * + * Or let them be auto-assigned when you use them in actions: + * ecs.action_db.add_action("chop_wood", 2, + * { bits = { has_axe = true } }, -- "has_axe" auto-assigned if new + * { bits = { has_wood = true } }) + * + * Use auto_assign_bit() to explicitly reserve a name: + * local idx = ecs.action_db.auto_assign_bit("my_flag") + * -- idx is now the bit index for "my_flag" + * + * Use list_bit_names() to see all currently assigned names: + * local bits = ecs.action_db.list_bit_names() + * for _, b in ipairs(bits) do + * print(b.index .. ": " .. b.name) + * end + * + * Behavior Tree Table Format (arg 5 of add_action): + * { + * type = "sequence", -- node type: sequence, selector, invert, task, check, etc. + * name = "optional_name", -- action/condition name, animation state, etc. + * params = "optional_params", -- extra parameters (e.g. delay seconds, bit index) + * children = { -- array of child nodes (for sequence/selector/invert) + * { type = "task", name = "myAction" }, + * { type = "setAnimationState", name = "SM/Walk" }, + * { type = "delay", params = "2.0" } + * } + * } + * + * See BehaviorTree.hpp for full list of node types. + */ + +namespace editScene +{ + +/** + * @brief Register all ActionDatabase-related Lua API functions. + * + * Adds action_db sub-table to the "ecs" global table. + * Must be called after LuaState is constructed. + * + * @param L The Lua state. + */ +void registerLuaActionApi(lua_State *L); + +} // namespace editScene + +#endif // EDITSCENE_LUA_ACTION_API_HPP diff --git a/src/features/editScene/lua/LuaComponentApi.cpp b/src/features/editScene/lua/LuaComponentApi.cpp index 2e57e96..f2b8dd1 100644 --- a/src/features/editScene/lua/LuaComponentApi.cpp +++ b/src/features/editScene/lua/LuaComponentApi.cpp @@ -1652,14 +1652,8 @@ static void registerAllComponents() c.autoReplan = lua_toboolean(L, -1) != 0; lua_pop(L, 1);); - // --- ActionDatabase --- - REGISTER_COMPONENT(ActionDatabase, "ActionDatabase", - lua_pushinteger(L, (int)c.actions.size()); - lua_setfield(L, -2, "numActions"); - lua_pushinteger(L, (int)c.goals.size()); - lua_setfield(L, -2, "numGoals"); - /* end PushBody */ - , (void)idx;); + // ActionDatabase is now a singleton, not a per-entity component. + // Use ActionDatabase:getSingleton() from Lua API instead. // --- ActionDebug --- REGISTER_COMPONENT( diff --git a/src/features/editScene/systems/ActuatorSystem.cpp b/src/features/editScene/systems/ActuatorSystem.cpp index 29adaa9..ba22424 100644 --- a/src/features/editScene/systems/ActuatorSystem.cpp +++ b/src/features/editScene/systems/ActuatorSystem.cpp @@ -118,13 +118,8 @@ bool ActuatorSystem::isActionComplete(flecs::entity character, float deltaTime) if (m_executingActionName.empty()) return true; - // Look up the action in the database to get its behavior tree - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Look up the action in the singleton database to get its behavior tree + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (!db) return true; diff --git a/src/features/editScene/systems/BehaviorTreeSystem.cpp b/src/features/editScene/systems/BehaviorTreeSystem.cpp index 8f8ffdd..0d0f9b7 100644 --- a/src/features/editScene/systems/BehaviorTreeSystem.cpp +++ b/src/features/editScene/systems/BehaviorTreeSystem.cpp @@ -994,12 +994,7 @@ void BehaviorTreeSystem::update(float deltaTime) }); /* --- ActionDebug test runs --- */ - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (!db) return; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index e7b7a40..7093c6f 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -606,8 +606,7 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth) indicators += " [Mat]"; if (entity.has()) indicators += " [Anim]"; - if (entity.has()) - indicators += " [AI]"; + // ActionDatabase is now a singleton, not a per-entity component if (entity.has()) indicators += " [Debug]"; if (entity.has()) @@ -999,12 +998,7 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } - // Render ActionDatabase if present - if (entity.has()) { - auto &db = entity.get_mut(); - m_componentRegistry.render(entity, db); - componentCount++; - } + // ActionDatabase is now a singleton, not a per-entity component // Render ActionDebug if present if (entity.has()) { diff --git a/src/features/editScene/systems/EventHandlerSystem.cpp b/src/features/editScene/systems/EventHandlerSystem.cpp index c09767a..789d5c1 100644 --- a/src/features/editScene/systems/EventHandlerSystem.cpp +++ b/src/features/editScene/systems/EventHandlerSystem.cpp @@ -182,13 +182,8 @@ void EventHandlerSystem::update(float deltaTime) flecs::entity_t id = pair.first; ActiveHandler &ah = pair.second; - // Look up the action in the database - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Look up the action in the singleton database + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (!db) { completedHandlers.push_back(id); continue; diff --git a/src/features/editScene/systems/GoapPlannerSystem.cpp b/src/features/editScene/systems/GoapPlannerSystem.cpp index 42c6e41..a932327 100644 --- a/src/features/editScene/systems/GoapPlannerSystem.cpp +++ b/src/features/editScene/systems/GoapPlannerSystem.cpp @@ -115,13 +115,8 @@ void GoapPlannerSystem::planForEntity(flecs::entity e, { (void)e; - // Find ActionDatabase - const ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Find ActionDatabase singleton + const ActionDatabase *db = ActionDatabase::getSingletonPtr(); // Select best valid goal const GoapGoal *goal = selectGoal(planner, db, blackboard); diff --git a/src/features/editScene/systems/GoapRunnerSystem.cpp b/src/features/editScene/systems/GoapRunnerSystem.cpp index 62eba7a..281d656 100644 --- a/src/features/editScene/systems/GoapRunnerSystem.cpp +++ b/src/features/editScene/systems/GoapRunnerSystem.cpp @@ -102,13 +102,8 @@ bool GoapRunnerSystem::startNextAction(flecs::entity e) runner.planActions[runner.currentActionIndex]; runner.currentActionName = actionName; - // Find action database - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Find action database singleton + ActionDatabase *db = ActionDatabase::getSingletonPtr(); const GoapAction *action = db ? db->findAction(actionName) : nullptr; @@ -273,15 +268,7 @@ void GoapRunnerSystem::update(float deltaTime) // Apply action effects if (e.has()) { - ActionDatabase *db = nullptr; - m_world - .query() - .each([&](flecs::entity, - ActionDatabase - &database) { - if (!db) - db = &database; - }); + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (db) { const GoapAction *action = db->findAction( diff --git a/src/features/editScene/systems/ItemSystem.cpp b/src/features/editScene/systems/ItemSystem.cpp index d19c5df..372e9f4 100644 --- a/src/features/editScene/systems/ItemSystem.cpp +++ b/src/features/editScene/systems/ItemSystem.cpp @@ -322,13 +322,8 @@ bool ItemSystem::useItem(flecs::entity characterEntity, // Execute the use action via behavior tree if (m_btSystem && characterEntity.is_alive()) { - // Look up the action in the database - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Look up the action in the singleton database + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (db) { const GoapAction *action = diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 943ea7d..4b97b2a 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -74,6 +74,9 @@ bool SceneSerializer::saveToFile(const std::string &filepath) } }); + // Save ActionDatabase singleton at scene level + scene["actionDatabase"] = serializeActionDatabase(); + // Write to file std::ofstream file(filepath); if (!file.is_open()) { @@ -121,6 +124,11 @@ bool SceneSerializer::loadFromFile(const std::string &filepath, } } + // Load ActionDatabase singleton at scene level + if (scene.contains("actionDatabase")) { + deserializeActionDatabase(scene["actionDatabase"]); + } + // Clear entity map for new load m_entityMap.clear(); @@ -295,9 +303,7 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["waterPlane"] = serializeWaterPlane(entity); } - if (entity.has()) { - json["actionDatabase"] = serializeActionDatabase(entity); - } + // ActionDatabase is now a singleton, serialized at scene level if (entity.has()) { json["actionDebug"] = serializeActionDebug(entity); } @@ -515,9 +521,7 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, deserializeWaterPlane(entity, json["waterPlane"]); } - if (json.contains("actionDatabase")) { - deserializeActionDatabase(entity, json["actionDatabase"]); - } + // ActionDatabase is now a singleton, deserialized at scene level if (json.contains("actionDebug")) { deserializeActionDebug(entity, json["actionDebug"]); } @@ -807,9 +811,7 @@ void SceneSerializer::deserializeEntityComponents( deserializeWaterPlane(entity, json["waterPlane"]); } - if (json.contains("actionDatabase")) { - deserializeActionDatabase(entity, json["actionDatabase"]); - } + // ActionDatabase is now a singleton, deserialized at scene level if (json.contains("actionDebug")) { deserializeActionDebug(entity, json["actionDebug"]); } @@ -3396,9 +3398,12 @@ static void deserializeGoapGoal(GoapGoal &goal, const nlohmann::json &json) goal.condition = json.value("condition", ""); } -nlohmann::json SceneSerializer::serializeActionDatabase(flecs::entity entity) +nlohmann::json SceneSerializer::serializeActionDatabase() { - const ActionDatabase &db = entity.get(); + const ActionDatabase *dbPtr = ActionDatabase::getSingletonPtr(); + if (!dbPtr) + return nlohmann::json(); + const ActionDatabase &db = *dbPtr; nlohmann::json json; json["actions"] = nlohmann::json::array(); @@ -3426,16 +3431,17 @@ nlohmann::json SceneSerializer::serializeActionDatabase(flecs::entity entity) return json; } -void SceneSerializer::deserializeActionDatabase(flecs::entity entity, - const nlohmann::json &json) +void SceneSerializer::deserializeActionDatabase(const nlohmann::json &json) { - ActionDatabase db; + ActionDatabase *db = ActionDatabase::getSingletonPtr(); + if (!db) + return; if (json.contains("actions") && json["actions"].is_array()) { for (const auto &actionJson : json["actions"]) { GoapAction action; deserializeGoapAction(action, actionJson); - db.actions.push_back(action); + db->addOrReplaceAction(action); } } @@ -3443,7 +3449,7 @@ void SceneSerializer::deserializeActionDatabase(flecs::entity entity, for (const auto &goalJson : json["goals"]) { GoapGoal goal; deserializeGoapGoal(goal, goalJson); - db.goals.push_back(goal); + db->addOrReplaceGoal(goal); } } @@ -3456,8 +3462,6 @@ void SceneSerializer::deserializeActionDatabase(flecs::entity entity, entry["name"].get()); } } - - entity.set(db); } nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity) diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 96d106c..4598ffb 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -212,7 +212,7 @@ private: const nlohmann::json &json); // AI/GOAP serialization - nlohmann::json serializeActionDatabase(flecs::entity entity); + nlohmann::json serializeActionDatabase(); nlohmann::json serializeActionDebug(flecs::entity entity); nlohmann::json serializePathFollowing(flecs::entity entity); nlohmann::json serializeSmartObject(flecs::entity entity); @@ -220,8 +220,7 @@ private: nlohmann::json serializeEventHandler(flecs::entity entity); nlohmann::json serializeGoapPlanner(flecs::entity entity); nlohmann::json serializeBehaviorTree(flecs::entity entity); - void deserializeActionDatabase(flecs::entity entity, - const nlohmann::json &json); + void deserializeActionDatabase(const nlohmann::json &json); void deserializeActionDebug(flecs::entity entity, const nlohmann::json &json); void deserializePathFollowing(flecs::entity entity, diff --git a/src/features/editScene/systems/SmartObjectSystem.cpp b/src/features/editScene/systems/SmartObjectSystem.cpp index 253e09e..ff2dcb6 100644 --- a/src/features/editScene/systems/SmartObjectSystem.cpp +++ b/src/features/editScene/systems/SmartObjectSystem.cpp @@ -156,13 +156,8 @@ bool SmartObjectSystem::testSmartObjectAction(flecs::entity character, if (!character.is_alive() || !smartObject.is_alive()) return false; - // Find the action in the database - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Find the action in the singleton database + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (!db) return false; @@ -208,13 +203,8 @@ void SmartObjectSystem::update(float deltaTime) navmeshEntity = e; }); - // Find the action database - ActionDatabase *db = nullptr; - m_world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Get the action database singleton + ActionDatabase *db = ActionDatabase::getSingletonPtr(); // Determine if we're in game mode. In game mode, player-controlled // characters are managed by PlayerControllerSystem and should NOT diff --git a/src/features/editScene/tests/Ogre.h b/src/features/editScene/tests/Ogre.h new file mode 100644 index 0000000..dac39ca --- /dev/null +++ b/src/features/editScene/tests/Ogre.h @@ -0,0 +1,91 @@ +/** + * @file Ogre.h + * @brief Stub Ogre.h for standalone tests. + * + * Provides minimal Ogre type aliases needed to compile + * ActionDatabase, GoapBlackboard, GoapGoal, GoapAction, + * and BehaviorTree components without the full Ogre SDK. + * + * Only for use in standalone tests (action_db_lua_test). + */ + +#ifndef OGRE_STUB_H +#define OGRE_STUB_H + +#include +#include +#include +#include +#include + +namespace Ogre +{ + +// String is just std::string +using String = std::string; + +// Minimal Vector3 for GoapBlackboard +struct Vector3 { + float x, y, z; + Vector3() + : x(0) + , y(0) + , z(0) + { + } + Vector3(float x_, float y_, float z_) + : x(x_) + , y(y_) + , z(z_) + { + } + static const Vector3 ZERO; + + // Member operator== defined inline inside the struct body + // This ensures it's found by ADL for std::pair::operator== + bool operator==(const Vector3 &other) const + { + return x == other.x && y == other.y && z == other.z; + } +}; + +inline const Vector3 Vector3::ZERO(0, 0, 0); + +// Minimal StringConverter stub for GoapBlackboard::dump() +struct StringConverter { + static String toString(int val) + { + return std::to_string(val); + } + static String toString(float val) + { + return std::to_string(val); + } +}; + +// Minimal LogManager stub (used by LuaActionApi) +class LogManager { +public: + static LogManager &getSingleton() + { + static LogManager instance; + return instance; + } + + class Stream { + public: + template Stream &operator<<(const T &) + { + return *this; + } + }; + + Stream stream() + { + return Stream(); + } +}; + +} // namespace Ogre + +#endif // OGRE_STUB_H diff --git a/src/features/editScene/tests/OgreLogManager.h b/src/features/editScene/tests/OgreLogManager.h new file mode 100644 index 0000000..15daa60 --- /dev/null +++ b/src/features/editScene/tests/OgreLogManager.h @@ -0,0 +1,14 @@ +/** + * @file OgreLogManager.h + * @brief Stub OgreLogManager.h for standalone tests. + * + * Provides Ogre::LogManager for LuaActionApi.cpp compilation + * in standalone test builds. + */ + +#ifndef OGRE_LOG_MANAGER_H +#define OGRE_LOG_MANAGER_H + +#include "Ogre.h" + +#endif // OGRE_LOG_MANAGER_H diff --git a/src/features/editScene/tests/action_db_lua_test.cpp b/src/features/editScene/tests/action_db_lua_test.cpp new file mode 100644 index 0000000..c618c5f --- /dev/null +++ b/src/features/editScene/tests/action_db_lua_test.cpp @@ -0,0 +1,895 @@ +/** + * @file action_db_lua_test.cpp + * @brief Compile-time test for ActionDatabase Lua API. + * + * This test creates a Lua state, registers the ActionDatabase singleton + * and the Lua action API, then runs Lua scripts that create actions + * (including with behavior trees), goals, queries them, and verifies + * the results match expectations. + * + * Build with: + * g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \ + * action_db_lua_test.cpp \ + * ../components/ActionDatabase.cpp \ + * ../components/GoapBlackboard.cpp \ + * ../components/GoapGoal.cpp \ + * ../components/GoapAction.cpp \ + * ../../lua/lua-5.4.8/src/liblua.a \ + * -o action_db_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/ActionDatabase.hpp" +#include "../components/GoapBlackboard.hpp" +#include "../components/GoapAction.hpp" +#include "../components/GoapGoal.hpp" +#include "../components/BehaviorTree.hpp" + +// Forward declare the registration function +namespace editScene +{ +void registerLuaActionApi(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; +} + +// --------------------------------------------------------------------------- +// Test 1: Basic action creation and lookup +// --------------------------------------------------------------------------- + +static int testBasicAction(lua_State *L) +{ + TEST("create and find a simple action"); + + // Clear any previous state + ActionDatabase::getSingleton().clear(); + + // Create action via Lua + bool ok = runLua( + L, + "ecs.action_db.add_action('test_action', 5, " + " { bits = { has_axe = true }, values = { stamina = 10 } }, " + " { bits = { has_wood = true }, values = { stamina = -5 } }" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + // Verify via C++ API + const GoapAction *action = + ActionDatabase::getSingleton().findAction("test_action"); + if (!action) + FAIL("action not found"); + if (action->name != "test_action") + FAIL("wrong name"); + if (action->cost != 5) + FAIL("wrong cost"); + + // Verify preconditions + if (!action->preconditions.getBit(0)) + FAIL("has_axe bit not set"); + if (action->preconditions.values.at("stamina") != 10) + FAIL("wrong stamina precondition"); + + // Verify effects + if (!action->effects.getBit(1)) + FAIL("has_wood bit not set"); + if (action->effects.values.at("stamina") != -5) + FAIL("wrong stamina effect"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 2: Action with behavior tree +// --------------------------------------------------------------------------- + +static int testActionWithBehaviorTree(lua_State *L) +{ + TEST("create action with behavior tree"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua(L, + "ecs.action_db.add_action('wave', 1, {}, {}, {" + " type = 'sequence'," + " children = {" + " { type = 'setAnimationState', name = 'SM/Wave' }," + " { type = 'delay', params = '2.0' }," + " { type = 'setAnimationState', name = 'SM/Idle' }" + " }" + "})"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapAction *action = + ActionDatabase::getSingleton().findAction("wave"); + if (!action) + FAIL("action not found"); + + // Verify behavior tree structure + const BehaviorTreeNode &bt = action->behaviorTree; + if (bt.type != "sequence") + FAIL("expected sequence root"); + if (bt.children.size() != 3) + FAIL("expected 3 children"); + + if (bt.children[0].type != "setAnimationState") + FAIL("child 0 wrong type"); + if (bt.children[0].name != "SM/Wave") + FAIL("child 0 wrong name"); + + if (bt.children[1].type != "delay") + FAIL("child 1 wrong type"); + if (bt.children[1].params != "2.0") + FAIL("child 1 wrong params"); + + if (bt.children[2].type != "setAnimationState") + FAIL("child 2 wrong type"); + if (bt.children[2].name != "SM/Idle") + FAIL("child 2 wrong name"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 3: Nested behavior tree (selector with sequences) +// --------------------------------------------------------------------------- + +static int testNestedBehaviorTree(lua_State *L) +{ + TEST("create action with nested behavior tree"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua( + L, + "ecs.action_db.add_action('chop_tree', 3," + " { bits = { near_tree = true }, values = { stamina = 15 } }," + " { bits = { has_wood = true }, values = { stamina = -8 } }," + " {" + " type = 'selector'," + " children = {" + " {" + " type = 'sequence'," + " children = {" + " { type = 'checkBit', name = 'has_axe', params = '1' }," + " { type = 'setAnimationState', name = 'SM/Chop' }," + " { type = 'delay', params = '3.0' }" + " }" + " }," + " {" + " type = 'sequence'," + " children = {" + " { type = 'debugPrint', name = 'No axe!' }," + " { type = 'setAnimationState', name = 'SM/Pickup' }" + " }" + " }" + " }" + " }" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapAction *action = + ActionDatabase::getSingleton().findAction("chop_tree"); + if (!action) + FAIL("action not found"); + + const BehaviorTreeNode &bt = action->behaviorTree; + if (bt.type != "selector") + FAIL("expected selector root"); + if (bt.children.size() != 2) + FAIL("expected 2 children"); + + // First child: sequence with 3 nodes + const BehaviorTreeNode &seq1 = bt.children[0]; + if (seq1.type != "sequence") + FAIL("child 0 should be sequence"); + if (seq1.children.size() != 3) + FAIL("seq1 expected 3 children"); + if (seq1.children[0].type != "checkBit") + FAIL("seq1 child 0 wrong type"); + if (seq1.children[0].name != "has_axe") + FAIL("seq1 child 0 wrong name"); + if (seq1.children[0].params != "1") + FAIL("seq1 child 0 wrong params"); + + // Second child: sequence with 2 nodes + const BehaviorTreeNode &seq2 = bt.children[1]; + if (seq2.type != "sequence") + FAIL("child 1 should be sequence"); + if (seq2.children.size() != 2) + FAIL("seq2 expected 2 children"); + if (seq2.children[0].type != "debugPrint") + FAIL("seq2 child 0 wrong type"); + if (seq2.children[0].name != "No axe!") + FAIL("seq2 child 0 wrong name"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 4: Goal creation and lookup +// --------------------------------------------------------------------------- + +static int testGoal(lua_State *L) +{ + TEST("create and find a goal"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua(L, "ecs.action_db.add_goal('test_goal', 80," + " { values = { health = 100, stamina = 80 } }," + " 'health < 50 || stamina < 30'" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapGoal *goal = + ActionDatabase::getSingleton().findGoal("test_goal"); + if (!goal) + FAIL("goal not found"); + if (goal->name != "test_goal") + FAIL("wrong name"); + if (goal->priority != 80) + FAIL("wrong priority"); + if (goal->condition != "health < 50 || stamina < 30") + FAIL("wrong condition"); + if (goal->target.values.at("health") != 100) + FAIL("wrong health target"); + if (goal->target.values.at("stamina") != 80) + FAIL("wrong stamina target"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 5: Action replacement (same name) +// --------------------------------------------------------------------------- + +static int testActionReplacement(lua_State *L) +{ + TEST("replace action with same name"); + + ActionDatabase::getSingleton().clear(); + + // Create initial action + runLua(L, + "ecs.action_db.add_action('replace_me', 1, " + "{ bits = { old_flag = true } }, { bits = { old_done = true } })"); + + // Verify initial + const GoapAction *first = + ActionDatabase::getSingleton().findAction("replace_me"); + if (!first) + FAIL("initial action not found"); + if (first->cost != 1) + FAIL("initial cost wrong"); + + // Replace with new definition + runLua(L, + "ecs.action_db.add_action('replace_me', 99, " + "{ bits = { new_flag = true } }, { bits = { new_done = true } })"); + + // Verify replacement + const GoapAction *second = + ActionDatabase::getSingleton().findAction("replace_me"); + if (!second) + FAIL("replaced action not found"); + if (second->cost != 99) + FAIL("replaced cost wrong"); + if (second->preconditions.getBit(0)) + FAIL("old precondition still present"); + + // Should only be one action with this name + int count = 0; + for (const auto &a : ActionDatabase::getSingleton().actions) { + if (a.name == "replace_me") + count++; + } + if (count != 1) + FAIL("expected exactly one action with this name"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 6: Remove action +// --------------------------------------------------------------------------- + +static int testRemoveAction(lua_State *L) +{ + TEST("remove action"); + + ActionDatabase::getSingleton().clear(); + + runLua(L, "ecs.action_db.add_action('to_remove', 1)"); + runLua(L, "ecs.action_db.add_action('to_keep', 2)"); + + if (!ActionDatabase::getSingleton().findAction("to_remove")) + FAIL("action should exist before removal"); + + // Remove via Lua + bool ok = runLua(L, + "local r = ecs.action_db.remove_action('to_remove');" + "assert(r == true, 'remove should return true')"); + if (!ok) + FAIL("failed to run Lua remove"); + + if (ActionDatabase::getSingleton().findAction("to_remove")) + FAIL("action should not exist after removal"); + if (!ActionDatabase::getSingleton().findAction("to_keep")) + FAIL("other action should still exist"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 7: List actions +// --------------------------------------------------------------------------- + +static int testListActions(lua_State *L) +{ + TEST("list actions"); + + ActionDatabase::getSingleton().clear(); + + runLua(L, "ecs.action_db.add_action('alpha', 1)"); + runLua(L, "ecs.action_db.add_action('beta', 2)"); + runLua(L, "ecs.action_db.add_action('gamma', 3)"); + + bool ok = runLua( + L, "local list = ecs.action_db.list_actions();" + "assert(#list == 3, 'expected 3 actions, got ' .. #list);" + "assert(list[1] == 'alpha', 'expected alpha first');" + "assert(list[2] == 'beta', 'expected beta second');" + "assert(list[3] == 'gamma', 'expected gamma third')"); + if (!ok) + FAIL("list actions assertion failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 8: Find action via Lua (returns table) +// --------------------------------------------------------------------------- + +static int testFindActionLua(lua_State *L) +{ + TEST("find action from Lua returns table"); + + ActionDatabase::getSingleton().clear(); + + runLua(L, "ecs.action_db.add_action('find_me', 7," + " { values = { x = 42 } }," + " { values = { y = 99 } }," + " { type = 'task', name = 'do_something' }" + ")"); + + bool ok = runLua( + L, + "local a = ecs.action_db.find_action('find_me');" + "assert(a ~= nil, 'action should exist');" + "assert(a.name == 'find_me', 'wrong name');" + "assert(a.cost == 7, 'wrong cost');" + "assert(a.preconditions.values.x == 42, 'wrong precond');" + "assert(a.effects.values.y == 99, 'wrong effects');" + "assert(a.behaviorTree ~= nil, 'should have behaviorTree');" + "assert(a.behaviorTree.type == 'task', 'wrong bt type');" + "assert(a.behaviorTree.name == 'do_something', 'wrong bt name')"); + if (!ok) + FAIL("find action Lua assertions failed"); + + // Test nil for non-existent + ok = runLua(L, "local a = ecs.action_db.find_action('nonexistent');" + "assert(a == nil, 'nonexistent should be nil')"); + if (!ok) + FAIL("nonexistent action should be nil"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 9: Clear all +// --------------------------------------------------------------------------- + +static int testClear(lua_State *L) +{ + TEST("clear all actions and goals"); + + ActionDatabase::getSingleton().clear(); + + runLua(L, "ecs.action_db.add_action('a1', 1)"); + runLua(L, "ecs.action_db.add_action('a2', 2)"); + runLua(L, "ecs.action_db.add_goal('g1', 10)"); + + if (ActionDatabase::getSingleton().actions.size() != 2) + FAIL("expected 2 actions before clear"); + if (ActionDatabase::getSingleton().goals.size() != 1) + FAIL("expected 1 goal before clear"); + + runLua(L, "ecs.action_db.clear()"); + + if (ActionDatabase::getSingleton().actions.size() != 0) + FAIL("expected 0 actions after clear"); + if (ActionDatabase::getSingleton().goals.size() != 0) + FAIL("expected 0 goals after clear"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 10: Action with inventory behavior tree nodes +// --------------------------------------------------------------------------- + +static int testInventoryBehaviorTree(lua_State *L) +{ + TEST("action with inventory behavior tree nodes"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua( + L, "ecs.action_db.add_action('gather_wood', 2," + " { bits = { near_forest = true } }," + " { values = { wood_count = 3 } }," + " {" + " type = 'sequence'," + " children = {" + " { type = 'setAnimationState', name = 'SM/Gather' }," + " { type = 'delay', params = '2.0' }," + " { type = 'addItemToInventory'," + " params = 'wood,Firewood,material,3,1.0,0' }," + " { type = 'setAnimationState', name = 'SM/Idle' }" + " }" + " }" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapAction *action = + ActionDatabase::getSingleton().findAction("gather_wood"); + if (!action) + FAIL("action not found"); + + const BehaviorTreeNode &bt = action->behaviorTree; + if (bt.type != "sequence") + FAIL("expected sequence"); + if (bt.children.size() != 4) + FAIL("expected 4 children"); + if (bt.children[2].type != "addItemToInventory") + FAIL("wrong node type"); + if (bt.children[2].params != "wood,Firewood,material,3,1.0,0") + FAIL("wrong inventory params"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 11: Action with teleport/disablePhysics behavior tree +// --------------------------------------------------------------------------- + +static int testPhysicsBehaviorTree(lua_State *L) +{ + TEST("action with physics behavior tree nodes"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua( + L, "ecs.action_db.add_action('sit_on_chair', 1," + " { bits = { near_chair = true } }," + " { bits = { is_sitting = true } }," + " {" + " type = 'sequence'," + " children = {" + " { type = 'teleportToChild', name = 'SitTarget' }," + " { type = 'disablePhysics' }," + " { type = 'setAnimationState', name = 'SM/Sit' }," + " { type = 'delay', params = '5.0' }," + " { type = 'setAnimationState', name = 'SM/Stand' }," + " { type = 'enablePhysics' }" + " }" + " }" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapAction *action = + ActionDatabase::getSingleton().findAction("sit_on_chair"); + if (!action) + FAIL("action not found"); + + const BehaviorTreeNode &bt = action->behaviorTree; + if (bt.children.size() != 6) + FAIL("expected 6 children"); + if (bt.children[0].type != "teleportToChild") + FAIL("wrong child 0 type"); + if (bt.children[0].name != "SitTarget") + FAIL("wrong child 0 name"); + if (bt.children[1].type != "disablePhysics") + FAIL("wrong child 1 type"); + if (bt.children[4].type != "setAnimationState") + FAIL("wrong child 4 type"); + if (bt.children[4].name != "SM/Stand") + FAIL("wrong child 4 name"); + if (bt.children[5].type != "enablePhysics") + FAIL("wrong child 5 type"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: Action with blackboard check nodes +// --------------------------------------------------------------------------- + +static int testBlackboardCheckBehaviorTree(lua_State *L) +{ + TEST("action with blackboard check behavior tree nodes"); + + ActionDatabase::getSingleton().clear(); + + bool ok = runLua( + L, + "ecs.action_db.add_action('travel_to_market', 5," + " { bits = { is_awake = true }, values = { energy = 20 } }," + " { bits = { at_market = true }, values = { energy = -15 } }," + " {" + " type = 'sequence'," + " children = {" + " { type = 'checkValue', name = 'energy', params = '>= 20' }," + " { type = 'setAnimationState', name = 'SM/Walk' }," + " { type = 'delay', params = '5.0' }," + " { type = 'setAnimationState', name = 'SM/Idle' }," + " { type = 'setBit', name = 'at_market', params = '1' }," + " { type = 'setBit', name = 'at_home', params = '0' }" + " }" + " }" + ")"); + if (!ok) + FAIL("failed to run Lua"); + + const GoapAction *action = + ActionDatabase::getSingleton().findAction("travel_to_market"); + if (!action) + FAIL("action not found"); + + const BehaviorTreeNode &bt = action->behaviorTree; + if (bt.children.size() != 6) + FAIL("expected 6 children"); + if (bt.children[0].type != "checkValue") + FAIL("wrong child 0 type"); + if (bt.children[0].name != "energy") + FAIL("wrong child 0 name"); + if (bt.children[0].params != ">= 20") + FAIL("wrong child 0 params"); + if (bt.children[4].type != "setBit") + FAIL("wrong child 4 type"); + if (bt.children[4].name != "at_market") + FAIL("wrong child 4 name"); + if (bt.children[4].params != "1") + FAIL("wrong child 4 params"); + if (bt.children[5].type != "setBit") + FAIL("wrong child 5 type"); + if (bt.children[5].name != "at_home") + FAIL("wrong child 5 name"); + if (bt.children[5].params != "0") + FAIL("wrong child 5 params"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 13: Set and get bit names from Lua +// --------------------------------------------------------------------------- + +static int testSetGetBitName(lua_State *L) +{ + TEST("set and get bit names from Lua"); + + // Clear any previous bit names + for (int i = 0; i < 64; i++) + GoapBlackboard::setBitName(i, ""); + + // Set bit names via Lua + bool ok = runLua(L, "ecs.action_db.set_bit_name(0, 'has_axe');" + "ecs.action_db.set_bit_name(1, 'has_wood');" + "ecs.action_db.set_bit_name(5, 'is_hungry')"); + if (!ok) + FAIL("failed to set bit names"); + + // Verify via C++ API + const char *name0 = GoapBlackboard::getBitName(0); + if (!name0 || strcmp(name0, "has_axe") != 0) + FAIL("bit 0 should be 'has_axe'"); + + const char *name1 = GoapBlackboard::getBitName(1); + if (!name1 || strcmp(name1, "has_wood") != 0) + FAIL("bit 1 should be 'has_wood'"); + + const char *name5 = GoapBlackboard::getBitName(5); + if (!name5 || strcmp(name5, "is_hungry") != 0) + FAIL("bit 5 should be 'is_hungry'"); + + // Verify unset bits are null + if (GoapBlackboard::getBitName(2) != nullptr) + FAIL("bit 2 should be unset"); + + // Verify via Lua get_bit_name + ok = runLua( + L, + "local n0 = ecs.action_db.get_bit_name(0);" + "assert(n0 == 'has_axe', 'expected has_axe, got ' .. tostring(n0));" + "local n1 = ecs.action_db.get_bit_name(1);" + "assert(n1 == 'has_wood', 'expected has_wood, got ' .. tostring(n1));" + "local n5 = ecs.action_db.get_bit_name(5);" + "assert(n5 == 'is_hungry', 'expected is_hungry, got ' .. tostring(n5));" + "local n2 = ecs.action_db.get_bit_name(2);" + "assert(n2 == nil, 'bit 2 should be nil, got ' .. tostring(n2))"); + if (!ok) + FAIL("get_bit_name assertions failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 14: Find bit by name from Lua +// --------------------------------------------------------------------------- + +static int testFindBitByName(lua_State *L) +{ + TEST("find bit by name from Lua"); + + // Clear and set known bits + for (int i = 0; i < 64; i++) + GoapBlackboard::setBitName(i, ""); + GoapBlackboard::setBitName(3, "near_tree"); + GoapBlackboard::setBitName(7, "near_fire"); + + bool ok = runLua( + L, + "local idx3 = ecs.action_db.find_bit_by_name('near_tree');" + "assert(idx3 == 3, 'expected 3, got ' .. tostring(idx3));" + "local idx7 = ecs.action_db.find_bit_by_name('near_fire');" + "assert(idx7 == 7, 'expected 7, got ' .. tostring(idx7));" + "local nilIdx = ecs.action_db.find_bit_by_name('nonexistent');" + "assert(nilIdx == nil, 'expected nil, got ' .. tostring(nilIdx))"); + if (!ok) + FAIL("find_bit_by_name assertions failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 15: Auto-assign bit from Lua +// --------------------------------------------------------------------------- + +static int testAutoAssignBit(lua_State *L) +{ + TEST("auto-assign bit from Lua"); + + // Clear all bits + for (int i = 0; i < 64; i++) + GoapBlackboard::setBitName(i, ""); + + // Auto-assign a new name + bool ok = runLua( + L, + "local idx = ecs.action_db.auto_assign_bit('my_flag');" + "assert(idx == 0, 'expected first free slot 0, got ' .. tostring(idx));" + "local name = ecs.action_db.get_bit_name(0);" + "assert(name == 'my_flag', 'expected my_flag, got ' .. tostring(name))"); + if (!ok) + FAIL("first auto_assign failed"); + + // Auto-assign another - should get slot 1 + ok = runLua( + L, + "local idx = ecs.action_db.auto_assign_bit('other_flag');" + "assert(idx == 1, 'expected slot 1, got ' .. tostring(idx))"); + if (!ok) + FAIL("second auto_assign failed"); + + // Auto-assign an already-existing name - should return existing index + ok = runLua( + L, + "local idx = ecs.action_db.auto_assign_bit('my_flag');" + "assert(idx == 0, 'expected existing slot 0, got ' .. tostring(idx))"); + if (!ok) + FAIL("re-assign existing name failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 16: List bit names from Lua +// --------------------------------------------------------------------------- + +static int testListBitNames(lua_State *L) +{ + TEST("list bit names from Lua"); + + // Clear and set known bits + for (int i = 0; i < 64; i++) + GoapBlackboard::setBitName(i, ""); + GoapBlackboard::setBitName(0, "alpha"); + GoapBlackboard::setBitName(5, "beta"); + GoapBlackboard::setBitName(10, "gamma"); + + bool ok = runLua( + L, + "local bits = ecs.action_db.list_bit_names();" + "assert(#bits == 3, 'expected 3 bits, got ' .. #bits);" + "assert(bits[1].index == 0 and bits[1].name == 'alpha', 'wrong bit 0');" + "assert(bits[2].index == 5 and bits[2].name == 'beta', 'wrong bit 5');" + "assert(bits[3].index == 10 and bits[3].name == 'gamma', 'wrong bit 10')"); + if (!ok) + FAIL("list_bit_names assertions failed"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 17: Bit names auto-assigned when used in action preconditions +// --------------------------------------------------------------------------- + +static int testAutoAssignInAction(lua_State *L) +{ + TEST("bit names auto-assigned in action preconditions"); + + // Clear all bits + for (int i = 0; i < 64; i++) + GoapBlackboard::setBitName(i, ""); + + // Create an action with a bit name that hasn't been defined yet + bool ok = runLua(L, "ecs.action_db.add_action('test_auto_bit', 1," + " { bits = { brand_new_bit = true } }," + " { bits = { another_new_bit = true } })"); + if (!ok) + FAIL("failed to create action with auto-assigned bits"); + + // The bits should have been auto-assigned + int idx1 = GoapBlackboard::findBitByName("brand_new_bit"); + if (idx1 < 0) + FAIL("brand_new_bit should have been auto-assigned"); + if (idx1 != 0) + FAIL("brand_new_bit should be at slot 0"); + + int idx2 = GoapBlackboard::findBitByName("another_new_bit"); + if (idx2 < 0) + FAIL("another_new_bit should have been auto-assigned"); + if (idx2 != 1) + FAIL("another_new_bit should be at slot 1"); + + // Verify the action's preconditions use the correct bit + const GoapAction *action = + ActionDatabase::getSingleton().findAction("test_auto_bit"); + if (!action) + FAIL("action not found"); + if (!action->preconditions.getBit(idx1)) + FAIL("precondition bit should be set"); + if (!action->effects.getBit(idx2)) + FAIL("effect bit should be set"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +int main() +{ + printf("ActionDatabase 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 action API + editScene::registerLuaActionApi(L); + + // Run tests + int failures = 0; + failures += testBasicAction(L); + failures += testActionWithBehaviorTree(L); + failures += testNestedBehaviorTree(L); + failures += testGoal(L); + failures += testActionReplacement(L); + failures += testRemoveAction(L); + failures += testListActions(L); + failures += testFindActionLua(L); + failures += testClear(L); + failures += testInventoryBehaviorTree(L); + failures += testPhysicsBehaviorTree(L); + failures += testBlackboardCheckBehaviorTree(L); + failures += testSetGetBitName(L); + failures += testFindBitByName(L); + failures += testAutoAssignBit(L); + failures += testListBitNames(L); + failures += testAutoAssignInAction(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/tests/ogre_stub.h b/src/features/editScene/tests/ogre_stub.h new file mode 100644 index 0000000..0f51090 --- /dev/null +++ b/src/features/editScene/tests/ogre_stub.h @@ -0,0 +1,77 @@ +/** + * @file ogre_stub.h + * @brief Minimal Ogre stub for standalone tests. + * + * Provides just enough Ogre type aliases to compile the + * ActionDatabase, GoapBlackboard, GoapGoal, GoapAction, + * and BehaviorTree components without the full Ogre SDK. + * + * Only for use in standalone tests (action_db_lua_test). + */ + +#ifndef OGRE_STUB_H +#define OGRE_STUB_H + +#include +#include +#include + +namespace Ogre +{ + +// String is just std::string +using String = std::string; + +// Minimal Vector3 for GoapBlackboard +struct Vector3 { + float x, y, z; + Vector3() + : x(0) + , y(0) + , z(0) + { + } + Vector3(float x_, float y_, float z_) + : x(x_) + , y(y_) + , z(z_) + { + } + static const Vector3 ZERO; + + // Member operator== needed by std::pair::operator== when comparing + // unordered_map in GoapBlackboard::operator== + bool operator==(const Vector3 &other) const + { + return x == other.x && y == other.y && z == other.z; + } +}; + +inline const Vector3 Vector3::ZERO(0, 0, 0); + +// Minimal LogManager stub (used by LuaActionApi) +class LogManager { +public: + static LogManager &getSingleton() + { + static LogManager instance; + return instance; + } + + class Stream { + public: + template Stream &operator<<(const T &) + { + return *this; + } + }; + + Stream stream() + { + return Stream(); + } +}; + +} // namespace Ogre + +#endif // OGRE_STUB_H diff --git a/src/features/editScene/ui/ActionDatabaseEditor.cpp b/src/features/editScene/ui/ActionDatabaseEditor.cpp index f0692b6..be12582 100644 --- a/src/features/editScene/ui/ActionDatabaseEditor.cpp +++ b/src/features/editScene/ui/ActionDatabaseEditor.cpp @@ -4,7 +4,7 @@ #include bool ActionDatabaseEditor::renderComponent(flecs::entity entity, - ActionDatabase &db) + ActionDatabaseComponent &db) { bool modified = false; (void)entity; @@ -14,15 +14,18 @@ bool ActionDatabaseEditor::renderComponent(flecs::entity entity, ImGuiTreeNodeFlags_DefaultOpen)) modified |= renderBitNames(); - if (ImGui::CollapsingHeader("Actions", - ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen)) renderActions(db); - if (ImGui::CollapsingHeader("Goals", - ImGuiTreeNodeFlags_DefaultOpen)) + if (ImGui::CollapsingHeader("Goals", ImGuiTreeNodeFlags_DefaultOpen)) renderGoals(db); ImGui::PopID(); + + // Sync to singleton if modified + if (modified) + db.syncToSingleton(); + return modified; } @@ -53,7 +56,7 @@ bool ActionDatabaseEditor::renderBitNames() return changed; } -void ActionDatabaseEditor::renderActions(ActionDatabase &db) +void ActionDatabaseEditor::renderActions(ActionDatabaseComponent &db) { ImGui::Indent(); @@ -71,6 +74,7 @@ void ActionDatabaseEditor::renderActions(ActionDatabase &db) ImGui::SameLine(); if (ImGui::SmallButton("X")) { db.actions.erase(db.actions.begin() + i); + db.syncToSingleton(); if (m_selectedAction == (int)i) m_selectedAction = -1; else if (m_selectedAction > (int)i) @@ -86,18 +90,20 @@ void ActionDatabaseEditor::renderActions(ActionDatabase &db) snprintf(newName, sizeof(newName), "Action_%zu", db.actions.size()); db.actions.emplace_back(newName); + db.syncToSingleton(); m_selectedAction = (int)db.actions.size() - 1; } if (m_selectedAction >= 0 && m_selectedAction < (int)db.actions.size()) { renderActionEditor(db.actions[m_selectedAction]); + db.syncToSingleton(); } ImGui::Unindent(); } -void ActionDatabaseEditor::renderGoals(ActionDatabase &db) +void ActionDatabaseEditor::renderGoals(ActionDatabaseComponent &db) { ImGui::Indent(); @@ -115,6 +121,7 @@ void ActionDatabaseEditor::renderGoals(ActionDatabase &db) ImGui::SameLine(); if (ImGui::SmallButton("X")) { db.goals.erase(db.goals.begin() + i); + db.syncToSingleton(); if (m_selectedGoal == (int)i) m_selectedGoal = -1; else if (m_selectedGoal > (int)i) @@ -127,15 +134,15 @@ void ActionDatabaseEditor::renderGoals(ActionDatabase &db) if (ImGui::Button("Add Goal")) { char newName[64]; - snprintf(newName, sizeof(newName), "Goal_%zu", - db.goals.size()); + snprintf(newName, sizeof(newName), "Goal_%zu", db.goals.size()); db.goals.emplace_back(newName); + db.syncToSingleton(); m_selectedGoal = (int)db.goals.size() - 1; } - if (m_selectedGoal >= 0 && - m_selectedGoal < (int)db.goals.size()) { + if (m_selectedGoal >= 0 && m_selectedGoal < (int)db.goals.size()) { renderGoalEditor(db.goals[m_selectedGoal]); + db.syncToSingleton(); } ImGui::Unindent(); @@ -168,8 +175,7 @@ void ActionDatabaseEditor::renderActionEditor(GoapAction &action) } if (ImGui::TreeNode("Preconditions")) { - GoapBlackboardEditor::render(action.preconditions, - "precond"); + GoapBlackboardEditor::render(action.preconditions, "precond"); ImGui::TreePop(); } @@ -179,8 +185,7 @@ void ActionDatabaseEditor::renderActionEditor(GoapAction &action) 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; + bool enabled = (action.preconditionMask >> bit) & 1ULL; char label[8]; snprintf(label, sizeof(label), "%d", bit); if (j > 0) diff --git a/src/features/editScene/ui/ActionDatabaseEditor.hpp b/src/features/editScene/ui/ActionDatabaseEditor.hpp index 6e56950..ebb4530 100644 --- a/src/features/editScene/ui/ActionDatabaseEditor.hpp +++ b/src/features/editScene/ui/ActionDatabaseEditor.hpp @@ -6,21 +6,25 @@ #include "../components/ActionDatabase.hpp" /** - * Editor for ActionDatabase component. + * Editor for ActionDatabaseComponent. * * Allows editing the global list of GOAP actions and goals. + * Edits the component data and syncs to the ActionDatabase singleton. */ -class ActionDatabaseEditor : public ComponentEditor { +class ActionDatabaseEditor : public ComponentEditor { public: - const char *getName() const override { return "Action Database"; } + const char *getName() const override + { + return "Action Database"; + } protected: bool renderComponent(flecs::entity entity, - ActionDatabase &db) override; + ActionDatabaseComponent &db) override; private: - void renderActions(ActionDatabase &db); - void renderGoals(ActionDatabase &db); + void renderActions(ActionDatabaseComponent &db); + void renderGoals(ActionDatabaseComponent &db); void renderActionEditor(GoapAction &action); void renderGoalEditor(GoapGoal &goal); bool renderBitNames(); diff --git a/src/features/editScene/ui/ActionDebugEditor.cpp b/src/features/editScene/ui/ActionDebugEditor.cpp index 22b4c9b..207e929 100644 --- a/src/features/editScene/ui/ActionDebugEditor.cpp +++ b/src/features/editScene/ui/ActionDebugEditor.cpp @@ -7,14 +7,8 @@ ActionDatabase *ActionDebugEditor::findDatabase(flecs::entity entity) { - auto world = entity.world(); - ActionDatabase *db = nullptr; - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); - return db; + (void)entity; + return ActionDatabase::getSingletonPtr(); } bool ActionDebugEditor::renderComponent(flecs::entity entity, diff --git a/src/features/editScene/ui/ActuatorEditor.cpp b/src/features/editScene/ui/ActuatorEditor.cpp index df7bc01..4d39149 100644 --- a/src/features/editScene/ui/ActuatorEditor.cpp +++ b/src/features/editScene/ui/ActuatorEditor.cpp @@ -4,14 +4,8 @@ ActionDatabase *ActuatorEditor::findDatabase(flecs::entity entity) { - auto world = entity.world(); - ActionDatabase *db = nullptr; - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); - return db; + (void)entity; + return ActionDatabase::getSingletonPtr(); } bool ActuatorEditor::renderComponent(flecs::entity entity, diff --git a/src/features/editScene/ui/EventHandlerEditor.cpp b/src/features/editScene/ui/EventHandlerEditor.cpp index dece51c..af887f6 100644 --- a/src/features/editScene/ui/EventHandlerEditor.cpp +++ b/src/features/editScene/ui/EventHandlerEditor.cpp @@ -4,14 +4,8 @@ static ActionDatabase *findDatabase(flecs::entity entity) { - auto world = entity.world(); - ActionDatabase *db = nullptr; - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); - return db; + (void)entity; + return ActionDatabase::getSingletonPtr(); } bool EventHandlerEditor::renderComponent(flecs::entity entity, diff --git a/src/features/editScene/ui/GoapPlannerEditor.cpp b/src/features/editScene/ui/GoapPlannerEditor.cpp index a2bba17..c273db4 100644 --- a/src/features/editScene/ui/GoapPlannerEditor.cpp +++ b/src/features/editScene/ui/GoapPlannerEditor.cpp @@ -5,35 +5,9 @@ ActionDatabase *GoapPlannerEditor::findDatabase( flecs::entity entity, GoapPlannerComponent &planner) { - auto world = entity.world(); - - // 1. Try to find by referenced entity name - if (!planner.actionDatabaseRef.empty()) { - ActionDatabase *found = nullptr; - world.query() - .each([&](flecs::entity dbEntity, ActionDatabase &db) { - if (found) - return; - if (dbEntity.has()) { - auto &name = - dbEntity.get(); - if (name.name == - planner.actionDatabaseRef) - found = &db; - } - }); - if (found) - return found; - } - - // 2. Fall back to any ActionDatabase in the world - ActionDatabase *db = nullptr; - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); - return db; + (void)entity; + (void)planner; + return ActionDatabase::getSingletonPtr(); } bool GoapPlannerEditor::renderComponent(flecs::entity entity, diff --git a/src/features/editScene/ui/ItemEditor.cpp b/src/features/editScene/ui/ItemEditor.cpp index a8527db..6e69feb 100644 --- a/src/features/editScene/ui/ItemEditor.cpp +++ b/src/features/editScene/ui/ItemEditor.cpp @@ -83,14 +83,8 @@ bool ItemEditor::renderComponent(flecs::entity entity, ItemComponent &item) modified = true; } - // Pick from action database - ActionDatabase *db = nullptr; - auto world = entity.world(); - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); + // Pick from action database singleton + ActionDatabase *db = ActionDatabase::getSingletonPtr(); if (db && !db->actions.empty()) { static int selectedAction = -1; diff --git a/src/features/editScene/ui/SmartObjectEditor.cpp b/src/features/editScene/ui/SmartObjectEditor.cpp index e71b067..bcea4fc 100644 --- a/src/features/editScene/ui/SmartObjectEditor.cpp +++ b/src/features/editScene/ui/SmartObjectEditor.cpp @@ -3,14 +3,8 @@ ActionDatabase *SmartObjectEditor::findDatabase(flecs::entity entity) { - auto world = entity.world(); - ActionDatabase *db = nullptr; - world.query().each( - [&](flecs::entity, ActionDatabase &database) { - if (!db) - db = &database; - }); - return db; + (void)entity; + return ActionDatabase::getSingletonPtr(); } bool SmartObjectEditor::renderComponent(flecs::entity entity,