Lua-based behavior tree node
This commit is contained in:
@@ -107,6 +107,7 @@ set(EDITSCENE_SOURCES
|
||||
ui/InlineBehaviorTreeEditor.cpp
|
||||
ui/NavMeshEditor.cpp
|
||||
ui/ActionDatabaseEditor.cpp
|
||||
ui/ActionDatabaseSingletonEditor.cpp
|
||||
ui/ActionDebugEditor.cpp
|
||||
ui/ComponentRegistration.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
@@ -152,6 +153,7 @@ set(EDITSCENE_SOURCES
|
||||
lua/LuaComponentApi.cpp
|
||||
lua/LuaEventApi.cpp
|
||||
lua/LuaActionApi.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
)
|
||||
|
||||
set(EDITSCENE_HEADERS
|
||||
@@ -283,6 +285,7 @@ set(EDITSCENE_HEADERS
|
||||
ui/NavMeshEditor.hpp
|
||||
ui/NavMeshGeometrySourceEditor.hpp
|
||||
ui/ActionDatabaseEditor.hpp
|
||||
ui/ActionDatabaseSingletonEditor.hpp
|
||||
ui/ActionDebugEditor.hpp
|
||||
components/GoapBlackboard.hpp
|
||||
components/GoapExpression.hpp
|
||||
@@ -301,6 +304,7 @@ set(EDITSCENE_HEADERS
|
||||
lua/LuaComponentApi.hpp
|
||||
lua/LuaEventApi.hpp
|
||||
lua/LuaActionApi.hpp
|
||||
lua/LuaBehaviorTreeApi.hpp
|
||||
)
|
||||
|
||||
add_executable(editSceneEditor ${EDITSCENE_SOURCES} ${EDITSCENE_HEADERS})
|
||||
@@ -412,7 +416,27 @@ target_include_directories(action_db_lua_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
# Test: Behavior Tree Lua API
|
||||
add_executable(behavior_tree_lua_test
|
||||
tests/behavior_tree_lua_test.cpp
|
||||
lua/LuaBehaviorTreeApi.cpp
|
||||
lua/LuaEntityApi.cpp
|
||||
components/GoapBlackboard.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(behavior_tree_lua_test
|
||||
lua
|
||||
flecs::flecs_static
|
||||
OgreMain
|
||||
)
|
||||
|
||||
target_include_directories(behavior_tree_lua_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
target_compile_definitions(behavior_tree_lua_test PRIVATE flecs_STATIC)
|
||||
|
||||
# Copy local resources (materials, etc.)
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/resources")
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
#include "lua/LuaComponentApi.hpp"
|
||||
#include "lua/LuaEventApi.hpp"
|
||||
#include "lua/LuaActionApi.hpp"
|
||||
#include "lua/LuaBehaviorTreeApi.hpp"
|
||||
|
||||
//=============================================================================
|
||||
// ImGuiRenderListener Implementation
|
||||
@@ -506,6 +507,7 @@ void EditorApp::setup()
|
||||
editScene::registerLuaComponentApi(L);
|
||||
editScene::registerLuaEventApi(L);
|
||||
editScene::registerLuaActionApi(L);
|
||||
editScene::registerLuaBehaviorTreeApi(L);
|
||||
|
||||
// Run late setup: load data.lua and initial scripts.
|
||||
m_lua.lateSetup();
|
||||
|
||||
@@ -50,6 +50,15 @@
|
||||
* "addItemToInventory"- Leaf: adds an item directly to character's inventory
|
||||
* (for quest rewards, etc.).
|
||||
* params="itemId,itemName,itemType,count,weight,value"
|
||||
*
|
||||
* --- Lua node ---
|
||||
* "luaTask" - Leaf: calls a registered Lua function.
|
||||
* name = registered node handler name.
|
||||
* params = "key=val,key2=val2" passed to the Lua function.
|
||||
* The Lua function receives (entity_id, params_table)
|
||||
* and must return "success", "failure", or "running".
|
||||
* Register handlers via:
|
||||
* ecs.behavior_tree.register_node("name", function)
|
||||
*/
|
||||
struct BehaviorTreeNode {
|
||||
Ogre::String type = "task";
|
||||
@@ -95,7 +104,8 @@ struct BehaviorTreeNode {
|
||||
type == "sendEvent" || type == "hasItem" ||
|
||||
type == "hasItemByName" || type == "countItem" ||
|
||||
type == "pickupItem" || type == "dropItem" ||
|
||||
type == "useItem" || type == "addItemToInventory";
|
||||
type == "useItem" || type == "addItemToInventory" ||
|
||||
type == "luaTask";
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
316
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
316
src/features/editScene/lua-examples/behavior_tree_example.lua
Normal file
@@ -0,0 +1,316 @@
|
||||
-- =============================================================================
|
||||
-- Behavior Tree Lua API Examples
|
||||
-- =============================================================================
|
||||
-- This file demonstrates how to create custom behavior tree nodes using
|
||||
-- Lua functions via the ecs.behavior_tree API.
|
||||
--
|
||||
-- The API allows you to:
|
||||
-- 1. Register Lua functions as behavior tree node handlers
|
||||
-- 2. Create behavior tree nodes that invoke those handlers
|
||||
-- 3. Return "success", "failure", or "running" to control tree flow
|
||||
-- 4. Pass parameters from the behavior tree editor to your Lua function
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Registering Node Handlers
|
||||
-- =============================================================================
|
||||
-- Use ecs.behavior_tree.register_node(name, function) to register a Lua
|
||||
-- function as a behavior tree node handler.
|
||||
--
|
||||
-- The function receives two arguments:
|
||||
-- entity_id - The entity ID executing this behavior tree node
|
||||
-- params - A table of parameters parsed from the node's params string
|
||||
--
|
||||
-- The function must return one of:
|
||||
-- "success" - Node completed successfully (tree continues)
|
||||
-- "failure" - Node failed (tree stops with failure)
|
||||
-- "running" - Node is still running (will be called again next frame)
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 1: Simple greeting node
|
||||
-- =============================================================================
|
||||
-- This node just prints a message and succeeds immediately.
|
||||
|
||||
ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
local message = params.message or "Hello!"
|
||||
print("Entity " .. entity_id .. " says: " .. message)
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 2: Node that checks a blackboard value
|
||||
-- =============================================================================
|
||||
-- This node checks if a blackboard value meets a threshold.
|
||||
-- It succeeds if the check passes, fails otherwise.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local min_val = tonumber(params.min) or 0
|
||||
|
||||
if not key then
|
||||
print("[BT] check_blackboard_value: no 'key' param provided")
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
print("[BT] Entity " .. entity_id .. " has no GoapBlackboard")
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local value = bb.values[key]
|
||||
if value == nil then
|
||||
print("[BT] Blackboard has no key '" .. key .. "'")
|
||||
return "failure"
|
||||
end
|
||||
|
||||
if value >= min_val then
|
||||
print("[BT] Check passed: " .. key .. " = " .. value .. " >= " .. min_val)
|
||||
return "success"
|
||||
else
|
||||
print("[BT] Check failed: " .. key .. " = " .. value .. " < " .. min_val)
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 3: Node that runs over multiple frames (running state)
|
||||
-- =============================================================================
|
||||
-- This node waits for a specified duration, storing its progress in the
|
||||
-- blackboard's floatValues. It returns "running" each frame until done.
|
||||
|
||||
ecs.behavior_tree.register_node("wait_for_duration", function(entity_id, params)
|
||||
local duration = tonumber(params.duration) or 1.0
|
||||
local timer_key = params.timer_key or "wait_timer"
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
-- Get elapsed time from blackboard (or start at 0)
|
||||
local elapsed = bb.floatValues[timer_key] or 0.0
|
||||
|
||||
-- Get delta time (requires ecs.get_delta_time to be available)
|
||||
-- For now, use a fixed step approximation
|
||||
local dt = 0.016 -- ~60 FPS
|
||||
|
||||
elapsed = elapsed + dt
|
||||
bb.floatValues[timer_key] = elapsed
|
||||
|
||||
-- Save updated blackboard
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
|
||||
if elapsed >= duration then
|
||||
-- Done waiting - clean up timer
|
||||
bb.floatValues[timer_key] = nil
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
print("[BT] Wait complete after " .. duration .. " seconds")
|
||||
return "success"
|
||||
end
|
||||
|
||||
-- Still waiting
|
||||
return "running"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 4: Node that modifies blackboard values
|
||||
-- =============================================================================
|
||||
-- This node adds a value to a blackboard integer value.
|
||||
|
||||
ecs.behavior_tree.register_node("add_blackboard_value", function(entity_id, params)
|
||||
local key = params.key
|
||||
local amount = tonumber(params.amount) or 1
|
||||
|
||||
if not key then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.values[key] = (bb.values[key] or 0) + amount
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
|
||||
print("[BT] Added " .. amount .. " to " .. key .. " (now " .. bb.values[key] .. ")")
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 5: Node that sets a blackboard bit
|
||||
-- =============================================================================
|
||||
-- This node sets or clears a named bit in the blackboard.
|
||||
|
||||
ecs.behavior_tree.register_node("set_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local value = params.value == "1" or params.value == "true"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
bb.bits[bit_name] = value
|
||||
ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
|
||||
print("[BT] Set bit '" .. bit_name .. "' to " .. tostring(value))
|
||||
return "success"
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 6: Node that checks a blackboard bit
|
||||
-- =============================================================================
|
||||
-- This node checks if a named bit is set to a specific value.
|
||||
|
||||
ecs.behavior_tree.register_node("check_blackboard_bit", function(entity_id, params)
|
||||
local bit_name = params.bit
|
||||
local expected = params.value ~= "0" and params.value ~= "false"
|
||||
|
||||
if not bit_name then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
if not bb then
|
||||
return "failure"
|
||||
end
|
||||
|
||||
local actual = bb.bits[bit_name] == true
|
||||
if actual == expected then
|
||||
return "success"
|
||||
else
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Example 7: Random chance node
|
||||
-- =============================================================================
|
||||
-- This node succeeds with a configurable probability.
|
||||
|
||||
ecs.behavior_tree.register_node("random_chance", function(entity_id, params)
|
||||
local probability = tonumber(params.probability) or 0.5
|
||||
|
||||
-- Simple pseudo-random using math.random
|
||||
local roll = math.random()
|
||||
if roll < probability then
|
||||
print("[BT] Random chance succeeded (roll=" .. roll .. " < " .. probability .. ")")
|
||||
return "success"
|
||||
else
|
||||
print("[BT] Random chance failed (roll=" .. roll .. " >= " .. probability .. ")")
|
||||
return "failure"
|
||||
end
|
||||
end)
|
||||
|
||||
-- =============================================================================
|
||||
-- Using the Nodes in Behavior Trees
|
||||
-- =============================================================================
|
||||
-- Once registered, you can use ecs.behavior_tree.create_node(name, params)
|
||||
-- to create node tables for use in action behavior trees.
|
||||
--
|
||||
-- The params string uses "key=val,key2=val2" format:
|
||||
-- - Numbers (int or float) are parsed automatically
|
||||
-- - Quoted strings preserve string values: msg="hello world"
|
||||
-- - Unquoted non-numeric values are treated as strings
|
||||
-- =============================================================================
|
||||
|
||||
-- Example: Action that uses Lua behavior tree nodes
|
||||
ecs.action_db.add_action("lua_demo_greet", 1,
|
||||
{}, -- no preconditions
|
||||
{}, -- no effects
|
||||
{ -- behavior tree
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("say_hello", "message=Welcome to the game!"),
|
||||
ecs.behavior_tree.create_node("wait_for_duration", "duration=2.0,timer_key=greet_timer"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=How are you today?")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Example: Action with conditional logic using Lua nodes
|
||||
ecs.action_db.add_action("lua_demo_conditional", 2,
|
||||
{
|
||||
values = { experience = 10 }
|
||||
},
|
||||
{
|
||||
values = { experience = 20 }
|
||||
},
|
||||
{
|
||||
type = "selector",
|
||||
children = {
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("check_blackboard_value", "key=experience,min=50"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=You are experienced!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=10")
|
||||
}
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("say_hello", "message=You are still learning..."),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value", "key=experience,amount=5")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- Example: Action with random outcomes
|
||||
ecs.action_db.add_action("lua_demo_gamble", 3,
|
||||
{},
|
||||
{
|
||||
values = { gold = 10 }
|
||||
},
|
||||
{
|
||||
type = "sequence",
|
||||
children = {
|
||||
ecs.behavior_tree.create_node("random_chance", "probability=0.3"),
|
||||
ecs.behavior_tree.create_node("say_hello", "message=You won the gamble!"),
|
||||
ecs.behavior_tree.create_node("add_blackboard_value", "key=gold,amount=10")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
-- =============================================================================
|
||||
-- Managing Registered Nodes
|
||||
-- =============================================================================
|
||||
|
||||
-- List all registered node handlers:
|
||||
local nodes = ecs.behavior_tree.list_nodes()
|
||||
print("Registered behavior tree nodes:")
|
||||
for i, name in ipairs(nodes) do
|
||||
print(" " .. i .. ". " .. name)
|
||||
end
|
||||
|
||||
-- Unregister a node handler:
|
||||
-- local removed = ecs.behavior_tree.unregister_node("say_hello")
|
||||
-- if removed then
|
||||
-- print("Unregistered node: say_hello")
|
||||
-- end
|
||||
|
||||
-- =============================================================================
|
||||
-- Parameter Format Reference
|
||||
-- =============================================================================
|
||||
--
|
||||
-- The params string passed to create_node() supports:
|
||||
--
|
||||
-- Integer: "count=42" -> params.count = 42 (number)
|
||||
-- Float: "speed=3.5" -> params.speed = 3.5 (number)
|
||||
-- String: "msg=hello" -> params.msg = "hello" (string)
|
||||
-- Quoted: 'msg="hello world"' -> params.msg = "hello world" (string)
|
||||
-- Multiple: "key=val,count=5" -> params.key = "val", params.count = 5
|
||||
-- Empty: "" -> params = {} (empty table)
|
||||
--
|
||||
-- The params table is passed as the second argument to your registered
|
||||
-- Lua function handler.
|
||||
-- =============================================================================
|
||||
348
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
348
src/features/editScene/lua/LuaBehaviorTreeApi.cpp
Normal file
@@ -0,0 +1,348 @@
|
||||
#include "LuaBehaviorTreeApi.hpp"
|
||||
#include "LuaEntityApi.hpp"
|
||||
#include "../components/GoapBlackboard.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global registry of Lua node handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
// Maps node name -> Lua registry reference for the callback function.
|
||||
// The callback is stored as a Lua reference in the registry so it persists
|
||||
// across Lua state resets and can be called from C++.
|
||||
|
||||
static std::unordered_map<std::string, int> g_luaNodeHandlers;
|
||||
|
||||
// Global Lua state pointer, set during registerLuaBehaviorTreeApi().
|
||||
// Used by callLuaBehaviorTreeNode() which is invoked from the C++
|
||||
// behavior tree system without direct access to the Lua state.
|
||||
static lua_State *g_luaState = nullptr;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: push a GoapBlackboard as a Lua table (reused from LuaActionApi)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushBlackboard(lua_State *L, const GoapBlackboard &bb)
|
||||
{
|
||||
lua_newtable(L); // blackboard table
|
||||
|
||||
// Bits
|
||||
lua_newtable(L); // bits table
|
||||
for (int i = 0; i < 64; i++) {
|
||||
if (bb.hasBit(i)) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
const char *key = name ? name : "";
|
||||
lua_pushboolean(L, bb.getBit(i));
|
||||
lua_setfield(L, -2, key);
|
||||
}
|
||||
}
|
||||
lua_setfield(L, -2, "bits");
|
||||
|
||||
// Integer values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.values) {
|
||||
lua_pushinteger(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "values");
|
||||
|
||||
// Float values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.floatValues) {
|
||||
lua_pushnumber(L, kv.second);
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "floatValues");
|
||||
|
||||
// String values
|
||||
lua_newtable(L);
|
||||
for (const auto &kv : bb.stringValues) {
|
||||
lua_pushstring(L, kv.second.c_str());
|
||||
lua_setfield(L, -2, kv.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "stringValues");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: parse "key=val,key2=val2" params into a Lua table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static void pushParamsTable(lua_State *L, const std::string ¶ms)
|
||||
{
|
||||
lua_newtable(L); // params table
|
||||
|
||||
if (params.empty())
|
||||
return;
|
||||
|
||||
const char *s = params.c_str();
|
||||
while (*s) {
|
||||
// Skip whitespace
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
if (!*s)
|
||||
break;
|
||||
|
||||
// Find key
|
||||
const char *keyStart = s;
|
||||
while (*s && *s != '=' && *s != ',')
|
||||
s++;
|
||||
std::string key(keyStart, static_cast<size_t>(s - keyStart));
|
||||
// Trim trailing spaces from key
|
||||
while (!key.empty() &&
|
||||
(key.back() == ' ' || key.back() == '\t'))
|
||||
key.pop_back();
|
||||
|
||||
if (*s != '=') {
|
||||
while (*s && *s != ',')
|
||||
s++;
|
||||
if (*s == ',')
|
||||
s++;
|
||||
continue;
|
||||
}
|
||||
s++; // skip '='
|
||||
|
||||
// Skip whitespace before value
|
||||
while (*s == ' ' || *s == '\t')
|
||||
s++;
|
||||
|
||||
// Find value end (next comma or end)
|
||||
const char *valStart = s;
|
||||
bool inQuotes = false;
|
||||
while (*s && (*s != ',' || inQuotes)) {
|
||||
if (*s == '"')
|
||||
inQuotes = !inQuotes;
|
||||
s++;
|
||||
}
|
||||
std::string val(valStart, static_cast<size_t>(s - valStart));
|
||||
// Trim trailing spaces from value
|
||||
while (!val.empty() &&
|
||||
(val.back() == ' ' || val.back() == '\t'))
|
||||
val.pop_back();
|
||||
|
||||
// Strip quotes if present
|
||||
if (val.size() >= 2 && val.front() == '"' &&
|
||||
val.back() == '"') {
|
||||
val = val.substr(1, val.size() - 2);
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try numeric
|
||||
char *end = nullptr;
|
||||
long iVal = strtol(val.c_str(), &end, 10);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushinteger(L, (int)iVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Try float
|
||||
end = nullptr;
|
||||
float fVal = strtof(val.c_str(), &end);
|
||||
if (end != val.c_str() && *end == '\0') {
|
||||
lua_pushnumber(L, fVal);
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
} else {
|
||||
// Fallback to string
|
||||
lua_pushstring(L, val.c_str());
|
||||
lua_setfield(L, -2, key.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*s == ',')
|
||||
s++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: Call a registered Lua node handler
|
||||
// ---------------------------------------------------------------------------
|
||||
// Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error
|
||||
|
||||
int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms)
|
||||
{
|
||||
// Use the global Lua state if none was passed
|
||||
lua_State *L = static_cast<lua_State *>(L_);
|
||||
if (!L)
|
||||
L = g_luaState;
|
||||
if (!L)
|
||||
return -1;
|
||||
|
||||
|
||||
auto it = g_luaNodeHandlers.find(nodeName);
|
||||
if (it == g_luaNodeHandlers.end())
|
||||
return -1;
|
||||
|
||||
int ref = it->second;
|
||||
|
||||
// Push the callback function
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
|
||||
|
||||
// Push entity ID as first argument
|
||||
lua_pushinteger(L, luaEntityToId(entity));
|
||||
|
||||
// Push params table as second argument
|
||||
pushParamsTable(L, params);
|
||||
|
||||
// Call the function
|
||||
if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Error calling node '" << nodeName
|
||||
<< "': " << lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Read the result
|
||||
if (!lua_isstring(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *result = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
|
||||
if (strcmp(result, "success") == 0)
|
||||
return 0;
|
||||
if (strcmp(result, "failure") == 0)
|
||||
return 1;
|
||||
if (strcmp(result, "running") == 0)
|
||||
return 2;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Node '" << nodeName
|
||||
<< "' returned invalid result: " << result;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.register_node(name, function)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaRegisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
if (!lua_isfunction(L, 2))
|
||||
luaL_error(L, "Expected function as second argument");
|
||||
|
||||
// Remove any existing handler with the same name
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it != g_luaNodeHandlers.end()) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
}
|
||||
|
||||
// Store the function reference
|
||||
lua_pushvalue(L, 2);
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
g_luaNodeHandlers[name] = ref;
|
||||
|
||||
Ogre::LogManager::getSingleton().stream()
|
||||
<< "[Lua BT] Registered node handler: " << name;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.unregister_node(name) -> bool
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaUnregisterNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
|
||||
auto it = g_luaNodeHandlers.find(name);
|
||||
if (it == g_luaNodeHandlers.end()) {
|
||||
lua_pushboolean(L, false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, it->second);
|
||||
g_luaNodeHandlers.erase(it);
|
||||
|
||||
lua_pushboolean(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.list_nodes() -> table of names
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaListNodeHandlers(lua_State *L)
|
||||
{
|
||||
lua_newtable(L);
|
||||
int idx = 1;
|
||||
for (const auto &kv : g_luaNodeHandlers) {
|
||||
lua_pushstring(L, kv.first.c_str());
|
||||
lua_rawseti(L, -2, idx++);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lua: ecs.behavior_tree.create_node(name, params) -> table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int luaCreateNode(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const char *params = luaL_optstring(L, 2, "");
|
||||
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushstring(L, "luaTask");
|
||||
lua_setfield(L, -2, "type");
|
||||
|
||||
lua_pushstring(L, name);
|
||||
lua_setfield(L, -2, "name");
|
||||
|
||||
if (params && params[0]) {
|
||||
lua_pushstring(L, params);
|
||||
lua_setfield(L, -2, "params");
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void registerLuaBehaviorTreeApi(lua_State *L)
|
||||
{
|
||||
// Store the Lua state globally so callLuaBehaviorTreeNode can use it
|
||||
g_luaState = L;
|
||||
|
||||
// Get or create the "ecs" global table
|
||||
lua_getglobal(L, "ecs");
|
||||
if (lua_isnil(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
lua_newtable(L);
|
||||
}
|
||||
|
||||
// Create the behavior_tree sub-table
|
||||
lua_newtable(L);
|
||||
|
||||
lua_pushcfunction(L, luaRegisterNode);
|
||||
lua_setfield(L, -2, "register_node");
|
||||
|
||||
lua_pushcfunction(L, luaUnregisterNode);
|
||||
lua_setfield(L, -2, "unregister_node");
|
||||
|
||||
lua_pushcfunction(L, luaListNodeHandlers);
|
||||
lua_setfield(L, -2, "list_nodes");
|
||||
|
||||
lua_pushcfunction(L, luaCreateNode);
|
||||
lua_setfield(L, -2, "create_node");
|
||||
|
||||
// Set behavior_tree as a field of ecs
|
||||
lua_setfield(L, -2, "behavior_tree");
|
||||
|
||||
// Ensure ecs is global
|
||||
lua_setglobal(L, "ecs");
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
83
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
83
src/features/editScene/lua/LuaBehaviorTreeApi.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#ifndef EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#define EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
/**
|
||||
* @file LuaBehaviorTreeApi.hpp
|
||||
* @brief Lua API for creating behavior tree nodes that run Lua functions.
|
||||
*
|
||||
* Provides Lua bindings to register named Lua functions as behavior tree
|
||||
* node handlers, and to create behavior tree nodes that invoke those
|
||||
* functions during tree evaluation.
|
||||
*
|
||||
* Exposed Lua globals (under the "ecs" table):
|
||||
* ecs.behavior_tree.register_node(name, function) -> nil
|
||||
* Register a Lua function as a behavior tree node handler.
|
||||
* The function receives (entity_id, params_table) and must return
|
||||
* "success", "failure", or "running".
|
||||
*
|
||||
* ecs.behavior_tree.unregister_node(name) -> bool
|
||||
* Remove a previously registered node handler.
|
||||
*
|
||||
* ecs.behavior_tree.list_nodes() -> table of registered node names
|
||||
* Returns an array of all registered node handler names.
|
||||
*
|
||||
* ecs.behavior_tree.create_node(name, params) -> table
|
||||
* Creates a behavior tree node table suitable for use in
|
||||
* ecs.action_db.add_action() behavior trees.
|
||||
* Returns { type = "luaTask", name = name, params = params }
|
||||
*
|
||||
* Example:
|
||||
* -- Register a node handler
|
||||
* ecs.behavior_tree.register_node("say_hello", function(entity_id, params)
|
||||
* print("Hello from entity " .. entity_id .. "! " .. (params.message or ""))
|
||||
* return "success"
|
||||
* end)
|
||||
*
|
||||
* -- Use it in an action's behavior tree
|
||||
* ecs.action_db.add_action("greet", 1, {}, {}, {
|
||||
* type = "sequence",
|
||||
* children = {
|
||||
* ecs.behavior_tree.create_node("say_hello", "message=Hello World")
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* -- A node that runs for a while:
|
||||
* ecs.behavior_tree.register_node("wait_random", function(entity_id, params)
|
||||
* -- Use the blackboard to store state across frames
|
||||
* local bb = ecs.get_component(entity_id, "GoapBlackboard")
|
||||
* if not bb then return "failure" end
|
||||
*
|
||||
* local key = "wait_timer_" .. params.timer_key
|
||||
* local dt = ecs.get_delta_time()
|
||||
* bb.floatValues[key] = (bb.floatValues[key] or 0) + dt
|
||||
*
|
||||
* local duration = tonumber(params.duration) or 1.0
|
||||
* if bb.floatValues[key] >= duration then
|
||||
* bb.floatValues[key] = nil
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "success"
|
||||
* end
|
||||
* ecs.set_component(entity_id, "GoapBlackboard", bb)
|
||||
* return "running"
|
||||
* end)
|
||||
*/
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register all behavior tree Lua API functions into the "ecs" table.
|
||||
*
|
||||
* Adds a behavior_tree sub-table to the "ecs" global table with functions
|
||||
* for registering Lua node handlers and creating behavior tree nodes.
|
||||
*
|
||||
* @param L The Lua state.
|
||||
*/
|
||||
void registerLuaBehaviorTreeApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP
|
||||
@@ -18,6 +18,20 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
/* Forward declaration for Lua behavior tree node support.
|
||||
* The actual implementation is in lua/LuaBehaviorTreeApi.cpp.
|
||||
* Returns: 0 = success, 1 = failure, 2 = running, -1 = not found/error.
|
||||
* We use a function pointer approach to avoid a hard dependency on
|
||||
* the Lua headers in this translation unit.
|
||||
*
|
||||
* The lua_State pointer is passed as void* to avoid needing the Lua
|
||||
* headers in this translation unit. */
|
||||
namespace editScene
|
||||
{
|
||||
int callLuaBehaviorTreeNode(void *L, const std::string &nodeName,
|
||||
flecs::entity entity, const std::string ¶ms);
|
||||
}
|
||||
|
||||
static float g_epsilon = 0.0001f;
|
||||
|
||||
static bool parseValueString(const Ogre::String &str, int &outInt,
|
||||
@@ -973,6 +987,23 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
/* --- Lua behavior tree node --- */
|
||||
if (node.type == "luaTask") {
|
||||
/* Call the Lua function via the forward-declared API.
|
||||
* Returns: 0 = success, 1 = failure, 2 = running, -1 = error/not found */
|
||||
int result = editScene::callLuaBehaviorTreeNode(
|
||||
nullptr, node.name, e, node.params);
|
||||
if (result == 0) return Status::success;
|
||||
if (result == 1)
|
||||
return Status::failure;
|
||||
if (result == 2)
|
||||
return Status::running;
|
||||
/* -1 or other: log error and return failure */
|
||||
std::cout << "[BT] luaTask: node '" << node.name
|
||||
<< "' not registered or error" << std::endl;
|
||||
return Status::failure;
|
||||
}
|
||||
|
||||
return Status::success;
|
||||
}
|
||||
|
||||
|
||||
@@ -326,6 +326,12 @@ void EditorUISystem::update(float deltaTime)
|
||||
renderPrefabBrowser();
|
||||
renderCursorPanel();
|
||||
|
||||
// Render Action Database singleton editor window
|
||||
if (m_showActionDatabaseSingleton) {
|
||||
m_actionDatabaseSingletonEditor.render(
|
||||
&m_showActionDatabaseSingleton);
|
||||
}
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
}
|
||||
@@ -409,6 +415,11 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
if (ImGui::MenuItem("3D Cursor")) {
|
||||
m_showCursorPanel = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(
|
||||
"Action Database (Singleton)")) {
|
||||
m_showActionDatabaseSingleton = true;
|
||||
}
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -627,9 +638,11 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
indicators += " [Mat]";
|
||||
if (entity.has<AnimationTreeComponent>())
|
||||
indicators += " [Anim]";
|
||||
// ActionDatabase is now a singleton, not a per-entity component
|
||||
if (entity.has<ActionDatabaseComponent>())
|
||||
indicators += " [ActDB]";
|
||||
if (entity.has<ActionDebug>())
|
||||
indicators += " [Debug]";
|
||||
|
||||
if (entity.has<BehaviorTreeComponent>())
|
||||
indicators += " [BT]";
|
||||
if (entity.has<GoapBlackboard>())
|
||||
@@ -1019,9 +1032,15 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// ActionDatabase is now a singleton, not a per-entity component
|
||||
// Render ActionDatabaseComponent if present
|
||||
if (entity.has<ActionDatabaseComponent>()) {
|
||||
auto &db = entity.get_mut<ActionDatabaseComponent>();
|
||||
m_componentRegistry.render<ActionDatabaseComponent>(entity, db);
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
// Render ActionDebug if present
|
||||
|
||||
if (entity.has<ActionDebug>()) {
|
||||
auto &debug = entity.get_mut<ActionDebug>();
|
||||
m_componentRegistry.render<ActionDebug>(entity, debug);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "../ui/ComponentRegistry.hpp"
|
||||
#include "../ui/ActionDatabaseSingletonEditor.hpp"
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../gizmo/Gizmo.hpp"
|
||||
#include "../gizmo/Cursor3D.hpp"
|
||||
@@ -297,6 +298,10 @@ private:
|
||||
// Camera reference for cursor placement/rotation
|
||||
EditorCamera *m_editorCamera = nullptr;
|
||||
|
||||
// Action Database singleton editor state
|
||||
bool m_showActionDatabaseSingleton = false;
|
||||
ActionDatabaseSingletonEditor m_actionDatabaseSingletonEditor;
|
||||
|
||||
// Queries
|
||||
flecs::query<EntityNameComponent> m_nameQuery;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <cassert>
|
||||
|
||||
namespace Ogre
|
||||
{
|
||||
@@ -119,6 +120,17 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// OgreAssert macro (used by LuaEntityApi.hpp)
|
||||
#ifndef OgreAssert
|
||||
#define OgreAssert(expr, msg) \
|
||||
do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "OgreAssert failed: %s\n", msg); \
|
||||
assert(expr); \
|
||||
} \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
} // namespace Ogre
|
||||
|
||||
#endif // OGRE_STUB_H
|
||||
|
||||
492
src/features/editScene/tests/behavior_tree_lua_test.cpp
Normal file
492
src/features/editScene/tests/behavior_tree_lua_test.cpp
Normal file
@@ -0,0 +1,492 @@
|
||||
/**
|
||||
* @file behavior_tree_lua_test.cpp
|
||||
* @brief Standalone test for Behavior Tree Lua API.
|
||||
*
|
||||
* This test creates a Lua state, registers the behavior tree Lua API,
|
||||
* then runs Lua scripts that register node handlers, create nodes,
|
||||
* list/unregister handlers, and verify the results.
|
||||
*
|
||||
* Build with:
|
||||
* g++ -std=c++17 -I. -I../.. -I../../lua/lua-5.4.8/src \
|
||||
* behavior_tree_lua_test.cpp \
|
||||
* ../lua/LuaBehaviorTreeApi.cpp \
|
||||
* ../../lua/lua-5.4.8/src/liblua.a \
|
||||
* -o behavior_tree_lua_test -lm
|
||||
*
|
||||
* Or via CMake (see CMakeLists.txt in this directory).
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Ogre stub (provides Ogre::String, Ogre::Vector3, Ogre::LogManager)
|
||||
// Must be included before any component headers that use Ogre types.
|
||||
#include "ogre_stub.h"
|
||||
|
||||
// Include Lua
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// Include the components we need
|
||||
#include "../components/BehaviorTree.hpp"
|
||||
|
||||
// Forward declare the registration function
|
||||
namespace editScene
|
||||
{
|
||||
void registerLuaBehaviorTreeApi(lua_State *L);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", msg); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: run a Lua string and check for errors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static bool runLua(lua_State *L, const char *code)
|
||||
{
|
||||
if (luaL_dostring(L, code) != LUA_OK) {
|
||||
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get a global integer from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int getGlobalInt(lua_State *L, const char *name)
|
||||
{
|
||||
lua_getglobal(L, name);
|
||||
int val = (int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
return val;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: get a global string from Lua
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static std::string getGlobalString(lua_State *L, const char *name)
|
||||
{
|
||||
lua_getglobal(L, name);
|
||||
const char *s = lua_tostring(L, -1);
|
||||
std::string val = s ? s : "";
|
||||
lua_pop(L, 1);
|
||||
return val;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: Register a node handler and verify it's stored
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testRegisterNode(lua_State *L)
|
||||
{
|
||||
TEST("register a node handler");
|
||||
|
||||
bool ok = runLua(L, "ecs.behavior_tree.register_node('test_node', "
|
||||
" function(entity_id, params) "
|
||||
" return 'success' "
|
||||
" end"
|
||||
")");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
// Verify via list_nodes
|
||||
ok = runLua(L, "local nodes = ecs.behavior_tree.list_nodes()\n"
|
||||
"found = false\n"
|
||||
"for i, name in ipairs(nodes) do\n"
|
||||
" if name == 'test_node' then found = true end\n"
|
||||
"end\n"
|
||||
"test_result = found\n");
|
||||
if (!ok)
|
||||
FAIL("failed to list nodes");
|
||||
|
||||
lua_getglobal(L, "test_result");
|
||||
if (!lua_toboolean(L, -1))
|
||||
FAIL("test_node not found in list");
|
||||
lua_pop(L, 1);
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: Register multiple nodes and list them
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testListNodes(lua_State *L)
|
||||
{
|
||||
TEST("list multiple registered nodes");
|
||||
|
||||
bool ok = runLua(L, "ecs.behavior_tree.register_node('node_a', "
|
||||
" function(e, p) return 'success' end)\n"
|
||||
"ecs.behavior_tree.register_node('node_b', "
|
||||
" function(e, p) return 'failure' end)\n"
|
||||
"ecs.behavior_tree.register_node('node_c', "
|
||||
" function(e, p) return 'running' end)\n"
|
||||
"local nodes = ecs.behavior_tree.list_nodes()\n"
|
||||
"count = #nodes\n");
|
||||
if (!ok)
|
||||
FAIL("failed to register/list nodes");
|
||||
|
||||
int count = getGlobalInt(L, "count");
|
||||
if (count < 3)
|
||||
FAIL("expected at least 3 nodes");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: Unregister a node handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testUnregisterNode(lua_State *L)
|
||||
{
|
||||
TEST("unregister a node handler");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local removed = ecs.behavior_tree.unregister_node('node_a')\n"
|
||||
"unreg_result = removed\n"
|
||||
"local nodes = ecs.behavior_tree.list_nodes()\n"
|
||||
"found_a = false\n"
|
||||
"for i, name in ipairs(nodes) do\n"
|
||||
" if name == 'node_a' then found_a = true end\n"
|
||||
"end\n");
|
||||
if (!ok)
|
||||
FAIL("failed to unregister node");
|
||||
|
||||
lua_getglobal(L, "unreg_result");
|
||||
if (!lua_toboolean(L, -1))
|
||||
FAIL("unregister_node returned false");
|
||||
lua_pop(L, 1);
|
||||
|
||||
lua_getglobal(L, "found_a");
|
||||
if (lua_toboolean(L, -1))
|
||||
FAIL("node_a still found after unregister");
|
||||
lua_pop(L, 1);
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: Unregister non-existent node returns false
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testUnregisterNonExistent(lua_State *L)
|
||||
{
|
||||
TEST("unregister non-existent node returns false");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"local removed = ecs.behavior_tree.unregister_node('nonexistent')\n"
|
||||
"unreg_none = removed\n");
|
||||
if (!ok)
|
||||
FAIL("failed to run Lua");
|
||||
|
||||
lua_getglobal(L, "unreg_none");
|
||||
if (lua_toboolean(L, -1))
|
||||
FAIL("unregister of non-existent should return false");
|
||||
lua_pop(L, 1);
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: Create a behavior tree node via create_node
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCreateNode(lua_State *L)
|
||||
{
|
||||
TEST("create_node returns correct node table");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local node = ecs.behavior_tree.create_node('test_node', "
|
||||
" 'key=val,count=42')\n"
|
||||
"node_type = node.type\n"
|
||||
"node_name = node.name\n"
|
||||
"node_params = node.params\n");
|
||||
if (!ok)
|
||||
FAIL("failed to create node");
|
||||
|
||||
std::string type = getGlobalString(L, "node_type");
|
||||
std::string name = getGlobalString(L, "node_name");
|
||||
std::string params = getGlobalString(L, "node_params");
|
||||
|
||||
if (type != "luaTask")
|
||||
FAIL("expected type 'luaTask', got '" + type + "'");
|
||||
if (name != "test_node")
|
||||
FAIL("expected name 'test_node', got '" + name + "'");
|
||||
if (params != "key=val,count=42")
|
||||
FAIL("expected params 'key=val,count=42', got '" + params +
|
||||
"'");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: Create node without params (optional)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCreateNodeNoParams(lua_State *L)
|
||||
{
|
||||
TEST("create_node without params");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local node = ecs.behavior_tree.create_node('simple_node')\n"
|
||||
"no_params_type = node.type\n"
|
||||
"no_params_name = node.name\n"
|
||||
"no_params_has_params = (node.params ~= nil)\n");
|
||||
if (!ok)
|
||||
FAIL("failed to create node without params");
|
||||
|
||||
std::string type = getGlobalString(L, "no_params_type");
|
||||
std::string name = getGlobalString(L, "no_params_name");
|
||||
|
||||
if (type != "luaTask")
|
||||
FAIL("expected type 'luaTask'");
|
||||
if (name != "simple_node")
|
||||
FAIL("expected name 'simple_node'");
|
||||
|
||||
// params should be nil or empty when not provided
|
||||
lua_getglobal(L, "no_params_has_params");
|
||||
if (lua_toboolean(L, -1)) {
|
||||
// It's okay if params is set to empty string
|
||||
lua_getglobal(L, "node_params");
|
||||
const char *p = lua_tostring(L, -1);
|
||||
if (p && p[0] != '\0') {
|
||||
FAIL("expected no params, got '" + std::string(p) +
|
||||
"'");
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: Create node and use in a behavior tree structure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCreateNodeInTree(lua_State *L)
|
||||
{
|
||||
TEST("create_node used in behavior tree structure");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local tree = {\n"
|
||||
" type = 'sequence',\n"
|
||||
" children = {\n"
|
||||
" ecs.behavior_tree.create_node('say_hello', "
|
||||
" 'message=Hello'),\n"
|
||||
" ecs.behavior_tree.create_node('wait_for_duration', "
|
||||
" 'duration=2.0,timer_key=test'),\n"
|
||||
" ecs.behavior_tree.create_node('say_hello', "
|
||||
" 'message=Done')\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"tree_type = tree.type\n"
|
||||
"child_count = #tree.children\n"
|
||||
"child0_type = tree.children[1].type\n"
|
||||
"child0_name = tree.children[1].name\n"
|
||||
"child0_params = tree.children[1].params\n"
|
||||
"child1_type = tree.children[2].type\n"
|
||||
"child1_name = tree.children[2].name\n"
|
||||
"child1_params = tree.children[2].params\n");
|
||||
if (!ok)
|
||||
FAIL("failed to create tree with Lua nodes");
|
||||
|
||||
std::string treeType = getGlobalString(L, "tree_type");
|
||||
int childCount = getGlobalInt(L, "child_count");
|
||||
|
||||
if (treeType != "sequence")
|
||||
FAIL("expected tree type 'sequence'");
|
||||
if (childCount != 3)
|
||||
FAIL("expected 3 children");
|
||||
|
||||
std::string c0type = getGlobalString(L, "child0_type");
|
||||
std::string c0name = getGlobalString(L, "child0_name");
|
||||
std::string c0params = getGlobalString(L, "child0_params");
|
||||
|
||||
if (c0type != "luaTask")
|
||||
FAIL("child 0: expected type 'luaTask'");
|
||||
if (c0name != "say_hello")
|
||||
FAIL("child 0: expected name 'say_hello'");
|
||||
if (c0params != "message=Hello")
|
||||
FAIL("child 0: expected params 'message=Hello'");
|
||||
|
||||
std::string c1type = getGlobalString(L, "child1_type");
|
||||
std::string c1name = getGlobalString(L, "child1_name");
|
||||
std::string c1params = getGlobalString(L, "child1_params");
|
||||
|
||||
if (c1type != "luaTask")
|
||||
FAIL("child 1: expected type 'luaTask'");
|
||||
if (c1name != "wait_for_duration")
|
||||
FAIL("child 1: expected name 'wait_for_duration'");
|
||||
if (c1params != "duration=2.0,timer_key=test")
|
||||
FAIL("child 1: unexpected params");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: Re-register a node (replace existing handler)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testReregisterNode(lua_State *L)
|
||||
{
|
||||
TEST("re-register a node handler (replace)");
|
||||
|
||||
bool ok = runLua(
|
||||
L,
|
||||
"ecs.behavior_tree.register_node('replaceable', "
|
||||
" function(e, p) return 'failure' end)\n"
|
||||
"ecs.behavior_tree.register_node('replaceable', "
|
||||
" function(e, p) return 'success' end)\n"
|
||||
"local nodes = ecs.behavior_tree.list_nodes()\n"
|
||||
"found_count = 0\n"
|
||||
"for i, name in ipairs(nodes) do\n"
|
||||
" if name == 'replaceable' then found_count = found_count + 1 end\n"
|
||||
"end\n");
|
||||
if (!ok)
|
||||
FAIL("failed to re-register node");
|
||||
|
||||
int count = getGlobalInt(L, "found_count");
|
||||
if (count != 1)
|
||||
FAIL("expected exactly 1 'replaceable' after re-register, got " +
|
||||
std::to_string(count));
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 9: Empty list when no nodes registered
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testEmptyList(lua_State *L)
|
||||
{
|
||||
TEST("list_nodes returns empty when no nodes");
|
||||
|
||||
// Unregister all nodes we created
|
||||
bool ok = runLua(L, "ecs.behavior_tree.unregister_node('test_node')\n"
|
||||
"ecs.behavior_tree.unregister_node('node_b')\n"
|
||||
"ecs.behavior_tree.unregister_node('node_c')\n"
|
||||
"ecs.behavior_tree.unregister_node('replaceable')\n"
|
||||
"local nodes = ecs.behavior_tree.list_nodes()\n"
|
||||
"empty_count = #nodes\n");
|
||||
if (!ok)
|
||||
FAIL("failed to unregister all nodes");
|
||||
|
||||
int count = getGlobalInt(L, "empty_count");
|
||||
if (count != 0)
|
||||
FAIL("expected empty list, got " + std::to_string(count) +
|
||||
" nodes");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 10: Register node with invalid arguments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testRegisterInvalidArgs(lua_State *L)
|
||||
{
|
||||
TEST("register_node with invalid args (no function)");
|
||||
|
||||
// This should error - we just verify it doesn't crash
|
||||
bool ok = runLua(L, "local ok, err = pcall(function()\n"
|
||||
" ecs.behavior_tree.register_node('bad_node')\n"
|
||||
"end)\n"
|
||||
"pcall_ok = ok\n");
|
||||
if (!ok)
|
||||
FAIL("failed to run pcall");
|
||||
|
||||
lua_getglobal(L, "pcall_ok");
|
||||
if (lua_toboolean(L, -1))
|
||||
FAIL("expected register_node without function to error");
|
||||
lua_pop(L, 1);
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Behavior Tree Lua API Tests\n");
|
||||
printf("============================\n\n");
|
||||
|
||||
// Create Lua state
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L) {
|
||||
fprintf(stderr, "Failed to create Lua state\n");
|
||||
return 1;
|
||||
}
|
||||
luaL_openlibs(L);
|
||||
|
||||
// Register the behavior tree API
|
||||
editScene::registerLuaBehaviorTreeApi(L);
|
||||
|
||||
// Run tests
|
||||
int failures = 0;
|
||||
failures += testRegisterNode(L);
|
||||
failures += testListNodes(L);
|
||||
failures += testUnregisterNode(L);
|
||||
failures += testUnregisterNonExistent(L);
|
||||
failures += testCreateNode(L);
|
||||
failures += testCreateNodeNoParams(L);
|
||||
failures += testCreateNodeInTree(L);
|
||||
failures += testReregisterNode(L);
|
||||
failures += testEmptyList(L);
|
||||
failures += testRegisterInvalidArgs(L);
|
||||
|
||||
// Cleanup
|
||||
lua_close(L);
|
||||
|
||||
printf("\nResults: %d/%d passed, %d failed\n", passCount, testCount,
|
||||
failures);
|
||||
|
||||
return failures > 0 ? 1 : 0;
|
||||
}
|
||||
236
src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp
Normal file
236
src/features/editScene/ui/ActionDatabaseSingletonEditor.cpp
Normal file
@@ -0,0 +1,236 @@
|
||||
#include "ActionDatabaseSingletonEditor.hpp"
|
||||
#include "GoapBlackboardEditor.hpp"
|
||||
#include "InlineBehaviorTreeEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
void ActionDatabaseSingletonEditor::render(bool *open)
|
||||
{
|
||||
if (!ImGui::Begin("Action Database (Singleton)", open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::CollapsingHeader("Bit Names",
|
||||
ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderBitNames();
|
||||
|
||||
if (ImGui::CollapsingHeader("Actions", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderActions();
|
||||
|
||||
if (ImGui::CollapsingHeader("Goals", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
renderGoals();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderBitNames()
|
||||
{
|
||||
bool changed = false;
|
||||
ImGui::Indent();
|
||||
ImGui::Text("Name bits for use in preconditions/effects:");
|
||||
ImGui::TextDisabled("(empty slots show as numbers in blackboards)");
|
||||
|
||||
for (int i = 0; i < 64; i++) {
|
||||
const char *name = GoapBlackboard::getBitName(i);
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "%s", name ? name : "");
|
||||
|
||||
ImGui::PushID(i);
|
||||
char label[16];
|
||||
snprintf(label, sizeof(label), "%2d", i);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (ImGui::InputText(label, buf, sizeof(buf))) {
|
||||
GoapBlackboard::setBitName(i, buf);
|
||||
changed = true;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
(void)changed;
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderActions()
|
||||
{
|
||||
ActionDatabase &db = ActionDatabase::getSingleton();
|
||||
ImGui::Indent();
|
||||
|
||||
for (size_t i = 0; i < db.actions.size(); i++) {
|
||||
ImGui::PushID((int)i);
|
||||
bool isSelected = (m_selectedAction == (int)i);
|
||||
|
||||
char label[256];
|
||||
snprintf(label, sizeof(label), "%s (cost: %d)",
|
||||
db.actions[i].name.c_str(), db.actions[i].cost);
|
||||
|
||||
if (ImGui::Selectable(label, isSelected))
|
||||
m_selectedAction = isSelected ? -1 : (int)i;
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.actions.erase(db.actions.begin() + i);
|
||||
if (m_selectedAction == (int)i)
|
||||
m_selectedAction = -1;
|
||||
else if (m_selectedAction > (int)i)
|
||||
m_selectedAction--;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Action")) {
|
||||
char newName[64];
|
||||
snprintf(newName, sizeof(newName), "Action_%zu",
|
||||
db.actions.size());
|
||||
db.actions.emplace_back(newName);
|
||||
m_selectedAction = (int)db.actions.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedAction >= 0 &&
|
||||
m_selectedAction < (int)db.actions.size()) {
|
||||
renderActionEditor(db.actions[m_selectedAction]);
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderGoals()
|
||||
{
|
||||
ActionDatabase &db = ActionDatabase::getSingleton();
|
||||
ImGui::Indent();
|
||||
|
||||
for (size_t i = 0; i < db.goals.size(); i++) {
|
||||
ImGui::PushID((int)i + 10000);
|
||||
bool isSelected = (m_selectedGoal == (int)i);
|
||||
|
||||
char label[256];
|
||||
snprintf(label, sizeof(label), "%s (priority: %d)",
|
||||
db.goals[i].name.c_str(), db.goals[i].priority);
|
||||
|
||||
if (ImGui::Selectable(label, isSelected))
|
||||
m_selectedGoal = isSelected ? -1 : (int)i;
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) {
|
||||
db.goals.erase(db.goals.begin() + i);
|
||||
if (m_selectedGoal == (int)i)
|
||||
m_selectedGoal = -1;
|
||||
else if (m_selectedGoal > (int)i)
|
||||
m_selectedGoal--;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Add Goal")) {
|
||||
char newName[64];
|
||||
snprintf(newName, sizeof(newName), "Goal_%zu", db.goals.size());
|
||||
db.goals.emplace_back(newName);
|
||||
m_selectedGoal = (int)db.goals.size() - 1;
|
||||
}
|
||||
|
||||
if (m_selectedGoal >= 0 && m_selectedGoal < (int)db.goals.size()) {
|
||||
renderGoalEditor(db.goals[m_selectedGoal]);
|
||||
}
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderActionEditor(GoapAction &action)
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Action: %s", action.name.c_str());
|
||||
|
||||
char nameBuf[256];
|
||||
snprintf(nameBuf, sizeof(nameBuf), "%s", action.name.c_str());
|
||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
|
||||
action.name = nameBuf;
|
||||
|
||||
if (ImGui::InputInt("Cost", &action.cost))
|
||||
action.cost = action.cost < 0 ? 0 : action.cost;
|
||||
|
||||
char btNameBuf[256];
|
||||
snprintf(btNameBuf, sizeof(btNameBuf), "%s",
|
||||
action.behaviorTreeName.c_str());
|
||||
if (ImGui::InputText("Behavior Tree Name", btNameBuf,
|
||||
sizeof(btNameBuf)))
|
||||
action.behaviorTreeName = btNameBuf;
|
||||
|
||||
if (ImGui::TreeNode("Behavior Tree")) {
|
||||
if (InlineBehaviorTreeEditor::render(action.behaviorTree))
|
||||
/* modified */;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Preconditions")) {
|
||||
GoapBlackboardEditor::render(action.preconditions, "precond");
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// Precondition mask editor
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Precondition Mask (bits to check)");
|
||||
for (int i = 0; i < 64; i += 8) {
|
||||
for (int j = 0; j < 8 && (i + j) < 64; j++) {
|
||||
int bit = i + j;
|
||||
bool enabled = (action.preconditionMask >> bit) & 1ULL;
|
||||
char label[8];
|
||||
snprintf(label, sizeof(label), "%d", bit);
|
||||
if (j > 0)
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox(label, &enabled)) {
|
||||
if (enabled)
|
||||
action.preconditionMask |=
|
||||
(1ULL << bit);
|
||||
else
|
||||
action.preconditionMask &=
|
||||
~(1ULL << bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::Text("Mask: 0x%016llX",
|
||||
(unsigned long long)action.preconditionMask);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Set All##mask")) {
|
||||
action.preconditionMask = ~0ULL;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Clear All##mask")) {
|
||||
action.preconditionMask = 0ULL;
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Effects")) {
|
||||
GoapBlackboardEditor::render(action.effects, "effects");
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
void ActionDatabaseSingletonEditor::renderGoalEditor(GoapGoal &goal)
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Goal: %s", goal.name.c_str());
|
||||
|
||||
char nameBuf[256];
|
||||
snprintf(nameBuf, sizeof(nameBuf), "%s", goal.name.c_str());
|
||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf)))
|
||||
goal.name = nameBuf;
|
||||
|
||||
if (ImGui::InputInt("Priority", &goal.priority))
|
||||
goal.priority = goal.priority < 0 ? 0 : goal.priority;
|
||||
|
||||
char condBuf[512];
|
||||
snprintf(condBuf, sizeof(condBuf), "%s", goal.condition.c_str());
|
||||
if (ImGui::InputText("Condition", condBuf, sizeof(condBuf)))
|
||||
goal.condition = condBuf;
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip(
|
||||
"Optional expression: health > 20 && hunger < 50");
|
||||
}
|
||||
|
||||
if (ImGui::TreeNode("Target Blackboard")) {
|
||||
GoapBlackboardEditor::render(goal.target, "target");
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
38
src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp
Normal file
38
src/features/editScene/ui/ActionDatabaseSingletonEditor.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
#define EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
|
||||
/**
|
||||
* Editor for the ActionDatabase singleton.
|
||||
*
|
||||
* Unlike ActionDatabaseEditor (which edits the ActionDatabaseComponent on a
|
||||
* scene entity), this editor works directly on the global ActionDatabase
|
||||
* singleton. It is opened from the editor menu and renders in its own
|
||||
* ImGui window.
|
||||
*/
|
||||
class ActionDatabaseSingletonEditor {
|
||||
public:
|
||||
ActionDatabaseSingletonEditor() = default;
|
||||
|
||||
/**
|
||||
* Render the singleton editor window.
|
||||
* Call this inside an ImGui frame.
|
||||
*
|
||||
* @param open Pointer to bool controlling window visibility.
|
||||
*/
|
||||
void render(bool *open);
|
||||
|
||||
private:
|
||||
void renderBitNames();
|
||||
void renderActions();
|
||||
void renderGoals();
|
||||
void renderActionEditor(GoapAction &action);
|
||||
void renderGoalEditor(GoapGoal &goal);
|
||||
|
||||
int m_selectedAction = -1;
|
||||
int m_selectedGoal = -1;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_ACTION_DATABASE_SINGLETON_EDITOR_HPP
|
||||
Reference in New Issue
Block a user