Lua action APIs
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<SkyboxComponent>();
|
||||
|
||||
// Register AI/GOAP components
|
||||
m_world.component<ActionDatabase>();
|
||||
// ActionDatabase is now a singleton, registered in ActionDatabaseModule
|
||||
m_world.component<ActionDebug>();
|
||||
m_world.component<BehaviorTreeComponent>();
|
||||
m_world.component<GoapBlackboard>();
|
||||
|
||||
@@ -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<const GoapAction *> ActionDatabase::getValidActions(
|
||||
const GoapBlackboard &blackboard) const
|
||||
std::vector<const GoapAction *>
|
||||
ActionDatabase::getValidActions(const GoapBlackboard &blackboard) const
|
||||
{
|
||||
std::vector<const GoapAction *> result;
|
||||
for (const auto &action : actions) {
|
||||
@@ -85,3 +138,27 @@ std::vector<const GoapAction *> 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);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,22 @@
|
||||
#include "GoapAction.hpp"
|
||||
#include "GoapGoal.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* 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<GoapAction> actions;
|
||||
std::vector<GoapGoal> 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<const GoapAction *> getValidActions(
|
||||
const GoapBlackboard &blackboard) const;
|
||||
std::vector<const GoapAction *>
|
||||
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<GoapAction> actions;
|
||||
std::vector<GoapGoal> goals;
|
||||
|
||||
/** Sync this component's data to the ActionDatabase singleton */
|
||||
void syncToSingleton() const;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DATABASE_HPP
|
||||
|
||||
@@ -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<ActionDatabase>(
|
||||
registry.registerComponent<ActionDatabaseComponent>(
|
||||
"Action Database", "AI",
|
||||
std::make_unique<ActionDatabaseEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<ActionDatabase>())
|
||||
e.set<ActionDatabase>({});
|
||||
if (!e.has<ActionDatabaseComponent>())
|
||||
e.set<ActionDatabaseComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<ActionDatabase>())
|
||||
e.remove<ActionDatabase>();
|
||||
if (e.has<ActionDatabaseComponent>())
|
||||
e.remove<ActionDatabaseComponent>();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ REGISTER_COMPONENT_GROUP("Behavior Tree", "AI", BehaviorTreeComponent,
|
||||
BehaviorTreeEditor)
|
||||
{
|
||||
registry.registerComponent<BehaviorTreeComponent>(
|
||||
"Behavior Tree", "AI",
|
||||
std::make_unique<BehaviorTreeEditor>(),
|
||||
"Behavior Tree", "AI", std::make_unique<BehaviorTreeEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<BehaviorTreeComponent>()) {
|
||||
|
||||
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
418
src/features/editScene/lua-examples/action_db_example.lua
Normal file
@@ -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
|
||||
-- =============================================================================
|
||||
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
576
src/features/editScene/lua/LuaActionApi.cpp
Normal file
@@ -0,0 +1,576 @@
|
||||
#include "LuaActionApi.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <cstring>
|
||||
|
||||
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
|
||||
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
101
src/features/editScene/lua/LuaActionApi.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#ifndef EDITSCENE_LUA_ACTION_API_HPP
|
||||
#define EDITSCENE_LUA_ACTION_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @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
|
||||
@@ -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(
|
||||
|
||||
@@ -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<ActionDatabase>().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;
|
||||
|
||||
|
||||
@@ -994,12 +994,7 @@ void BehaviorTreeSystem::update(float deltaTime)
|
||||
});
|
||||
|
||||
/* --- ActionDebug test runs --- */
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (!db)
|
||||
return;
|
||||
|
||||
|
||||
@@ -606,8 +606,7 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [Mat]";
|
||||
if (entity.has<AnimationTreeComponent>())
|
||||
indicators += " [Anim]";
|
||||
if (entity.has<ActionDatabase>())
|
||||
indicators += " [AI]";
|
||||
// ActionDatabase is now a singleton, not a per-entity component
|
||||
if (entity.has<ActionDebug>())
|
||||
indicators += " [Debug]";
|
||||
if (entity.has<BehaviorTreeComponent>())
|
||||
@@ -999,12 +998,7 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render ActionDatabase if present
|
||||
if (entity.has<ActionDatabase>()) {
|
||||
auto &db = entity.get_mut<ActionDatabase>();
|
||||
m_componentRegistry.render<ActionDatabase>(entity, db);
|
||||
componentCount++;
|
||||
}
|
||||
// ActionDatabase is now a singleton, not a per-entity component
|
||||
|
||||
// Render ActionDebug if present
|
||||
if (entity.has<ActionDebug>()) {
|
||||
|
||||
@@ -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<ActionDatabase>().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;
|
||||
|
||||
@@ -115,13 +115,8 @@ void GoapPlannerSystem::planForEntity(flecs::entity e,
|
||||
{
|
||||
(void)e;
|
||||
|
||||
// Find ActionDatabase
|
||||
const ActionDatabase *db = nullptr;
|
||||
m_world.query<ActionDatabase>().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);
|
||||
|
||||
@@ -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<ActionDatabase>().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<GoapBlackboard>()) {
|
||||
ActionDatabase *db = nullptr;
|
||||
m_world
|
||||
.query<ActionDatabase>()
|
||||
.each([&](flecs::entity,
|
||||
ActionDatabase
|
||||
&database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
if (db) {
|
||||
const GoapAction *action =
|
||||
db->findAction(
|
||||
|
||||
@@ -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<ActionDatabase>().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 =
|
||||
|
||||
@@ -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<ActionDatabase>()) {
|
||||
json["actionDatabase"] = serializeActionDatabase(entity);
|
||||
}
|
||||
// ActionDatabase is now a singleton, serialized at scene level
|
||||
if (entity.has<ActionDebug>()) {
|
||||
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<ActionDatabase>();
|
||||
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<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
entity.set<ActionDatabase>(db);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ActionDatabase>().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<ActionDatabase>().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
|
||||
|
||||
91
src/features/editScene/tests/Ogre.h
Normal file
91
src/features/editScene/tests/Ogre.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
|
||||
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 <typename T> Stream &operator<<(const T &)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Stream stream()
|
||||
{
|
||||
return Stream();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
#endif // OGRE_STUB_H
|
||||
14
src/features/editScene/tests/OgreLogManager.h
Normal file
14
src/features/editScene/tests/OgreLogManager.h
Normal file
@@ -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
|
||||
895
src/features/editScene/tests/action_db_lua_test.cpp
Normal file
895
src/features/editScene/tests/action_db_lua_test.cpp
Normal file
@@ -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 <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
// Must be included before any component headers that use Ogre types.
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// Include the components we need
|
||||
#include "../components/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;
|
||||
}
|
||||
77
src/features/editScene/tests/ogre_stub.h
Normal file
77
src/features/editScene/tests/ogre_stub.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
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<string, Vector3> 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 <typename T> Stream &operator<<(const T &)
|
||||
{
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Stream stream()
|
||||
{
|
||||
return Stream();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
#endif // OGRE_STUB_H
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <imgui.h>
|
||||
|
||||
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)
|
||||
|
||||
@@ -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<ActionDatabase> {
|
||||
class ActionDatabaseEditor : public ComponentEditor<ActionDatabaseComponent> {
|
||||
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();
|
||||
|
||||
@@ -7,14 +7,8 @@
|
||||
|
||||
ActionDatabase *ActionDebugEditor::findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
(void)entity;
|
||||
return ActionDatabase::getSingletonPtr();
|
||||
}
|
||||
|
||||
bool ActionDebugEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
|
||||
ActionDatabase *ActuatorEditor::findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
(void)entity;
|
||||
return ActionDatabase::getSingletonPtr();
|
||||
}
|
||||
|
||||
bool ActuatorEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
|
||||
static ActionDatabase *findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
(void)entity;
|
||||
return ActionDatabase::getSingletonPtr();
|
||||
}
|
||||
|
||||
bool EventHandlerEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
@@ -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<ActionDatabase>()
|
||||
.each([&](flecs::entity dbEntity, ActionDatabase &db) {
|
||||
if (found)
|
||||
return;
|
||||
if (dbEntity.has<EntityNameComponent>()) {
|
||||
auto &name =
|
||||
dbEntity.get<EntityNameComponent>();
|
||||
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<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
(void)entity;
|
||||
(void)planner;
|
||||
return ActionDatabase::getSingletonPtr();
|
||||
}
|
||||
|
||||
bool GoapPlannerEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
@@ -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<ActionDatabase>().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;
|
||||
|
||||
@@ -3,14 +3,8 @@
|
||||
|
||||
ActionDatabase *SmartObjectEditor::findDatabase(flecs::entity entity)
|
||||
{
|
||||
auto world = entity.world();
|
||||
ActionDatabase *db = nullptr;
|
||||
world.query<ActionDatabase>().each(
|
||||
[&](flecs::entity, ActionDatabase &database) {
|
||||
if (!db)
|
||||
db = &database;
|
||||
});
|
||||
return db;
|
||||
(void)entity;
|
||||
return ActionDatabase::getSingletonPtr();
|
||||
}
|
||||
|
||||
bool SmartObjectEditor::renderComponent(flecs::entity entity,
|
||||
|
||||
Reference in New Issue
Block a user