diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.cpp b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp index 144ea64..00fa9cf 100644 --- a/src/features/editScene/lua/LuaBehaviorTreeApi.cpp +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace editScene { @@ -170,7 +171,6 @@ int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, L = g_luaState; if (!L) return -1; - auto it = g_luaNodeHandlers.find(nodeName); if (it == g_luaNodeHandlers.end()) @@ -218,6 +218,66 @@ int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, return -1; } +// --------------------------------------------------------------------------- +// Convenience overload: call with raw entity ID (for tests) +// --------------------------------------------------------------------------- + +int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, + uint64_t entityId, const std::string ¶ms) +{ + // Use the global Lua state if none was passed + lua_State *L = static_cast(L_); + if (!L) + L = g_luaState; + if (!L) + return -1; + + auto it = g_luaNodeHandlers.find(nodeName); + if (it == g_luaNodeHandlers.end()) + return -1; + + int ref = it->second; + + // Push the callback function + lua_rawgeti(L, LUA_REGISTRYINDEX, ref); + + // Push entity ID as first argument + lua_pushinteger(L, (lua_Integer)entityId); + + // 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) // --------------------------------------------------------------------------- @@ -307,6 +367,19 @@ static int luaCreateNode(lua_State *L) return 1; } +// --------------------------------------------------------------------------- +// Public C++ API: get list of registered Lua node names +// --------------------------------------------------------------------------- + +std::vector getRegisteredLuaNodeNames() +{ + std::vector names; + names.reserve(g_luaNodeHandlers.size()); + for (const auto &kv : g_luaNodeHandlers) + names.push_back(kv.first); + return names; +} + // --------------------------------------------------------------------------- // Registration // --------------------------------------------------------------------------- diff --git a/src/features/editScene/lua/LuaBehaviorTreeApi.hpp b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp index 8153224..b411ded 100644 --- a/src/features/editScene/lua/LuaBehaviorTreeApi.hpp +++ b/src/features/editScene/lua/LuaBehaviorTreeApi.hpp @@ -3,6 +3,10 @@ #pragma once #include +#include +#include +#include +#include /** * @file LuaBehaviorTreeApi.hpp @@ -78,6 +82,45 @@ namespace editScene */ void registerLuaBehaviorTreeApi(lua_State *L); +/** + * @brief Get the list of registered Lua node handler names. + * + * Used by the behavior tree editor UI to show registered Lua nodes + * in a dropdown instead of requiring manual entry. + * + * @return A vector of registered node handler names. + */ +std::vector getRegisteredLuaNodeNames(); + +/** + * @brief Call a registered Lua behavior tree node handler. + * + * This is the primary evaluation function used by the behavior tree system. + * It looks up the registered handler by name, pushes entity and params + * as arguments, calls the Lua function, and interprets the result. + * + * @param L_ Pointer to the Lua state (can be nullptr to use global state). + * @param nodeName The name of the registered node handler. + * @param entity The flecs entity executing this node. + * @param params The params string (key=val,key2=val2 format). + * @return 0 = success, 1 = failure, 2 = running, -1 = error/not found. + */ +int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, + flecs::entity entity, const std::string ¶ms); + +/** + * @brief Convenience overload for calling a registered Lua node handler + * with a raw entity ID (for use in tests or contexts without flecs). + * + * @param L_ Pointer to the Lua state (can be nullptr to use global state). + * @param nodeName The name of the registered node handler. + * @param entityId The raw entity ID. + * @param params The params string (key=val,key2=val2 format). + * @return 0 = success, 1 = failure, 2 = running, -1 = error/not found. + */ +int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, + uint64_t entityId, const std::string ¶ms); + } // namespace editScene #endif // EDITSCENE_LUA_BEHAVIOR_TREE_API_HPP diff --git a/src/features/editScene/tests/behavior_tree_lua_test.cpp b/src/features/editScene/tests/behavior_tree_lua_test.cpp index 8fe8691..65bee42 100644 --- a/src/features/editScene/tests/behavior_tree_lua_test.cpp +++ b/src/features/editScene/tests/behavior_tree_lua_test.cpp @@ -35,11 +35,14 @@ extern "C" { // Include the components we need #include "../components/BehaviorTree.hpp" +#include "../components/GoapBlackboard.hpp" // Forward declare the registration function namespace editScene { void registerLuaBehaviorTreeApi(lua_State *L); +int callLuaBehaviorTreeNode(void *L_, const std::string &nodeName, + uint64_t entityId, const std::string ¶ms); } // --------------------------------------------------------------------------- @@ -61,10 +64,10 @@ static int passCount = 0; printf("PASS\n"); \ } while (0) -#define FAIL(msg) \ - do { \ - printf("FAIL: %s\n", msg); \ - return 1; \ +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", std::string(msg).c_str()); \ + return 1; \ } while (0) // --------------------------------------------------------------------------- @@ -449,6 +452,185 @@ static int testRegisterInvalidArgs(lua_State *L) return 0; } +// --------------------------------------------------------------------------- +// Test 11: Full pipeline - register, create, and evaluate a node +// --------------------------------------------------------------------------- +// This test exercises the complete flow: register a Lua handler, +// create a node table, then call callLuaBehaviorTreeNode() to +// evaluate it and verify the return value. + +static int testFullPipeline(lua_State *L) +{ + TEST("full pipeline: register, create, evaluate"); + + // Register a node that returns "success" + bool ok = runLua(L, "ecs.behavior_tree.register_node('pipeline_test', " + " function(entity_id, params) " + " return 'success' " + " end" + ")"); + if (!ok) + FAIL("failed to register pipeline_test node"); + + // Call the C++ evaluation function + int result = editScene::callLuaBehaviorTreeNode(L, "pipeline_test", 42, + "msg=hello"); + + if (result != 0) + FAIL("expected 0 (success), got " + std::to_string(result)); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 12: Full pipeline - node returning "failure" +// --------------------------------------------------------------------------- + +static int testFullPipelineFailure(lua_State *L) +{ + TEST("full pipeline: node returning failure"); + + bool ok = runLua(L, "ecs.behavior_tree.register_node('fail_test', " + " function(entity_id, params) " + " return 'failure' " + " end" + ")"); + if (!ok) + FAIL("failed to register fail_test node"); + + int result = editScene::callLuaBehaviorTreeNode(L, "fail_test", 42, ""); + + if (result != 1) + FAIL("expected 1 (failure), got " + std::to_string(result)); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 13: Full pipeline - node returning "running" +// --------------------------------------------------------------------------- + +static int testFullPipelineRunning(lua_State *L) +{ + TEST("full pipeline: node returning running"); + + bool ok = runLua(L, "ecs.behavior_tree.register_node('run_test', " + " function(entity_id, params) " + " return 'running' " + " end" + ")"); + if (!ok) + FAIL("failed to register run_test node"); + + int result = editScene::callLuaBehaviorTreeNode(L, "run_test", 42, ""); + + if (result != 2) + FAIL("expected 2 (running), got " + std::to_string(result)); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 14: Full pipeline - node that reads params +// --------------------------------------------------------------------------- + +static int testFullPipelineParams(lua_State *L) +{ + TEST("full pipeline: node reading params"); + + // Register a node that stores params in a global for verification + bool ok = runLua(L, "ecs.behavior_tree.register_node('param_test', " + " function(entity_id, params) " + " param_count = params.count " + " param_msg = params.msg " + " return 'success' " + " end" + ")"); + if (!ok) + FAIL("failed to register param_test node"); + + int result = editScene::callLuaBehaviorTreeNode(L, "param_test", 42, + "count=42,msg=hello"); + + if (result != 0) + FAIL("expected 0 (success), got " + std::to_string(result)); + + // Verify params were passed correctly + lua_getglobal(L, "param_count"); + if (!lua_isnumber(L, -1)) + FAIL("param_count should be a number"); + int count = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (count != 42) + FAIL("expected param_count=42, got " + std::to_string(count)); + + lua_getglobal(L, "param_msg"); + const char *msg = lua_tostring(L, -1); + lua_pop(L, 1); + if (!msg || strcmp(msg, "hello") != 0) + FAIL(std::string("expected param_msg='hello', got '") + + (msg ? msg : "nil") + "'"); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 15: Full pipeline - node that reads entity_id +// --------------------------------------------------------------------------- + +static int testFullPipelineEntityId(lua_State *L) +{ + TEST("full pipeline: node reading entity_id"); + + bool ok = runLua(L, "ecs.behavior_tree.register_node('eid_test', " + " function(entity_id, params) " + " eid_result = entity_id " + " return 'success' " + " end" + ")"); + if (!ok) + FAIL("failed to register eid_test node"); + + int result = + editScene::callLuaBehaviorTreeNode(L, "eid_test", 12345, ""); + + if (result != 0) + FAIL("expected 0 (success), got " + std::to_string(result)); + + lua_getglobal(L, "eid_result"); + int entityId = (int)lua_tointeger(L, -1); + lua_pop(L, 1); + if (entityId != 12345) + FAIL("expected entity_id=12345, got " + + std::to_string(entityId)); + + PASS(); + return 0; +} + +// --------------------------------------------------------------------------- +// Test 16: Full pipeline - unregistered node returns failure +// --------------------------------------------------------------------------- + +static int testFullPipelineUnregistered(lua_State *L) +{ + TEST("full pipeline: unregistered node returns failure"); + + int result = editScene::callLuaBehaviorTreeNode(L, "nonexistent_node", + 42, ""); + + if (result != -1) + FAIL("expected -1 (not found) for unregistered node, got " + + std::to_string(result)); + + PASS(); + return 0; +} + // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- @@ -481,6 +663,12 @@ int main() failures += testReregisterNode(L); failures += testEmptyList(L); failures += testRegisterInvalidArgs(L); + failures += testFullPipeline(L); + failures += testFullPipelineFailure(L); + failures += testFullPipelineRunning(L); + failures += testFullPipelineParams(L); + failures += testFullPipelineEntityId(L); + failures += testFullPipelineUnregistered(L); // Cleanup lua_close(L); diff --git a/src/features/editScene/ui/BehaviorTreeEditor.cpp b/src/features/editScene/ui/BehaviorTreeEditor.cpp index 2b185cc..8f83428 100644 --- a/src/features/editScene/ui/BehaviorTreeEditor.cpp +++ b/src/features/editScene/ui/BehaviorTreeEditor.cpp @@ -1,4 +1,5 @@ #include "BehaviorTreeEditor.hpp" +#include "../lua/LuaBehaviorTreeApi.hpp" #include static ImU32 nodeTypeColorU32(const Ogre::String &type) @@ -29,6 +30,8 @@ static ImU32 nodeTypeColorU32(const Ogre::String &type) return IM_COL32(0x4C, 0xCC, 0x4C, 0xFF); if (type == "sendEvent") return IM_COL32(0xFF, 0x99, 0x00, 0xFF); + if (type == "luaTask") + return IM_COL32(0xAA, 0x66, 0xFF, 0xFF); return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF); } @@ -106,12 +109,21 @@ void BehaviorTreeEditor::applyEditOps(BehaviorTreeComponent &bt) case TreeEditOp::AddChild: { BehaviorTreeNode child; child.type = op.childType; - if (op.childType == "task" || op.childType == "check") + if (op.childType == "task" || op.childType == "check" || + op.childType == "luaTask") child.name = "unnamed"; vec.push_back(child); changed = true; break; } + case TreeEditOp::AddLuaNode: { + BehaviorTreeNode child; + child.type = "luaTask"; + child.name = op.luaNodeName; + vec.push_back(child); + changed = true; + break; + } case TreeEditOp::RemoveNode: { if (op.childIndex < vec.size()) { vec.erase(vec.begin() + op.childIndex); @@ -137,6 +149,33 @@ void BehaviorTreeEditor::applyEditOps(BehaviorTreeComponent &bt) bt.markDirty(); } +// Helper: render a submenu of registered Lua node names. +// Returns true if a node was selected (via queueAddLuaNode). +static bool +renderLuaNodeSubmenu(BehaviorTreeNode *parent, + std::vector &editOps) +{ + auto names = editScene::getRegisteredLuaNodeNames(); + if (names.empty()) { + ImGui::MenuItem("(no Lua nodes registered)", nullptr, false, + false); + return false; + } + + bool selected = false; + for (const auto &name : names) { + if (ImGui::MenuItem(name.c_str())) { + BehaviorTreeEditor::TreeEditOp op; + op.type = BehaviorTreeEditor::TreeEditOp::AddLuaNode; + op.targetNode = parent; + op.luaNodeName = name; + editOps.push_back(op); + selected = true; + } + } + return selected; +} + void BehaviorTreeEditor::renderTree(BehaviorTreeNode &node, BehaviorTreeNode *parent, BehaviorTreeNode *&selectedNode, @@ -208,6 +247,10 @@ void BehaviorTreeEditor::renderTree(BehaviorTreeNode &node, queueAddChild(&node, "enablePhysics"); if (ImGui::MenuItem("Add Send Event")) queueAddChild(&node, "sendEvent"); + if (ImGui::BeginMenu("Add Lua Task")) { + renderLuaNodeSubmenu(&node, m_editOps); + ImGui::EndMenu(); + } } if (parent) { size_t idx = findChildIndex(*parent, &node); @@ -263,6 +306,10 @@ void BehaviorTreeEditor::renderTree(BehaviorTreeNode &node, queueAddChild(&node, "enablePhysics"); if (ImGui::MenuItem("Send Event")) queueAddChild(&node, "sendEvent"); + if (ImGui::BeginMenu("Lua Task")) { + renderLuaNodeSubmenu(&node, m_editOps); + ImGui::EndMenu(); + } ImGui::EndPopup(); } } @@ -317,7 +364,8 @@ void BehaviorTreeEditor::renderProperties(BehaviorTreeNode *node) "teleportToChild", "disablePhysics", "enablePhysics", - "sendEvent" }; + "sendEvent", + "luaTask" }; int typeIdx = 0; for (int i = 0; i < IM_ARRAYSIZE(types); i++) { if (node->type == types[i]) { @@ -329,52 +377,85 @@ void BehaviorTreeEditor::renderProperties(BehaviorTreeNode *node) node->type = types[typeIdx]; if (node->isLeaf()) { - char buf[256]; - snprintf(buf, sizeof(buf), "%s", node->name.c_str()); + // For luaTask nodes, show a dropdown of registered Lua nodes + if (node->type == "luaTask") { + auto names = editScene::getRegisteredLuaNodeNames(); + if (names.empty()) { + ImGui::TextDisabled("No Lua nodes registered"); + } else { + // Find current index + int current = -1; + for (size_t i = 0; i < names.size(); i++) { + if (names[i] == node->name) { + current = (int)i; + break; + } + } + // Build a C-string array for ImGui Combo + std::vector cstrs; + cstrs.reserve(names.size()); + for (const auto &n : names) + cstrs.push_back(n.c_str()); - const char *label = "Name"; - const char *hint = nullptr; - if (node->type == "debugPrint") { - label = "Message"; - hint = "Text to print when node runs"; - } else if (node->type == "setAnimationState") { - label = "State (SM/State)"; - hint = "Format: stateMachineName/stateName"; - } else if (node->type == "isAnimationEnded") { - label = "State Machine"; - hint = "Name of state machine to check"; - } else if (node->type == "setBit") { - label = "Bit"; - hint = "Bit index or name"; - } else if (node->type == "checkBit") { - label = "Bit"; - hint = "Bit index or name to check"; - } else if (node->type == "setValue") { - label = "Key"; - hint = "Variable name"; - } else if (node->type == "checkValue") { - label = "Key"; - hint = "Variable name to check"; - } else if (node->type == "blackboardDump") { - label = "Label"; - hint = "Optional label prefix"; - } else if (node->type == "teleportToChild") { - label = "Child Name"; - hint = "Name of child entity under Smart Object to teleport to"; - } else if (node->type == "sendEvent") { - label = "Event Name"; - hint = "Name of the event to send via EventBus"; + if (ImGui::Combo("Lua Node", ¤t, + cstrs.data(), + (int)cstrs.size())) { + if (current >= 0 && + current < (int)names.size()) + node->name = names[current]; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Select a registered Lua node handler"); + } + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "%s", node->name.c_str()); + + const char *label = "Name"; + const char *hint = nullptr; + if (node->type == "debugPrint") { + label = "Message"; + hint = "Text to print when node runs"; + } else if (node->type == "setAnimationState") { + label = "State (SM/State)"; + hint = "Format: stateMachineName/stateName"; + } else if (node->type == "isAnimationEnded") { + label = "State Machine"; + hint = "Name of state machine to check"; + } else if (node->type == "setBit") { + label = "Bit"; + hint = "Bit index or name"; + } else if (node->type == "checkBit") { + label = "Bit"; + hint = "Bit index or name to check"; + } else if (node->type == "setValue") { + label = "Key"; + hint = "Variable name"; + } else if (node->type == "checkValue") { + label = "Key"; + hint = "Variable name to check"; + } else if (node->type == "blackboardDump") { + label = "Label"; + hint = "Optional label prefix"; + } else if (node->type == "teleportToChild") { + label = "Child Name"; + hint = "Name of child entity under Smart Object to teleport to"; + } else if (node->type == "sendEvent") { + label = "Event Name"; + hint = "Name of the event to send via EventBus"; + } + + if (ImGui::InputText(label, buf, sizeof(buf))) + node->name = buf; + if (hint && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", hint); } - if (ImGui::InputText(label, buf, sizeof(buf))) - node->name = buf; - if (hint && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", hint); - // Params input for nodes that need it if (node->type == "setBit" || node->type == "setValue" || node->type == "checkValue" || node->type == "delay" || - node->type == "sendEvent") { + node->type == "sendEvent" || node->type == "luaTask") { char pbuf[256]; snprintf(pbuf, sizeof(pbuf), "%s", node->params.c_str()); @@ -389,6 +470,8 @@ void BehaviorTreeEditor::renderProperties(BehaviorTreeNode *node) phint = "Duration in seconds (float, default 1.0)"; else if (node->type == "sendEvent") phint = "Event params: key=val key2=val2 ..."; + else if (node->type == "luaTask") + phint = "Parameters: key=val,key2=val2 passed to Lua function"; if (ImGui::InputText("Params", pbuf, sizeof(pbuf))) node->params = pbuf; if (phint && ImGui::IsItemHovered()) diff --git a/src/features/editScene/ui/BehaviorTreeEditor.hpp b/src/features/editScene/ui/BehaviorTreeEditor.hpp index 708a330..28903e3 100644 --- a/src/features/editScene/ui/BehaviorTreeEditor.hpp +++ b/src/features/editScene/ui/BehaviorTreeEditor.hpp @@ -12,18 +12,21 @@ */ class BehaviorTreeEditor : public ComponentEditor { public: - const char *getName() const override { return "Behavior Tree"; } + const char *getName() const override + { + return "Behavior Tree"; + } struct TreeEditOp { - enum OpType { AddChild, RemoveNode, MoveNode } type; + enum OpType { AddChild, RemoveNode, MoveNode, AddLuaNode } type; BehaviorTreeNode *targetNode = nullptr; size_t childIndex = 0; Ogre::String childType; + Ogre::String luaNodeName; int moveDelta = 0; }; - void queueAddChild(BehaviorTreeNode *parent, - const Ogre::String &type); + void queueAddChild(BehaviorTreeNode *parent, const Ogre::String &type); void queueRemoveNode(BehaviorTreeNode *parent, size_t childIndex); void queueMoveNode(BehaviorTreeNode *parent, size_t childIndex, int delta); @@ -33,11 +36,9 @@ protected: BehaviorTreeComponent &bt) override; private: - void renderTree(BehaviorTreeNode &node, - BehaviorTreeNode *parent, + void renderTree(BehaviorTreeNode &node, BehaviorTreeNode *parent, BehaviorTreeNode *&selectedNode, - BehaviorTreeNode *&selectedParent, - int &id); + BehaviorTreeNode *&selectedParent, int &id); void renderProperties(BehaviorTreeNode *node); void applyEditOps(BehaviorTreeComponent &bt); diff --git a/src/features/editScene/ui/InlineBehaviorTreeEditor.cpp b/src/features/editScene/ui/InlineBehaviorTreeEditor.cpp index 8b274fb..2858e51 100644 --- a/src/features/editScene/ui/InlineBehaviorTreeEditor.cpp +++ b/src/features/editScene/ui/InlineBehaviorTreeEditor.cpp @@ -1,4 +1,5 @@ #include "InlineBehaviorTreeEditor.hpp" +#include "../lua/LuaBehaviorTreeApi.hpp" #include std::vector & @@ -17,19 +18,36 @@ void InlineBehaviorTreeEditor::applyOps(BehaviorTreeNode &root) { for (auto &op : getOps()) { BehaviorTreeNode *parent = op.parent ? op.parent : &root; - if (op.type == EditOp::Add && op.parent) { - if (op.childIndex <= op.parent->children.size()) + switch (op.type) { + case EditOp::Add: + if (op.parent && + op.childIndex <= op.parent->children.size()) op.parent->children.insert( op.parent->children.begin() + op.childIndex, BehaviorTreeNode{ .type = op.addType }); - } else if (op.type == EditOp::Remove && op.parent) { - if (op.childIndex < op.parent->children.size()) + break; + case EditOp::AddLuaNode: + if (op.parent) { + BehaviorTreeNode child; + child.type = "luaTask"; + child.name = op.luaNodeName; + op.parent->children.insert( + op.parent->children.begin() + + op.childIndex, + child); + } + break; + case EditOp::Remove: + if (op.parent && + op.childIndex < op.parent->children.size()) op.parent->children.erase( op.parent->children.begin() + op.childIndex); - } else if (op.type == EditOp::Move && op.parent) { - if (op.childIndex < op.parent->children.size()) { + break; + case EditOp::Move: + if (op.parent && + op.childIndex < op.parent->children.size()) { size_t newIndex = (int)op.childIndex + op.moveDelta; if (newIndex >= 0 && @@ -47,6 +65,7 @@ void InlineBehaviorTreeEditor::applyOps(BehaviorTreeNode &root) child); } } + break; } } getOps().clear(); @@ -98,9 +117,36 @@ static ImVec4 typeColorVec(const char *type) return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); if (!strcmp(type, "sendEvent")) return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); + if (!strcmp(type, "luaTask")) + return ImVec4(0.67f, 0.4f, 1.0f, 1.0f); return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); } +// Helper: render a submenu of registered Lua node names. +// Returns true if a node was selected. +bool InlineBehaviorTreeEditor::renderLuaNodeSubmenu(BehaviorTreeNode *parent, + size_t childIndex) +{ + auto names = editScene::getRegisteredLuaNodeNames(); + if (names.empty()) { + ImGui::MenuItem("(no Lua nodes registered)", nullptr, false, + false); + return false; + } + + bool selected = false; + for (const auto &name : names) { + if (ImGui::MenuItem(name.c_str())) { + queueOp(EditOp{ .type = EditOp::AddLuaNode, + .parent = parent, + .childIndex = childIndex, + .luaNodeName = name }); + selected = true; + } + } + return selected; +} + void InlineBehaviorTreeEditor::renderNode(BehaviorTreeNode &node, BehaviorTreeNode *parent, BehaviorTreeNode *&selected, int &id) @@ -153,6 +199,10 @@ void InlineBehaviorTreeEditor::renderNode(BehaviorTreeNode &node, .childIndex = node.children.size(), .addType = "sendEvent" }); } + if (ImGui::BeginMenu("Add Lua Task")) { + renderLuaNodeSubmenu(&node, node.children.size()); + ImGui::EndMenu(); + } if (parent && ImGui::MenuItem("Remove")) { for (size_t i = 0; i < parent->children.size(); i++) { if (&parent->children[i] == &node) { @@ -269,7 +319,8 @@ void InlineBehaviorTreeEditor::renderProps(BehaviorTreeNode *node) "teleportToChild", "disablePhysics", "enablePhysics", - "sendEvent" }; + "sendEvent", + "luaTask" }; int current = 0; for (int i = 0; i < IM_ARRAYSIZE(types); i++) { if (node->type == types[i]) { @@ -282,52 +333,83 @@ void InlineBehaviorTreeEditor::renderProps(BehaviorTreeNode *node) } if (node->isLeaf()) { - char buf[256]; - snprintf(buf, sizeof(buf), "%s", node->name.c_str()); + // For luaTask nodes, show a dropdown of registered Lua nodes + if (node->type == "luaTask") { + auto names = editScene::getRegisteredLuaNodeNames(); + if (names.empty()) { + ImGui::TextDisabled("No Lua nodes registered"); + } else { + int currentIdx = -1; + for (size_t i = 0; i < names.size(); i++) { + if (names[i] == node->name) { + currentIdx = (int)i; + break; + } + } + std::vector cstrs; + cstrs.reserve(names.size()); + for (const auto &n : names) + cstrs.push_back(n.c_str()); - const char *label = "Name"; - const char *hint = nullptr; - if (node->type == "debugPrint") { - label = "Message"; - hint = "Text to print when node runs"; - } else if (node->type == "setAnimationState") { - label = "State (SM/State)"; - hint = "Format: stateMachineName/stateName"; - } else if (node->type == "isAnimationEnded") { - label = "State Machine"; - hint = "Name of state machine to check"; - } else if (node->type == "setBit") { - label = "Bit"; - hint = "Bit index or name"; - } else if (node->type == "checkBit") { - label = "Bit"; - hint = "Bit index or name to check"; - } else if (node->type == "setValue") { - label = "Key"; - hint = "Variable name"; - } else if (node->type == "checkValue") { - label = "Key"; - hint = "Variable name to check"; - } else if (node->type == "blackboardDump") { - label = "Label"; - hint = "Optional label prefix"; - } else if (node->type == "teleportToChild") { - label = "Child Name"; - hint = "Name of child entity under Smart Object to teleport to"; - } else if (node->type == "sendEvent") { - label = "Event Name"; - hint = "Name of the event to send via EventBus"; + if (ImGui::Combo("Lua Node", ¤tIdx, + cstrs.data(), + (int)cstrs.size())) { + if (currentIdx >= 0 && + currentIdx < (int)names.size()) + node->name = names[currentIdx]; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip( + "Select a registered Lua node handler"); + } + } else { + char buf[256]; + snprintf(buf, sizeof(buf), "%s", node->name.c_str()); + + const char *label = "Name"; + const char *hint = nullptr; + if (node->type == "debugPrint") { + label = "Message"; + hint = "Text to print when node runs"; + } else if (node->type == "setAnimationState") { + label = "State (SM/State)"; + hint = "Format: stateMachineName/stateName"; + } else if (node->type == "isAnimationEnded") { + label = "State Machine"; + hint = "Name of state machine to check"; + } else if (node->type == "setBit") { + label = "Bit"; + hint = "Bit index or name"; + } else if (node->type == "checkBit") { + label = "Bit"; + hint = "Bit index or name to check"; + } else if (node->type == "setValue") { + label = "Key"; + hint = "Variable name"; + } else if (node->type == "checkValue") { + label = "Key"; + hint = "Variable name to check"; + } else if (node->type == "blackboardDump") { + label = "Label"; + hint = "Optional label prefix"; + } else if (node->type == "teleportToChild") { + label = "Child Name"; + hint = "Name of child entity under Smart Object to teleport to"; + } else if (node->type == "sendEvent") { + label = "Event Name"; + hint = "Name of the event to send via EventBus"; + } + + if (ImGui::InputText(label, buf, sizeof(buf))) + node->name = buf; + if (hint && ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", hint); } - if (ImGui::InputText(label, buf, sizeof(buf))) - node->name = buf; - if (hint && ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", hint); - // Params input for nodes that need it if (node->type == "setBit" || node->type == "setValue" || node->type == "checkValue" || node->type == "delay" || - node->type == "sendEvent") { + node->type == "sendEvent" || node->type == "luaTask") { char pbuf[256]; snprintf(pbuf, sizeof(pbuf), "%s", node->params.c_str()); @@ -342,6 +424,8 @@ void InlineBehaviorTreeEditor::renderProps(BehaviorTreeNode *node) phint = "Duration in seconds (float, default 1.0)"; else if (node->type == "sendEvent") phint = "Event params: key=val key2=val2 ..."; + else if (node->type == "luaTask") + phint = "Parameters: key=val,key2=val2 passed to Lua function"; if (ImGui::InputText("Params", pbuf, sizeof(pbuf))) node->params = pbuf; if (phint && ImGui::IsItemHovered()) diff --git a/src/features/editScene/ui/InlineBehaviorTreeEditor.hpp b/src/features/editScene/ui/InlineBehaviorTreeEditor.hpp index 136b656..6a6b37c 100644 --- a/src/features/editScene/ui/InlineBehaviorTreeEditor.hpp +++ b/src/features/editScene/ui/InlineBehaviorTreeEditor.hpp @@ -15,20 +15,21 @@ public: private: struct EditOp { - enum Type { Add, Remove, Move } type; + enum Type { Add, Remove, Move, AddLuaNode } type; BehaviorTreeNode *parent = nullptr; size_t childIndex = 0; Ogre::String addType; + Ogre::String luaNodeName; int moveDelta = 0; }; static void queueOp(EditOp op); static void applyOps(BehaviorTreeNode &root); - static void renderNode(BehaviorTreeNode &node, - BehaviorTreeNode *parent, - BehaviorTreeNode *&selected, - int &id); + static void renderNode(BehaviorTreeNode &node, BehaviorTreeNode *parent, + BehaviorTreeNode *&selected, int &id); static void renderProps(BehaviorTreeNode *node); + static bool renderLuaNodeSubmenu(BehaviorTreeNode *parent, + size_t childIndex); static std::vector &getOps(); };